feat: product item
This commit is contained in:
parent
6f27ff3bbc
commit
2e2bdecc48
|
@ -7,24 +7,26 @@ import (
|
|||
)
|
||||
|
||||
type ProductItems struct {
|
||||
ID primitive.ObjectID `bson:"_id,omitempty"` // 專案 ID
|
||||
ProductID string `bson:"product_id"` // 對應的專案 ID
|
||||
Name string `bson:"name" json:"name"` // 名稱
|
||||
Cover string `bson:"cover"` // 封面
|
||||
Description string `bson:"description"` // 描述
|
||||
ShortDescription string `bson:"short_description"` // 封面簡短描述
|
||||
Price decimal.Decimal `bson:"price"` // 價格
|
||||
SKU string `bson:"sku"` // 型號:對應顯示 Item 的 FK
|
||||
TimeSeries product.TimeSeries `bson:"time_series"` // 時段種類
|
||||
Media []Media `bson:"medias,omitempty"` // 專案動態內容(圖片或者影片)
|
||||
AverageRating float64 `bson:"average_rating"` // 綜合評價(如:4.5 顆星)
|
||||
AverageRatingUpdateTime int64 `bson:"average_rating_time"` // 更新評價的時間
|
||||
Orders uint64 `bson:"total_orders"` // 總接單數
|
||||
OrdersUpdateTime int64 `bson:"total_orders_update_time"` // 更新總接單數的時間
|
||||
UpdatedAt int64 `bson:"updated_at" json:"updated_at"` // 更新時間
|
||||
CreatedAt int64 `bson:"created_at" json:"created_at"` // 創建時間
|
||||
ID primitive.ObjectID `bson:"_id,omitempty"` // 專案 ID
|
||||
ReferenceID string `bson:"reference_id"` // 對應的專案 ID
|
||||
Name string `bson:"name"` // 名稱
|
||||
Description string `bson:"description"` // 描述
|
||||
ShortDescription string `bson:"short_description"` // 封面簡短描述
|
||||
IsUnLimit bool `bson:"is_un_limit"` // 是否沒有數量上限
|
||||
IsFree bool `bson:"is_free"` // 是否為免費品項(贈品) -> 開啟就是自訂金額
|
||||
Stock uint64 `bson:"stock"` // 庫存總數
|
||||
Price decimal.Decimal `bson:"price"` // 價格
|
||||
SKU string `bson:"sku"` // 型號:對應顯示 Item 的 FK
|
||||
TimeSeries product.TimeSeries `bson:"time_series"` // 時段種類
|
||||
Media []Media `bson:"media,omitempty"` // 專案動態內容(圖片或者影片)
|
||||
Status product.ItemStatus `bson:"status"` // 商品狀態
|
||||
Freight []CustomFields `bson:"freight,omitempty"` // 運費
|
||||
CustomFields []CustomFields `bson:"custom_fields,omitempty"` // 自定義屬性
|
||||
SalesCount uint64 `bson:"sales_count" ` // 已賣出數量(相反,減到零就不能在賣)
|
||||
UpdatedAt int64 `bson:"updated_at"` // 更新時間
|
||||
CreatedAt int64 `bson:"created_at"` // 創建時間
|
||||
}
|
||||
|
||||
func (p *ProductItems) CollectionName() string {
|
||||
return "product"
|
||||
return "product_items"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package product
|
||||
|
||||
type ItemStatus int8
|
||||
|
||||
func (p *ItemStatus) ToString() string {
|
||||
s, _ := StatusToString(*p)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
const (
|
||||
StatusActive ItemStatus = 1 + iota // 上架
|
||||
StatusInactive // 下架
|
||||
StatusOutOfStock // 缺貨
|
||||
)
|
||||
|
||||
const (
|
||||
StatusActiveStr = "active"
|
||||
StatusInactiveStr = "inactive"
|
||||
StatusOutOfStockStr = "out_of_stock"
|
||||
)
|
||||
|
||||
var statusToStringMap = map[ItemStatus]string{
|
||||
StatusActive: StatusActiveStr,
|
||||
StatusInactive: StatusInactiveStr,
|
||||
StatusOutOfStock: StatusOutOfStockStr,
|
||||
}
|
||||
|
||||
var stringToStatusMap = map[string]ItemStatus{
|
||||
StatusActiveStr: StatusActive,
|
||||
StatusInactiveStr: StatusInactive,
|
||||
StatusOutOfStockStr: StatusOutOfStock,
|
||||
}
|
||||
|
||||
// StatusToString 將 ProductItemStatus 轉換為字串
|
||||
func StatusToString(status ItemStatus) (string, bool) {
|
||||
str, ok := statusToStringMap[status]
|
||||
|
||||
return str, ok
|
||||
}
|
||||
|
||||
// StringToItemStatus 將字串轉換為 ProductItemStatus
|
||||
func StringToItemStatus(statusStr string) (ItemStatus, bool) {
|
||||
status, ok := stringToStatusMap[statusStr]
|
||||
|
||||
return status, ok
|
||||
}
|
|
@ -15,9 +15,14 @@ func (key RedisKey) With(s ...string) RedisKey {
|
|||
}
|
||||
|
||||
const (
|
||||
GetProductRedisKey RedisKey = "get"
|
||||
GetProductRedisKey RedisKey = "get"
|
||||
GetProductItemRedisKey RedisKey = "get_item"
|
||||
)
|
||||
|
||||
func GetProductRK(id string) string {
|
||||
return GetProductRedisKey.With(id).ToString()
|
||||
}
|
||||
|
||||
func GetProductItemRK(id string) string {
|
||||
return GetProductItemRedisKey.With(id).ToString()
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
package repository
|
||||
|
||||
type Index interface{}
|
|
@ -19,6 +19,11 @@ type ProductRepository interface {
|
|||
Transaction(ctx context.Context,
|
||||
fn func(sessCtx mongo.SessionContext) (any, error),
|
||||
opts ...*options.TransactionOptions) error
|
||||
ProductIndex
|
||||
}
|
||||
|
||||
type ProductIndex interface {
|
||||
Index20250317001UP(ctx context.Context) (*mongo.Cursor, error)
|
||||
}
|
||||
|
||||
// ProductQueryParams 用於查詢專案的參數
|
||||
|
|
|
@ -1,3 +1,76 @@
|
|||
package repository
|
||||
|
||||
type ProductItemRepo interface{}
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity"
|
||||
"code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/product"
|
||||
"github.com/shopspring/decimal"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
)
|
||||
|
||||
// ProductItemRepository 定義商品相關操作的接口
|
||||
type ProductItemRepository interface {
|
||||
ProductItemIndex
|
||||
ProductItemBasic
|
||||
ProductItemStatistics
|
||||
}
|
||||
|
||||
type ProductItemIndex interface {
|
||||
Index20250317001UP(ctx context.Context) (*mongo.Cursor, error)
|
||||
}
|
||||
|
||||
// ProductItemBasic 基礎操作
|
||||
type ProductItemBasic interface {
|
||||
// Insert 一次新增多筆
|
||||
Insert(ctx context.Context, item []entity.ProductItems) error
|
||||
// Delete 刪除商品
|
||||
Delete(ctx context.Context, ids []string) error
|
||||
// Update 更新商品
|
||||
Update(ctx context.Context, id string, param *ProductUpdateItem) error
|
||||
// DeleteByReferenceID 刪除某Project 下所有
|
||||
DeleteByReferenceID(ctx context.Context, id string) error
|
||||
// FindByID 根據 ID 查找商品
|
||||
FindByID(ctx context.Context, id string) (*entity.ProductItems, error)
|
||||
// ListProductItem 查找商品列表
|
||||
ListProductItem(ctx context.Context, param ProductItemQueryParams) ([]entity.ProductItems, int64, error)
|
||||
// UpdateStatus 更新品相狀態
|
||||
UpdateStatus(ctx context.Context, id string, status product.ItemStatus) error
|
||||
}
|
||||
|
||||
type ProductItemQueryParams struct {
|
||||
PageSize int64 // 每頁顯示的專案數量
|
||||
PageIndex int64 // 要查詢的頁數
|
||||
ItemID []string // Item 的 ID
|
||||
ReferenceID *string // 對應參照的ID
|
||||
IsFree *bool // 是否為免費品項(贈品)
|
||||
Status *product.ItemStatus // 商品狀態
|
||||
}
|
||||
type ProductUpdateItem struct {
|
||||
Name *string // 名稱
|
||||
Description *string // 描述
|
||||
Price *decimal.Decimal // 價格
|
||||
Stock *int64 // 庫存總數
|
||||
ShortDescription *string // 封面簡短描述
|
||||
IsUnLimit *bool // 是否沒有數量上限
|
||||
IsFree *bool // 是否為免費品項(贈品) -> 開啟就是自訂金額
|
||||
SKU *string // 型號:對應顯示 Item 的 FK
|
||||
TimeSeries *product.TimeSeries // 時段種類
|
||||
Media []entity.Media // 專案動態內容(圖片或者影片)
|
||||
Freight []entity.CustomFields // 運費
|
||||
CustomFields []entity.CustomFields // 自定義屬性
|
||||
}
|
||||
|
||||
type ProductItemStatistics interface {
|
||||
// IncSalesCount 更新接單總數 -> 新增
|
||||
IncSalesCount(ctx context.Context, id string, count int64) error
|
||||
// DecSalesCount 減少接單總數 -> 取消
|
||||
DecSalesCount(ctx context.Context, id string, count int64) error
|
||||
// GetSalesCount 取得這item 的接單數
|
||||
GetSalesCount(ctx context.Context, ids []string) ([]ProductItemSalesCount, error)
|
||||
}
|
||||
|
||||
type ProductItemSalesCount struct {
|
||||
ID string
|
||||
Count uint64
|
||||
}
|
||||
|
|
|
@ -256,3 +256,12 @@ func (repo *ProductRepository) Transaction(
|
|||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (repo *ProductRepository) Index20250317001UP(ctx context.Context) (*mongo.Cursor, error) {
|
||||
// 等價於 db.account.createIndex({"create_at": 1})
|
||||
repo.DB.PopulateIndex(ctx, "uid", 1, false)
|
||||
repo.DB.PopulateIndex(ctx, "slug", 1, true)
|
||||
repo.DB.PopulateIndex(ctx, "category", 1, false)
|
||||
|
||||
return repo.DB.GetClient().Indexes().List(ctx)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,351 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.30cm.net/digimon/app-cloudep-product-service/pkg/domain"
|
||||
"code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity"
|
||||
"code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/product"
|
||||
"code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/repository"
|
||||
mgo "code.30cm.net/digimon/library-go/mongo"
|
||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
type ProductItemRepositoryParam struct {
|
||||
Conf *mgo.Conf
|
||||
CacheConf cache.CacheConf
|
||||
DBOpts []mon.Option
|
||||
CacheOpts []cache.Option
|
||||
}
|
||||
|
||||
type ProductItemRepository struct {
|
||||
DB mgo.DocumentDBWithCacheUseCase
|
||||
}
|
||||
|
||||
func NewProductItemRepository(param ProductItemRepositoryParam) repository.ProductItemRepository {
|
||||
e := entity.ProductItems{}
|
||||
documentDB, err := mgo.MustDocumentDBWithCache(
|
||||
param.Conf,
|
||||
e.CollectionName(),
|
||||
param.CacheConf,
|
||||
param.DBOpts,
|
||||
param.CacheOpts,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &ProductItemRepository{
|
||||
DB: documentDB,
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *ProductItemRepository) Insert(ctx context.Context, items []entity.ProductItems) error {
|
||||
now := time.Now().UTC().UnixNano()
|
||||
docs := make([]any, 0, len(items))
|
||||
|
||||
for i := range items {
|
||||
if items[i].ID.IsZero() {
|
||||
items[i].ID = primitive.NewObjectID()
|
||||
items[i].CreatedAt = now
|
||||
items[i].UpdatedAt = now
|
||||
}
|
||||
docs = append(docs, items[i])
|
||||
}
|
||||
if len(docs) == 0 {
|
||||
return nil
|
||||
}
|
||||
_, err := repo.DB.GetClient().InsertMany(ctx, docs)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (repo *ProductItemRepository) FindByID(ctx context.Context, id string) (*entity.ProductItems, error) {
|
||||
oid, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidObjectID
|
||||
}
|
||||
|
||||
var data entity.ProductItems
|
||||
|
||||
rk := domain.GetProductItemRK(id)
|
||||
err = repo.DB.FindOne(ctx, rk, &data, bson.M{"_id": oid})
|
||||
switch {
|
||||
case err == nil:
|
||||
return &data, nil
|
||||
case errors.Is(err, mon.ErrNotFound):
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *ProductItemRepository) Delete(ctx context.Context, ids []string) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
objectIDs := make([]primitive.ObjectID, 0, len(ids))
|
||||
cacheKeys := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
oid, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return ErrInvalidObjectID
|
||||
}
|
||||
objectIDs = append(objectIDs, oid)
|
||||
cacheKeys = append(cacheKeys, domain.GetProductItemRK(id))
|
||||
}
|
||||
|
||||
// 刪除多筆 Mongo 資料
|
||||
filter := bson.M{"_id": bson.M{"$in": objectIDs}}
|
||||
_, err := repo.DB.GetClient().DeleteMany(ctx, filter)
|
||||
|
||||
// 可選:刪除 Redis 快取(每個 ID 都清除)
|
||||
_ = repo.DB.DelCache(ctx, cacheKeys...)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (repo *ProductItemRepository) Update(ctx context.Context, id string, param *repository.ProductUpdateItem) error {
|
||||
// 將 `id` 轉換為 MongoDB 的 ObjectID
|
||||
objectID, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return ErrInvalidObjectID
|
||||
}
|
||||
// 構建更新文檔
|
||||
setFields := bson.M{}
|
||||
// 檢查並添加需要更新的欄位
|
||||
if param.Name != nil {
|
||||
setFields["name"] = *param.Name
|
||||
}
|
||||
if param.Description != nil {
|
||||
setFields["description"] = *param.Description
|
||||
}
|
||||
if param.ShortDescription != nil {
|
||||
setFields["short_description"] = *param.ShortDescription
|
||||
}
|
||||
if param.Price != nil {
|
||||
setFields["price"] = *param.Price
|
||||
}
|
||||
if param.Stock != nil {
|
||||
setFields["stock"] = *param.Stock
|
||||
}
|
||||
if param.IsUnLimit != nil {
|
||||
setFields["is_un_limit"] = *param.IsUnLimit
|
||||
}
|
||||
if param.IsFree != nil {
|
||||
setFields["is_free"] = *param.IsFree
|
||||
}
|
||||
if param.SKU != nil {
|
||||
setFields["sku"] = *param.SKU
|
||||
}
|
||||
if param.TimeSeries != nil {
|
||||
setFields["time_series"] = *param.TimeSeries
|
||||
}
|
||||
if len(param.Media) > 0 {
|
||||
setFields["media"] = param.Media
|
||||
}
|
||||
if param.CustomFields != nil {
|
||||
setFields["custom_fields"] = param.CustomFields
|
||||
}
|
||||
if param.Freight != nil {
|
||||
setFields["freight"] = param.Freight
|
||||
}
|
||||
|
||||
// 如果沒有任何需要更新的內容,直接返回
|
||||
if len(setFields) == 0 {
|
||||
return fmt.Errorf("no fields to update")
|
||||
}
|
||||
|
||||
// 執行更新操作
|
||||
filter := bson.M{"_id": objectID}
|
||||
rk := domain.GetProductItemRK(id)
|
||||
_, err = repo.DB.UpdateOne(ctx, rk, filter, bson.M{
|
||||
"$set": setFields,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update product item: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *ProductItemRepository) DeleteByReferenceID(ctx context.Context, id string) error {
|
||||
// 1. 查詢所有符合 reference_id 的項目,只取 _id 欄位
|
||||
filter := bson.M{"reference_id": id}
|
||||
projection := bson.M{"_id": 1}
|
||||
opts := options.Find().SetProjection(projection)
|
||||
|
||||
var items []entity.ProductItems
|
||||
err := repo.DB.GetClient().Find(ctx, &items, filter, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 刪除這些 ID 對應的 Redis 快取
|
||||
cacheKeys := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
cacheKeys = append(cacheKeys, domain.GetProductItemRK(item.ID.Hex()))
|
||||
}
|
||||
|
||||
if len(cacheKeys) > 0 {
|
||||
_ = repo.DB.DelCache(ctx, cacheKeys...)
|
||||
}
|
||||
|
||||
// 3. 刪除 DB 中的資料
|
||||
delFilter := bson.M{"reference_id": id}
|
||||
_, err = repo.DB.GetClient().DeleteMany(ctx, delFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *ProductItemRepository) ListProductItem(ctx context.Context, param repository.ProductItemQueryParams) ([]entity.ProductItems, int64, error) {
|
||||
// 構建查詢過濾器
|
||||
filter := bson.M{}
|
||||
|
||||
ids := make([]primitive.ObjectID, 0, len(param.ItemID))
|
||||
for _, item := range param.ItemID {
|
||||
oid, err := primitive.ObjectIDFromHex(item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, oid)
|
||||
}
|
||||
// 添加篩選條件
|
||||
if len(param.ItemID) > 0 {
|
||||
filter["_id"] = bson.M{"$in": ids}
|
||||
}
|
||||
if param.ReferenceID != nil {
|
||||
filter["reference_id"] = *param.ReferenceID
|
||||
}
|
||||
if param.IsFree != nil {
|
||||
filter["is_free"] = *param.IsFree
|
||||
}
|
||||
if param.Status != nil {
|
||||
filter["status"] = *param.Status
|
||||
}
|
||||
|
||||
// 設置排序選項
|
||||
opts := options.Find().SetSkip((param.PageIndex - 1) * param.PageSize).SetLimit(param.PageSize)
|
||||
opts.SetSort(bson.D{{Key: "created_at", Value: -1}})
|
||||
|
||||
// 查詢符合條件的總數
|
||||
count, err := repo.DB.GetClient().CountDocuments(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 執行查詢並獲取結果
|
||||
var products []entity.ProductItems
|
||||
err = repo.DB.GetClient().Find(ctx, &products, filter, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return products, count, nil
|
||||
}
|
||||
|
||||
func (repo *ProductItemRepository) UpdateStatus(ctx context.Context, id string, status product.ItemStatus) error {
|
||||
objectID, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return ErrInvalidObjectID
|
||||
}
|
||||
|
||||
filter := bson.M{"_id": objectID}
|
||||
update := bson.M{"$set": bson.M{"status": status}}
|
||||
|
||||
rk := domain.GetProductItemRK(id)
|
||||
_, err = repo.DB.UpdateOne(ctx, rk, filter, update)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update status: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *ProductItemRepository) IncSalesCount(ctx context.Context, id string, count int64) error {
|
||||
objectID, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return ErrInvalidObjectID
|
||||
}
|
||||
|
||||
filter := bson.M{"_id": objectID}
|
||||
update := bson.M{"$inc": bson.M{"sales_count": count}}
|
||||
|
||||
rk := domain.GetProductItemRK(id)
|
||||
_, err = repo.DB.UpdateOne(ctx, rk, filter, update)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrease stock: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *ProductItemRepository) DecSalesCount(ctx context.Context, id string, count int64) error {
|
||||
objectID, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return ErrInvalidObjectID
|
||||
}
|
||||
|
||||
filter := bson.M{"_id": objectID, "sales_count": bson.M{"$gte": count}}
|
||||
update := bson.M{"$inc": bson.M{"sales_count": -count}}
|
||||
|
||||
rk := domain.GetProductItemRK(id)
|
||||
_, err = repo.DB.UpdateOne(ctx, rk, filter, update)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decrease stock: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *ProductItemRepository) GetSalesCount(ctx context.Context, ids []string) ([]repository.ProductItemSalesCount, error) {
|
||||
objectIDs := make([]primitive.ObjectID, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
objectID, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
objectIDs = append(objectIDs, objectID)
|
||||
}
|
||||
|
||||
if len(objectIDs) == 0 {
|
||||
return nil, ErrInvalidObjectID
|
||||
}
|
||||
|
||||
result := make([]entity.ProductItems, 0, len(ids))
|
||||
filter := bson.M{"_id": bson.M{"$in": objectIDs}}
|
||||
|
||||
err := repo.DB.GetClient().Find(ctx, &result, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stockMap := make([]repository.ProductItemSalesCount, 0, len(result))
|
||||
for _, item := range result {
|
||||
stockMap = append(stockMap, repository.ProductItemSalesCount{
|
||||
ID: item.ID.Hex(),
|
||||
Count: item.SalesCount,
|
||||
})
|
||||
}
|
||||
|
||||
return stockMap, nil
|
||||
}
|
||||
|
||||
func (repo *ProductItemRepository) Index20250317001UP(ctx context.Context) (*mongo.Cursor, error) {
|
||||
repo.DB.PopulateIndex(ctx, "reference_id", 1, false)
|
||||
repo.DB.PopulateIndex(ctx, "status", 1, false)
|
||||
|
||||
return repo.DB.GetClient().Indexes().List(ctx)
|
||||
}
|
|
@ -0,0 +1,800 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/entity"
|
||||
"code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/product"
|
||||
"code.30cm.net/digimon/app-cloudep-product-service/pkg/domain/repository"
|
||||
mgo "code.30cm.net/digimon/library-go/mongo"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func SetupTestProductItemRepository(db string) (repository.ProductItemRepository, func(), error) {
|
||||
h, p, tearDown, err := startMongoContainer()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
s, _ := miniredis.Run()
|
||||
|
||||
conf := &mgo.Conf{
|
||||
Schema: Schema,
|
||||
Host: fmt.Sprintf("%s:%s", h, p),
|
||||
Database: db,
|
||||
MaxStaleness: 300,
|
||||
MaxPoolSize: 100,
|
||||
MinPoolSize: 100,
|
||||
MaxConnIdleTime: 300,
|
||||
Compressors: []string{},
|
||||
EnableStandardReadWriteSplitMode: false,
|
||||
ConnectTimeoutMs: 3000,
|
||||
}
|
||||
|
||||
cacheConf := cache.CacheConf{
|
||||
cache.NodeConf{
|
||||
RedisConf: redis.RedisConf{
|
||||
Host: s.Addr(),
|
||||
Type: redis.NodeType,
|
||||
},
|
||||
Weight: 100,
|
||||
},
|
||||
}
|
||||
|
||||
cacheOpts := []cache.Option{
|
||||
cache.WithExpiry(1000 * time.Microsecond),
|
||||
cache.WithNotFoundExpiry(1000 * time.Microsecond),
|
||||
}
|
||||
|
||||
param := ProductItemRepositoryParam{
|
||||
Conf: conf,
|
||||
CacheConf: cacheConf,
|
||||
CacheOpts: cacheOpts,
|
||||
DBOpts: []mon.Option{
|
||||
mgo.SetCustomDecimalType(),
|
||||
mgo.InitMongoOptions(*conf),
|
||||
},
|
||||
}
|
||||
|
||||
repo := NewProductItemRepository(param)
|
||||
_, _ = repo.Index20250317001UP(context.Background())
|
||||
|
||||
return repo, tearDown, nil
|
||||
}
|
||||
|
||||
func TestInsertProductItems(t *testing.T) {
|
||||
model, tearDown, err := SetupTestProductItemRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 建立多筆 items
|
||||
items := []entity.ProductItems{
|
||||
{
|
||||
ReferenceID: primitive.NewObjectID().Hex(),
|
||||
Name: "Item A",
|
||||
},
|
||||
{
|
||||
ReferenceID: primitive.NewObjectID().Hex(),
|
||||
Name: "Item B",
|
||||
},
|
||||
}
|
||||
|
||||
// 呼叫插入
|
||||
err = model.Insert(context.Background(), items)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 驗證插入是否成功(逐筆查詢)
|
||||
for _, item := range items {
|
||||
// 檢查 ID 是否自動填入
|
||||
assert.False(t, item.ID.IsZero(), "ID should be generated")
|
||||
assert.NotZero(t, item.CreatedAt)
|
||||
assert.NotZero(t, item.UpdatedAt)
|
||||
|
||||
// 查詢 DB 確認存在
|
||||
var result *entity.ProductItems
|
||||
result, err = model.FindByID(context.Background(), item.ID.Hex())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, item.Name, result.Name)
|
||||
}
|
||||
|
||||
// 🧪 空陣列插入測試(不應報錯)
|
||||
t.Run("Insert empty slice", func(t *testing.T) {
|
||||
err := model.Insert(context.Background(), []entity.ProductItems{})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteProductItem(t *testing.T) {
|
||||
model, tearDown, err := SetupTestProductItemRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 建立多筆 items
|
||||
items := []entity.ProductItems{
|
||||
{
|
||||
ReferenceID: primitive.NewObjectID().Hex(),
|
||||
Name: "Item A",
|
||||
},
|
||||
{
|
||||
ReferenceID: primitive.NewObjectID().Hex(),
|
||||
Name: "Item B",
|
||||
},
|
||||
}
|
||||
|
||||
// 呼叫插入
|
||||
err = model.Insert(context.Background(), items)
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
inputID string
|
||||
expectErr error
|
||||
checkAfter bool // 是否需要確認資料已刪除
|
||||
}{
|
||||
{
|
||||
name: "Delete existing product",
|
||||
inputID: items[0].ID.Hex(),
|
||||
expectErr: nil,
|
||||
checkAfter: true,
|
||||
},
|
||||
{
|
||||
name: "Delete non-existing product",
|
||||
inputID: primitive.NewObjectID().Hex(),
|
||||
expectErr: nil,
|
||||
checkAfter: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid ObjectID format",
|
||||
inputID: "not-an-object-id",
|
||||
expectErr: ErrInvalidObjectID,
|
||||
checkAfter: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := model.Delete(context.Background(), []string{tt.inputID})
|
||||
if tt.expectErr != nil {
|
||||
assert.ErrorIs(t, err, tt.expectErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// 驗證資料是否真的刪除了(僅當需要)
|
||||
if tt.checkAfter {
|
||||
_, err := model.FindByID(context.Background(), tt.inputID)
|
||||
assert.ErrorIs(t, err, ErrNotFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListProductItem(t *testing.T) {
|
||||
model, tearDown, err := SetupTestProductItemRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
now := time.Now().UnixNano()
|
||||
rfcID := primitive.NewObjectID().Hex()
|
||||
// 插入測試資料
|
||||
item1 := entity.ProductItems{
|
||||
ID: primitive.NewObjectID(),
|
||||
ReferenceID: rfcID,
|
||||
Name: "Item A",
|
||||
IsFree: true,
|
||||
Status: product.StatusActive,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
item2 := entity.ProductItems{
|
||||
ID: primitive.NewObjectID(),
|
||||
ReferenceID: rfcID,
|
||||
Name: "Item B",
|
||||
IsFree: false,
|
||||
Status: product.StatusInactive,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
item3 := entity.ProductItems{
|
||||
ID: primitive.NewObjectID(),
|
||||
ReferenceID: rfcID,
|
||||
Name: "Item C",
|
||||
IsFree: true,
|
||||
Status: product.StatusActive,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err = model.Insert(context.Background(), []entity.ProductItems{item1, item2, item3})
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
params repository.ProductItemQueryParams
|
||||
expectCount int64
|
||||
expectIDs []primitive.ObjectID
|
||||
}{
|
||||
{
|
||||
name: "Filter by ReferenceID",
|
||||
params: repository.ProductItemQueryParams{
|
||||
ReferenceID: ptr(rfcID),
|
||||
PageSize: 10,
|
||||
PageIndex: 1,
|
||||
},
|
||||
expectCount: 3,
|
||||
expectIDs: []primitive.ObjectID{item3.ID, item2.ID, item1.ID},
|
||||
},
|
||||
{
|
||||
name: "Filter by IsFree = true",
|
||||
params: repository.ProductItemQueryParams{
|
||||
IsFree: ptr(true),
|
||||
PageSize: 10,
|
||||
PageIndex: 1,
|
||||
},
|
||||
expectCount: 2,
|
||||
expectIDs: []primitive.ObjectID{item3.ID, item1.ID},
|
||||
},
|
||||
{
|
||||
name: "Filter by Status = 2",
|
||||
params: repository.ProductItemQueryParams{
|
||||
Status: ptr(product.StatusInactive),
|
||||
PageSize: 10,
|
||||
PageIndex: 1,
|
||||
},
|
||||
expectCount: 1,
|
||||
expectIDs: []primitive.ObjectID{item2.ID},
|
||||
},
|
||||
{
|
||||
name: "Filter by ItemIDs",
|
||||
params: repository.ProductItemQueryParams{
|
||||
ItemID: []string{item1.ID.Hex(), item2.ID.Hex()},
|
||||
PageSize: 10,
|
||||
PageIndex: 1,
|
||||
},
|
||||
expectCount: 2,
|
||||
expectIDs: []primitive.ObjectID{item2.ID, item1.ID},
|
||||
},
|
||||
{
|
||||
name: "Pagination works",
|
||||
params: repository.ProductItemQueryParams{
|
||||
PageSize: 1,
|
||||
PageIndex: 2,
|
||||
},
|
||||
expectCount: 3,
|
||||
expectIDs: []primitive.ObjectID{item2.ID},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
results, count, err := model.ListProductItem(context.Background(), tt.params)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectCount, count)
|
||||
|
||||
var gotIDs []primitive.ObjectID
|
||||
for _, r := range results {
|
||||
gotIDs = append(gotIDs, r.ID)
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, tt.expectIDs, gotIDs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteByReferenceID(t *testing.T) {
|
||||
model, tearDown, err := SetupTestProductItemRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
now := time.Now().UnixNano()
|
||||
|
||||
// 插入測試資料
|
||||
refID := primitive.NewObjectID().Hex()
|
||||
item1 := entity.ProductItems{
|
||||
ID: primitive.NewObjectID(),
|
||||
ReferenceID: refID,
|
||||
Name: "Item A",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
item2 := entity.ProductItems{
|
||||
ID: primitive.NewObjectID(),
|
||||
ReferenceID: refID,
|
||||
Name: "Item B",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
itemOther := entity.ProductItems{
|
||||
ID: primitive.NewObjectID(),
|
||||
ReferenceID: primitive.NewObjectID().Hex(),
|
||||
Name: "Should not be deleted",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err = model.Insert(ctx, []entity.ProductItems{item1, item2, itemOther})
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
referenceID string
|
||||
expectDeleted []primitive.ObjectID
|
||||
expectRemained []primitive.ObjectID
|
||||
}{
|
||||
{
|
||||
name: "Delete existing reference_id items",
|
||||
referenceID: refID,
|
||||
expectDeleted: []primitive.ObjectID{item1.ID, item2.ID},
|
||||
expectRemained: []primitive.ObjectID{itemOther.ID},
|
||||
},
|
||||
{
|
||||
name: "Delete non-existent reference_id",
|
||||
referenceID: "no-match-ref",
|
||||
expectDeleted: nil,
|
||||
expectRemained: []primitive.ObjectID{itemOther.ID},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := model.DeleteByReferenceID(ctx, tt.referenceID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 檢查指定應被刪除的項目是否真的被刪除
|
||||
for _, id := range tt.expectDeleted {
|
||||
_, err = model.FindByID(ctx, id.Hex())
|
||||
assert.Error(t, err, "Expected item to be deleted: "+id.Hex())
|
||||
}
|
||||
|
||||
// 檢查不應刪除的項目仍存在
|
||||
for _, id := range tt.expectRemained {
|
||||
_, err = model.FindByID(ctx, id.Hex())
|
||||
assert.NoError(t, err, "Expected item to remain: "+id.Hex())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateProductItem(t *testing.T) {
|
||||
model, tearDown, err := SetupTestProductItemRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 建立一筆測試資料
|
||||
item := entity.ProductItems{
|
||||
ID: primitive.NewObjectID(),
|
||||
ReferenceID: primitive.NewObjectID().Hex(),
|
||||
Name: "Original Name",
|
||||
Stock: 10,
|
||||
Price: decimal.NewFromInt(500),
|
||||
IsUnLimit: false,
|
||||
IsFree: false,
|
||||
CreatedAt: time.Now().UnixNano(),
|
||||
UpdatedAt: time.Now().UnixNano(),
|
||||
}
|
||||
err = model.Insert(ctx, []entity.ProductItems{item})
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
update *repository.ProductUpdateItem
|
||||
expectErr error
|
||||
validate func(t *testing.T, updated *entity.ProductItems)
|
||||
}{
|
||||
{
|
||||
name: "Update is_un_limit and is_free",
|
||||
id: item.ID.Hex(),
|
||||
update: &repository.ProductUpdateItem{
|
||||
IsUnLimit: ptr(true),
|
||||
IsFree: ptr(true),
|
||||
},
|
||||
expectErr: nil,
|
||||
validate: func(t *testing.T, updated *entity.ProductItems) {
|
||||
assert.True(t, updated.IsUnLimit)
|
||||
assert.True(t, updated.IsFree)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update SKU and TimeSeries",
|
||||
id: item.ID.Hex(),
|
||||
update: &repository.ProductUpdateItem{
|
||||
SKU: ptr("SKU-XYZ-001"),
|
||||
TimeSeries: ptr(product.TimeSeriesTenMinutes),
|
||||
},
|
||||
expectErr: nil,
|
||||
validate: func(t *testing.T, updated *entity.ProductItems) {
|
||||
assert.Equal(t, "SKU-XYZ-001", updated.SKU)
|
||||
assert.Equal(t, product.TimeSeriesTenMinutes, updated.TimeSeries)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update Media field",
|
||||
id: item.ID.Hex(),
|
||||
update: &repository.ProductUpdateItem{
|
||||
Media: []entity.Media{
|
||||
{Sort: 1, Type: "image", URL: "https://example.com/img.jpg"},
|
||||
},
|
||||
},
|
||||
expectErr: nil,
|
||||
validate: func(t *testing.T, updated *entity.ProductItems) {
|
||||
assert.Len(t, updated.Media, 1)
|
||||
assert.Equal(t, "image", updated.Media[0].Type)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update CustomFields",
|
||||
id: item.ID.Hex(),
|
||||
update: &repository.ProductUpdateItem{
|
||||
CustomFields: []entity.CustomFields{
|
||||
{Key: "color", Value: "red"},
|
||||
},
|
||||
},
|
||||
expectErr: nil,
|
||||
validate: func(t *testing.T, updated *entity.ProductItems) {
|
||||
assert.Len(t, updated.CustomFields, 1)
|
||||
assert.Equal(t, "color", updated.CustomFields[0].Key)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update Freight",
|
||||
id: item.ID.Hex(),
|
||||
update: &repository.ProductUpdateItem{
|
||||
Freight: []entity.CustomFields{
|
||||
{
|
||||
Key: "color",
|
||||
Value: "red",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: nil,
|
||||
validate: func(t *testing.T, updated *entity.ProductItems) {
|
||||
assert.Equal(t, "color", updated.Freight[0].Key)
|
||||
assert.Equal(t, "red", updated.Freight[0].Value)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update name field",
|
||||
id: item.ID.Hex(),
|
||||
update: &repository.ProductUpdateItem{
|
||||
Name: ptr("Updated Name"),
|
||||
},
|
||||
expectErr: nil,
|
||||
validate: func(t *testing.T, updated *entity.ProductItems) {
|
||||
assert.Equal(t, "Updated Name", updated.Name)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Update stock and price",
|
||||
id: item.ID.Hex(),
|
||||
update: &repository.ProductUpdateItem{
|
||||
Stock: proto.Int64(99),
|
||||
Price: ptr(decimal.NewFromInt(999)),
|
||||
},
|
||||
expectErr: nil,
|
||||
validate: func(t *testing.T, updated *entity.ProductItems) {
|
||||
assert.Equal(t, uint64(99), updated.Stock)
|
||||
assert.Equal(t, "999", updated.Price.String())
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid ObjectID",
|
||||
id: "not-an-id",
|
||||
update: &repository.ProductUpdateItem{Name: ptr("Invalid")},
|
||||
expectErr: ErrInvalidObjectID,
|
||||
},
|
||||
{
|
||||
name: "Empty update struct",
|
||||
id: item.ID.Hex(),
|
||||
update: &repository.ProductUpdateItem{}, // no fields
|
||||
expectErr: fmt.Errorf("no fields to update"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := model.Update(ctx, tt.id, tt.update)
|
||||
if tt.expectErr != nil {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.expectErr.Error())
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
updated, err := model.FindByID(ctx, item.ID.Hex())
|
||||
assert.NoError(t, err)
|
||||
|
||||
if tt.validate != nil {
|
||||
tt.validate(t, updated)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateStatus_TableDriven(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestProductItemRepository("testDB")
|
||||
require.NoError(t, err)
|
||||
defer tearDown()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Insert a sample product item.
|
||||
now := time.Now().UnixNano()
|
||||
item := entity.ProductItems{
|
||||
ID: primitive.NewObjectID(),
|
||||
ReferenceID: primitive.NewObjectID().Hex(),
|
||||
Name: "Test Item",
|
||||
Status: product.StatusInactive, // initial status
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
err = repo.Insert(ctx, []entity.ProductItems{item})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
newStatus product.ItemStatus
|
||||
expectErr error
|
||||
check bool // whether to verify the update in DB
|
||||
}{
|
||||
{
|
||||
name: "Valid update",
|
||||
id: item.ID.Hex(),
|
||||
newStatus: product.StatusActive,
|
||||
expectErr: nil,
|
||||
check: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid ObjectID",
|
||||
id: "invalid-id",
|
||||
newStatus: product.StatusActive,
|
||||
expectErr: ErrInvalidObjectID,
|
||||
check: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := repo.UpdateStatus(ctx, tt.id, tt.newStatus)
|
||||
if tt.expectErr != nil {
|
||||
assert.ErrorIs(t, err, tt.expectErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// If expected to check update, verify that the product's status is updated.
|
||||
if tt.check {
|
||||
updated, err := repo.FindByID(ctx, tt.id)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.newStatus, updated.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncSalesCount(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestProductItemRepository("testDB")
|
||||
require.NoError(t, err)
|
||||
defer tearDown()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Insert a sample product item with initial SalesCount = 0.
|
||||
now := time.Now().UnixNano()
|
||||
item := entity.ProductItems{
|
||||
ID: primitive.NewObjectID(),
|
||||
ReferenceID: primitive.NewObjectID().Hex(),
|
||||
Name: "Sales Count Test Item",
|
||||
SalesCount: 0,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
err = repo.Insert(ctx, []entity.ProductItems{item})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
count int64
|
||||
expectErr error
|
||||
check bool // whether to verify the updated sales count in the DB.
|
||||
}{
|
||||
{
|
||||
name: "Increment sales count by 5",
|
||||
id: item.ID.Hex(),
|
||||
count: 5,
|
||||
expectErr: nil,
|
||||
check: true,
|
||||
},
|
||||
{
|
||||
name: "Invalid ObjectID",
|
||||
id: "invalid-id",
|
||||
count: 3,
|
||||
expectErr: ErrInvalidObjectID,
|
||||
check: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := repo.IncSalesCount(ctx, tt.id, tt.count)
|
||||
if tt.expectErr != nil {
|
||||
assert.ErrorIs(t, err, tt.expectErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// If check is true, verify that the sales_count is updated correctly.
|
||||
if tt.check {
|
||||
updated, err := repo.FindByID(ctx, tt.id)
|
||||
assert.NoError(t, err)
|
||||
// Since initial SalesCount was 0, after increment it should equal tt.count.
|
||||
assert.Equal(t, uint64(tt.count), updated.SalesCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecSalesCount(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestProductItemRepository("testDB")
|
||||
require.NoError(t, err)
|
||||
defer tearDown()
|
||||
|
||||
ctx := context.Background()
|
||||
now := time.Now().UnixNano()
|
||||
|
||||
// Insert an item with an initial SalesCount of 10.
|
||||
item := entity.ProductItems{
|
||||
ID: primitive.NewObjectID(),
|
||||
ReferenceID: primitive.NewObjectID().Hex(),
|
||||
Name: "Dec Sales Count Test Item",
|
||||
SalesCount: 10,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
// Insert an item with SalesCount equal to 0 (to test underflow behavior).
|
||||
zeroItem := entity.ProductItems{
|
||||
ID: primitive.NewObjectID(),
|
||||
ReferenceID: primitive.NewObjectID().Hex(),
|
||||
Name: "Zero Sales Count Item",
|
||||
SalesCount: 0,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
err = repo.Insert(ctx, []entity.ProductItems{item, zeroItem})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
decCount int64
|
||||
expectErr error
|
||||
check bool // whether to verify the updated sales count in the DB
|
||||
expected uint64 // expected SalesCount if check is true
|
||||
}{
|
||||
{
|
||||
name: "Valid decrement from 10",
|
||||
id: item.ID.Hex(),
|
||||
decCount: 3,
|
||||
expectErr: nil,
|
||||
check: true,
|
||||
expected: 7,
|
||||
},
|
||||
{
|
||||
name: "Invalid ObjectID",
|
||||
id: "invalid-id",
|
||||
decCount: 3,
|
||||
expectErr: ErrInvalidObjectID,
|
||||
check: false,
|
||||
},
|
||||
{
|
||||
name: "Decrement from zero (should not allow underflow)",
|
||||
id: zeroItem.ID.Hex(),
|
||||
decCount: 5,
|
||||
expectErr: nil,
|
||||
check: true,
|
||||
expected: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := repo.DecSalesCount(ctx, tt.id, tt.decCount)
|
||||
if tt.expectErr != nil {
|
||||
assert.Error(t, err)
|
||||
// Check that the error message contains "failed to decrease stock" (for the underflow case)
|
||||
if tt.expectErr.Error() != ErrInvalidObjectID.Error() {
|
||||
assert.Contains(t, err.Error(), "failed to decrease stock")
|
||||
} else {
|
||||
assert.ErrorIs(t, err, tt.expectErr)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// If expected to check, verify the updated SalesCount.
|
||||
if tt.check {
|
||||
updated, err := repo.FindByID(ctx, tt.id)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, updated.SalesCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSalesCount(t *testing.T) {
|
||||
// 取得測試 repository
|
||||
repo, tearDown, err := SetupTestProductItemRepository("testDB")
|
||||
require.NoError(t, err)
|
||||
defer tearDown()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 預先建立測試資料,設定各項目的 SalesCount 值
|
||||
testItems := []entity.ProductItems{
|
||||
{
|
||||
ReferenceID: primitive.NewObjectID().Hex(),
|
||||
Name: "Test Item 1",
|
||||
SalesCount: 5,
|
||||
},
|
||||
{
|
||||
ReferenceID: primitive.NewObjectID().Hex(),
|
||||
Name: "Test Item 2",
|
||||
SalesCount: 15,
|
||||
},
|
||||
}
|
||||
|
||||
// 插入測試資料
|
||||
err = repo.Insert(ctx, testItems)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 建立一組包含有效與無效 ID 的字串陣列
|
||||
validIDs := []string{
|
||||
testItems[0].ID.Hex(),
|
||||
testItems[1].ID.Hex(),
|
||||
}
|
||||
// 在陣列前面加上一個無法轉換的 ID
|
||||
mixedIDs := append([]string{"invalidID"}, validIDs...)
|
||||
|
||||
t.Run("with valid and invalid IDs", func(t *testing.T) {
|
||||
salesCounts, err := repo.GetSalesCount(ctx, mixedIDs)
|
||||
require.NoError(t, err)
|
||||
// 預期只會回傳有效的兩筆資料
|
||||
require.Len(t, salesCounts, len(validIDs))
|
||||
|
||||
// 驗證每筆資料的 SalesCount 是否正確
|
||||
for _, sc := range salesCounts {
|
||||
switch sc.ID {
|
||||
case testItems[0].ID.Hex():
|
||||
assert.Equal(t, uint64(5), sc.Count)
|
||||
case testItems[1].ID.Hex():
|
||||
assert.Equal(t, uint64(15), sc.Count)
|
||||
default:
|
||||
t.Errorf("unexpected product item ID: %s", sc.ID)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with all invalid IDs", func(t *testing.T) {
|
||||
salesCounts, err := repo.GetSalesCount(ctx, []string{"badid1", "badid2"})
|
||||
// 因無法轉換成 ObjectID,預期會回傳 ErrInvalidObjectID
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrInvalidObjectID, err)
|
||||
assert.Nil(t, salesCounts)
|
||||
})
|
||||
}
|
|
@ -60,7 +60,7 @@ func SetupTestProductRepository(db string) (repository.ProductRepository, func()
|
|||
}
|
||||
|
||||
repo := NewProductRepository(param)
|
||||
//_, _ = repo.Index20241226001UP(context.Background())
|
||||
_, _ = repo.Index20250317001UP(context.Background())
|
||||
|
||||
return repo, tearDown, nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue