diff --git a/go.mod b/go.mod index aff2a09..640d4fa 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,12 @@ go 1.22.3 require ( code.30cm.net/digimon/library-go/errs v1.2.5 code.30cm.net/digimon/library-go/validator v1.0.0 + github.com/go-playground/assert/v2 v2.2.0 github.com/go-playground/validator/v10 v10.22.0 github.com/shopspring/decimal v1.4.0 github.com/zeromicro/go-zero v1.7.3 go.mongodb.org/mongo-driver v1.17.1 + go.uber.org/mock v0.5.0 google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.35.1 ) diff --git a/internal/domain/order.go b/internal/domain/order.go index be56da4..97f10ec 100644 --- a/internal/domain/order.go +++ b/internal/domain/order.go @@ -23,6 +23,10 @@ const ( type OrderType int64 +const ( + OrderTypeTest OrderType = 0 // 測試訂單 +) + func (o *OrderType) ToInt() int { return int(*o) } diff --git a/internal/logic/orderservice/cancel_order_logic.go b/internal/logic/orderservice/cancel_order_logic.go index 02bc3db..76e6b5a 100644 --- a/internal/logic/orderservice/cancel_order_logic.go +++ b/internal/logic/orderservice/cancel_order_logic.go @@ -1,6 +1,9 @@ package orderservicelogic import ( + "app-cloudep-trade-service/internal/domain" + "app-cloudep-trade-service/internal/domain/usecase" + ers "code.30cm.net/digimon/library-go/errs" "context" "app-cloudep-trade-service/gen_result/pb/trade" @@ -23,9 +26,30 @@ func NewCancelOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Cance } } +// CancelOrderQuery 1.建單失敗 9.交易取消 10.交易異常 +type CancelOrderQuery struct { + BusinessID string `json:"business_id" validate:"required"` + Status int64 `json:"status" validate:"required,oneof=1 9 10"` +} + // CancelOrder 取消訂單 func (l *CancelOrderLogic) CancelOrder(in *trade.CancelOrderReq) (*trade.OKResp, error) { - // todo: add your logic here and delete this line + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&CancelOrderQuery{ + BusinessID: in.GetBusinessId(), + Status: in.GetStatus(), + }); err != nil { + // 錯誤代碼 06-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + err := l.svcCtx.OrderUseCase.CancelOrder(l.ctx, usecase.CancelOrderQuery{ + BusinessID: in.GetBusinessId(), + Status: domain.OrderStatus(in.GetStatus()), + }) + if err != nil { + return nil, err + } return &trade.OKResp{}, nil } diff --git a/internal/logic/orderservice/create_order_logic.go b/internal/logic/orderservice/create_order_logic.go index 7530de4..0cb2546 100644 --- a/internal/logic/orderservice/create_order_logic.go +++ b/internal/logic/orderservice/create_order_logic.go @@ -1,7 +1,11 @@ package orderservicelogic import ( + "app-cloudep-trade-service/internal/domain" + "app-cloudep-trade-service/internal/domain/usecase" + ers "code.30cm.net/digimon/library-go/errs" "context" + "github.com/shopspring/decimal" "app-cloudep-trade-service/gen_result/pb/trade" "app-cloudep-trade-service/internal/svc" @@ -23,9 +27,213 @@ func NewCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Creat } } +type createOrderReq struct { // 訂單ID + BusinessID string `json:"business_id" validate:"required"` // 訂單業務流水號 + OrderType int8 `json:"order_type" validate:"required"` // 訂單類型 + OrderStatus int8 `json:"order_status" validate:"oneof=0 1 2 3 4 5 6 7 8 9 10 11"` // 訂單狀態 + Brand string `json:"brand" validate:"required"` // 下單平台 + OrderUID string `json:"order_uid" validate:"required"` // 下單用戶 UID + ReferenceID string `json:"reference_id" validate:"required"` // 訂單來源 + Count decimal.Decimal `json:"count" validate:"required,decimalGt=0"` // 訂單數量 + OrderFee decimal.Decimal `json:"order_fee" validate:"required,decimalGte=0"` // 訂單手續費 + Amount decimal.Decimal `json:"amount" validate:"required,decimalGte=0"` // 單價 + ReferenceBrand *string `json:"reference_brand,omitempty" validate:"omitempty"` // 訂單來源平台 + ReferenceUID *string `json:"reference_uid,omitempty" validate:"omitempty"` // 訂單來源用戶 UID + WalletStatus *int64 `json:"wallet_status,omitempty" validate:"omitempty,oneof=1 2 3 4 5 6 7"` // 交易金額狀態 + ThreePartyStatus *int64 `json:"three_party_status,omitempty" validate:"omitempty,oneof=1 2 3"` // 三方請求狀態 + DirectionType *int64 `json:"direction_type,omitempty" validate:"omitempty,oneof=1 2"` // 交易方向 + CryptoType *string `json:"crypto_type,omitempty" validate:"omitempty"` // 交易幣種 + ThirdPartyFee *decimal.Decimal `json:"third_party_fee,omitempty" validate:"omitempty,decimalGte=0"` // 第三方手續費 + CryptoToUSDTRate *decimal.Decimal `json:"crypto_to_usdt_rate,omitempty" validate:"omitempty,decimalGte=0"` // 交易幣種對 USDT 匯率 + FiatToUSDRate *decimal.Decimal `json:"fiat_to_usd_rate,omitempty" validate:"omitempty,decimalGte=0"` // 法幣對 USD 匯率 + FeeCryptoToUSDTRate *decimal.Decimal `json:"fee_crypto_to_usdt_rate,omitempty" validate:"omitempty,decimalGte=0"` // 手續費幣種對 USDT 匯率 + USDTToCryptoTypeRate *decimal.Decimal `json:"usdt_to_crypto_type_rate,omitempty" validate:"omitempty,decimalGte=0"` // USDT 對交易幣種匯率 + PaymentFiat *string `json:"payment_fiat,omitempty" validate:"omitempty"` // 支付法幣 + PaymentUnitPrice *decimal.Decimal `json:"payment_unit_price,omitempty" validate:"omitempty,decimalGte=0"` // crypto 單價 + PaymentTemplateID *string `json:"payment_template_id,omitempty" validate:"omitempty"` // 支付方式配置 ID + OrderArrivalTime *int64 `json:"order_arrival_time,omitempty" validate:"omitempty"` // 訂單到帳時間 + OrderPaymentTime *int64 `json:"order_payment_time,omitempty" validate:"omitempty"` // 訂單付款時間 + UnpaidTimeoutSecond *int64 `json:"unpaid_timeout_second,omitempty" validate:"omitempty,decimalGte=0"` // 支付期限秒數 + ChainType *string `json:"chain_type,omitempty" validate:"omitempty"` // 主網類型 + TxHash *string `json:"tx_hash,omitempty" validate:"omitempty"` // 交易哈希 + FromAddress *string `json:"from_address,omitempty" validate:"omitempty"` // 來源地址 + ToAddress *string `json:"to_address,omitempty" validate:"omitempty"` // 目標地址 + ChainFee *decimal.Decimal `json:"chain_fee,omitempty" validate:"omitempty,decimalGte=0"` // 鏈上交易手續費 + ChainFeeCrypto *string `json:"chain_fee_crypto,omitempty" validate:"omitempty"` // 鏈上手續費使用幣別 + Memo *string `json:"memo,omitempty" validate:"omitempty"` // 鏈上備註 + OrderNote *string `json:"order_note,omitempty" validate:"omitempty"` +} + +//nolint:gocyclo,gocognit +func buildCreateOrderReq(in *trade.CreateOrderReq) (*createOrderReq, error) { + createOrderReq := &createOrderReq{ + BusinessID: in.BusinessId, + OrderType: int8(in.OrderType), + OrderStatus: int8(in.OrderStatus), + Brand: in.Brand, + OrderUID: in.OrderUid, + ReferenceID: in.ReferenceId, + } + + var err error + // 只有當 Count, OrderFee, Amount 不為空時才進行轉換和設置 + if in.Count != "" { + createOrderReq.Count, err = decimal.NewFromString(in.Count) + if err != nil { + return nil, err + } + } + if in.OrderFee != "" { + createOrderReq.OrderFee, err = decimal.NewFromString(in.OrderFee) + if err != nil { + return nil, err + } + } + if in.Amount != "" { + createOrderReq.Amount, err = decimal.NewFromString(in.Amount) + if err != nil { + return nil, err + } + } + + // 判斷可選字段,只有不為 nil 才設置 + if in.ReferenceBrand != nil { + createOrderReq.ReferenceBrand = in.ReferenceBrand + } + if in.ReferenceUid != nil { + createOrderReq.ReferenceUID = in.ReferenceUid + } + if in.WalletStatus != nil { + createOrderReq.WalletStatus = in.WalletStatus + } + if in.ThreePartyStatus != nil { + createOrderReq.ThreePartyStatus = in.ThreePartyStatus + } + if in.DirectionType != nil { + createOrderReq.DirectionType = in.DirectionType + } + if in.CryptoType != nil { + createOrderReq.CryptoType = in.CryptoType + } + if in.ThirdPartyFee != nil && *in.ThirdPartyFee != "" { + createOrderReq.ThirdPartyFee = decimalPtrFromString(*in.ThirdPartyFee) + } + if in.CryptoToUsdtRate != nil && *in.CryptoToUsdtRate != "" { + createOrderReq.CryptoToUSDTRate = decimalPtrFromString(*in.CryptoToUsdtRate) + } + if in.FiatToUsdRate != nil && *in.FiatToUsdRate != "" { + createOrderReq.FiatToUSDRate = decimalPtrFromString(*in.FiatToUsdRate) + } + if in.FeeCryptoToUsdtRate != nil && *in.FeeCryptoToUsdtRate != "" { + createOrderReq.FeeCryptoToUSDTRate = decimalPtrFromString(*in.FeeCryptoToUsdtRate) + } + if in.UsdtToCryptoTypeRate != nil && *in.UsdtToCryptoTypeRate != "" { + createOrderReq.USDTToCryptoTypeRate = decimalPtrFromString(*in.UsdtToCryptoTypeRate) + } + if in.PaymentFiat != nil { + createOrderReq.PaymentFiat = in.PaymentFiat + } + if in.PaymentUnitPrice != nil && *in.PaymentUnitPrice != "" { + createOrderReq.PaymentUnitPrice = decimalPtrFromString(*in.PaymentUnitPrice) + } + if in.PaymentTemplateId != nil { + createOrderReq.PaymentTemplateID = in.PaymentTemplateId + } + if in.OrderArrivalTime != nil { + createOrderReq.OrderArrivalTime = in.OrderArrivalTime + } + if in.OrderPaymentTime != nil { + createOrderReq.OrderPaymentTime = in.OrderPaymentTime + } + if in.UnpaidTimeoutSecond != nil { + createOrderReq.UnpaidTimeoutSecond = in.UnpaidTimeoutSecond + } + if in.ChainType != nil { + createOrderReq.ChainType = in.ChainType + } + if in.TxHash != nil { + createOrderReq.TxHash = in.TxHash + } + if in.FromAddress != nil { + createOrderReq.FromAddress = in.FromAddress + } + if in.ToAddress != nil { + createOrderReq.ToAddress = in.ToAddress + } + if in.ChainFee != nil && *in.ChainFee != "" { + createOrderReq.ChainFee = decimalPtrFromString(*in.ChainFee) + } + if in.ChainFeeCrypto != nil { + createOrderReq.ChainFeeCrypto = in.ChainFeeCrypto + } + if in.Memo != nil { + createOrderReq.Memo = in.Memo + } + if in.OrderNote != nil { + createOrderReq.OrderNote = in.OrderNote + } + + return createOrderReq, nil +} + +// toCreateOrderReq 將 createOrderReq 轉換為 CreateOrderReq +func toCreateOrderUseCase(req *createOrderReq) usecase.CreateOrderReq { + return usecase.CreateOrderReq{ + BusinessID: req.BusinessID, + OrderType: domain.OrderType(req.OrderType), + OrderStatus: domain.OrderStatus(req.OrderStatus), + Brand: req.Brand, + OrderUID: req.OrderUID, + ReferenceID: req.ReferenceID, + Count: req.Count, + OrderFee: req.OrderFee, + Amount: req.Amount, + WalletStatus: *req.WalletStatus, + DirectionType: *req.DirectionType, + ReferenceBrand: req.ReferenceBrand, + ReferenceUID: req.ReferenceUID, + ThreePartyStatus: req.ThreePartyStatus, + CryptoType: req.CryptoType, + ThirdPartyFee: req.ThirdPartyFee, + CryptoToUSDTRate: req.CryptoToUSDTRate, + FiatToUSDRate: req.FiatToUSDRate, + FeeCryptoToUSDTRate: req.FeeCryptoToUSDTRate, + USDTToCryptoTypeRate: req.USDTToCryptoTypeRate, + PaymentFiat: req.PaymentFiat, + PaymentUnitPrice: req.PaymentUnitPrice, + PaymentTemplateID: req.PaymentTemplateID, + OrderArrivalTime: req.OrderArrivalTime, + OrderPaymentTime: req.OrderPaymentTime, + UnpaidTimeoutSecond: req.UnpaidTimeoutSecond, + ChainType: req.ChainType, + TxHash: req.TxHash, + FromAddress: req.FromAddress, + ToAddress: req.ToAddress, + ChainFee: req.ChainFee, + ChainFeeCrypto: req.ChainFeeCrypto, + Memo: req.Memo, + OrderNote: req.OrderNote, + } +} + // CreateOrder 建立訂單 func (l *CreateOrderLogic) CreateOrder(in *trade.CreateOrderReq) (*trade.OKResp, error) { - // todo: add your logic here and delete this line + req, err := buildCreateOrderReq(in) + if err != nil { + // 錯誤代碼 06-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(req); err != nil { + // 錯誤代碼 06-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + err = l.svcCtx.OrderUseCase.CreateOrder(l.ctx, toCreateOrderUseCase(req)) + if err != nil { + return nil, err + } return &trade.OKResp{}, nil } diff --git a/internal/logic/orderservice/delete_order_logic.go b/internal/logic/orderservice/delete_order_logic.go index a3de943..c58e4fc 100644 --- a/internal/logic/orderservice/delete_order_logic.go +++ b/internal/logic/orderservice/delete_order_logic.go @@ -1,6 +1,8 @@ package orderservicelogic import ( + "app-cloudep-trade-service/internal/domain/usecase" + ers "code.30cm.net/digimon/library-go/errs" "context" "app-cloudep-trade-service/gen_result/pb/trade" @@ -23,9 +25,27 @@ func NewDeleteOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Delet } } +// DeleteOrderQuery 刪除訂單(軟刪除) +type DeleteOrderQuery struct { + BusinessID string `json:"business_id" validate:"required"` +} + // DeleteOrder 刪除訂單(軟刪除) func (l *DeleteOrderLogic) DeleteOrder(in *trade.DeleteOrderReq) (*trade.OKResp, error) { - // todo: add your logic here and delete this line + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&DeleteOrderQuery{ + BusinessID: in.GetBusinessId(), + }); err != nil { + // 錯誤代碼 06-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + err := l.svcCtx.OrderUseCase.DeleteOrder(l.ctx, usecase.DeleteOrderQuery{ + BusinessID: in.GetBusinessId(), + }) + if err != nil { + return nil, err + } return &trade.OKResp{}, nil } diff --git a/internal/logic/orderservice/get_order_logic.go b/internal/logic/orderservice/get_order_logic.go index 26feecd..6afabf7 100644 --- a/internal/logic/orderservice/get_order_logic.go +++ b/internal/logic/orderservice/get_order_logic.go @@ -1,6 +1,8 @@ package orderservicelogic import ( + "app-cloudep-trade-service/internal/domain/usecase" + ers "code.30cm.net/digimon/library-go/errs" "context" "app-cloudep-trade-service/gen_result/pb/trade" @@ -23,9 +25,64 @@ func NewGetOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetOrder } } +type GetOrderQuery struct { + BusinessID string `json:"business_id" validate:"required"` +} + // GetOrder 取得訂單詳情 func (l *GetOrderLogic) GetOrder(in *trade.GetOrderReq) (*trade.GetOrderResp, error) { - // todo: add your logic here and delete this line + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&GetOrderQuery{ + BusinessID: in.GetBusinessId(), + }); err != nil { + // 錯誤代碼 06-011-00 + return nil, ers.InvalidFormat(err.Error()) + } - return &trade.GetOrderResp{}, nil + o, err := l.svcCtx.OrderUseCase.GetOrder(l.ctx, usecase.GetOrderQuery{ + BusinessID: in.GetBusinessId(), + }) + if err != nil { + return nil, err + } + + return &trade.GetOrderResp{ + UpdateTime: o.UpdateTime, + CreateTime: o.CreateTime, + BusinessId: o.BusinessId, + OrderType: int32(o.OrderType), + OrderStatus: int32(o.OrderStatus), + Brand: o.Brand, + OrderUid: o.OrderUid, + ReferenceId: o.ReferenceId, + Count: o.Count, + OrderFee: o.OrderFee, + Amount: o.Amount, + // 下面的為未來擴充用的欄位 + ReferenceBrand: o.ReferenceBrand, + ReferenceUid: o.ReferenceUid, + WalletStatus: o.WalletStatus, + ThreePartyStatus: o.ThreePartyStatus, + DirectionType: o.DirectionType, + CryptoType: o.CryptoType, + ThirdPartyFee: o.ThirdPartyFee, + CryptoToUsdtRate: o.CryptoToUsdtRate, + FiatToUsdRate: o.FiatToUsdRate, + FeeCryptoToUsdtRate: o.FeeCryptoToUsdtRate, + UsdtToCryptoTypeRate: o.UsdtToCryptoTypeRate, + PaymentFiat: o.PaymentFiat, + PaymentUnitPrice: o.PaymentUnitPrice, + PaymentTemplateId: o.PaymentTemplateId, + OrderArrivalTime: o.OrderArrivalTime, + OrderPaymentTime: o.OrderPaymentTime, + UnpaidTimeoutSecond: o.UnpaidTimeoutSecond, + ChainType: o.ChainType, + TxHash: o.TxHash, + FromAddress: o.FromAddress, + ToAddress: o.ToAddress, + ChainFee: o.ChainFee, + ChainFeeCrypto: o.ChainFeeCrypto, + Memo: o.Memo, + OrderNote: o.OrderNote, + }, nil } diff --git a/internal/logic/orderservice/list_order_logic.go b/internal/logic/orderservice/list_order_logic.go index e327edd..ec5eb50 100644 --- a/internal/logic/orderservice/list_order_logic.go +++ b/internal/logic/orderservice/list_order_logic.go @@ -1,6 +1,9 @@ package orderservicelogic import ( + "app-cloudep-trade-service/internal/domain" + "app-cloudep-trade-service/internal/domain/usecase" + ers "code.30cm.net/digimon/library-go/errs" "context" "app-cloudep-trade-service/gen_result/pb/trade" @@ -23,9 +26,132 @@ func NewListOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ListOrd } } +type GetOrderListReq struct { + PageIndex int64 `json:"page_index" validate:"required"` + PageSize int64 `json:"page_size" validate:"required"` + + ReferenceID string `json:"reference_id"` + ReferenceUID string `json:"reference_uid"` + BusinessID string `json:"business_id"` + UID string `json:"uid"` + OrderType int `json:"order_type"` + DirectionType []int64 `json:"direction_type"` + OrderStatus []int64 `json:"order_status"` + + StartCreateTime int64 `json:"start_create_time"` + EndCreateTime int64 `json:"end_create_time"` + StartUpdateTime int64 `json:"start_update_time"` + EndUpdateTime int64 `json:"end_update_time"` + StartOrderArrivalTime int64 `json:"start_order_arrival_time"` + EndOrderArrivalTime int64 `json:"end_order_arrival_time"` + StartOrderPaymentTime int64 `json:"start_order_payment_time"` + EndOrderPaymentTime int64 `json:"end_order_payment_time"` + + CryptoType string `json:"crypto_type"` + TxHash string `json:"tx_hash"` +} + +// toGetOrderListReq 將 JSON 標籤結構轉換為無 JSON 標籤結構 +func toGetOrderListReq(req *trade.ListOrderReq) usecase.GetOrderListReq { + return usecase.GetOrderListReq{ + PageIndex: req.PageIndex, + PageSize: req.PageSize, + ReferenceID: req.ReferenceId, + ReferenceUID: req.ReferenceUid, + BusinessID: req.BusinessId, + UID: req.Uid, + OrderType: domain.OrderType(req.OrderType), + DirectionType: i32To64(req.DirectionType), + OrderStatus: i32To64(req.OrderStatus), + StartCreateTime: req.StartCreateTime, + EndCreateTime: req.EndCreateTime, + StartUpdateTime: req.StartUpdateTime, + EndUpdateTime: req.EndUpdateTime, + StartOrderArrivalTime: req.StartOrderArrivalTime, + EndOrderArrivalTime: req.EndOrderArrivalTime, + StartOrderPaymentTime: req.StartOrderPaymentTime, + EndOrderPaymentTime: req.EndOrderPaymentTime, + CryptoType: req.CryptoType, + TxHash: req.TxHash, + } +} + +func i32To64(i []int32) []int64 { + if len(i) == 0 { + return nil + } + result := make([]int64, 0, len(i)) + for item := range i { + result = append(result, int64(item)) + } + + return result +} + // ListOrder 取得訂單列表 func (l *ListOrderLogic) ListOrder(in *trade.ListOrderReq) (*trade.ListOrderResp, error) { - // todo: add your logic here and delete this line + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&GetOrderListReq{ + PageIndex: in.GetPageIndex(), + PageSize: in.GetPageSize(), + }); err != nil { + // 錯誤代碼 06-011-00 + return nil, ers.InvalidFormat(err.Error()) + } - return &trade.ListOrderResp{}, nil + order, err := l.svcCtx.OrderUseCase.ListOrder(l.ctx, toGetOrderListReq(in)) + if err != nil { + return nil, err + } + + data := make([]*trade.GetOrderResp, 0, len(order.Data)) + for _, item := range order.Data { + data = append(data, &trade.GetOrderResp{ + BusinessId: item.BusinessId, + OrderType: int32(item.OrderType), + OrderStatus: int32(item.OrderStatus), + Brand: item.Brand, + OrderUid: item.OrderUid, + ReferenceId: item.ReferenceId, + Count: item.Count, + OrderFee: item.OrderFee, + Amount: item.Amount, + ReferenceBrand: item.ReferenceBrand, + ReferenceUid: item.ReferenceUid, + WalletStatus: item.WalletStatus, + ThreePartyStatus: item.ThreePartyStatus, + DirectionType: item.DirectionType, + CryptoType: item.CryptoType, + ThirdPartyFee: item.ThirdPartyFee, + CryptoToUsdtRate: item.CryptoToUsdtRate, + FiatToUsdRate: item.FiatToUsdRate, + FeeCryptoToUsdtRate: item.FeeCryptoToUsdtRate, + UsdtToCryptoTypeRate: item.UsdtToCryptoTypeRate, + PaymentFiat: item.PaymentFiat, + PaymentUnitPrice: item.PaymentUnitPrice, + PaymentTemplateId: item.PaymentTemplateId, + OrderArrivalTime: item.OrderArrivalTime, + OrderPaymentTime: item.OrderPaymentTime, + UnpaidTimeoutSecond: item.UnpaidTimeoutSecond, + ChainType: item.ChainType, + TxHash: item.TxHash, + FromAddress: item.FromAddress, + ToAddress: item.ToAddress, + ChainFee: item.ChainFee, + ChainFeeCrypto: item.ChainFeeCrypto, + Memo: item.Memo, + OrderNote: item.OrderNote, + CreateTime: item.CreateTime, + UpdateTime: item.UpdateTime, + }) + } + + return &trade.ListOrderResp{ + Data: data, + Page: &trade.Pager{ + Index: order.Page.Index, + Size: order.Page.Size, + Total: order.Page.Total, + }, + }, nil } diff --git a/internal/logic/orderservice/modify_order_status_logic.go b/internal/logic/orderservice/modify_order_status_logic.go index 5c60413..a2cad15 100644 --- a/internal/logic/orderservice/modify_order_status_logic.go +++ b/internal/logic/orderservice/modify_order_status_logic.go @@ -1,6 +1,8 @@ package orderservicelogic import ( + "app-cloudep-trade-service/internal/domain/usecase" + ers "code.30cm.net/digimon/library-go/errs" "context" "app-cloudep-trade-service/gen_result/pb/trade" @@ -23,9 +25,34 @@ func NewModifyOrderStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) } } +// ModifyOrderQuery +// 0.建立訂單 1.建單失敗 2.審核中 3.付款中 4.已付款 +// 5.已付款待轉帳 6.申訴中 7.交易完成 +// 8.交易失敗 9.交易取消 10.交易異常 11.交易超時 + +type ModifyOrderQuery struct { + BusinessID string `json:"business_id" validate:"required"` + Status int64 `json:"status" validate:"required,oneof=2 3 4 5 6 7 8 11"` +} + // ModifyOrderStatus 修改訂單狀態 func (l *ModifyOrderStatusLogic) ModifyOrderStatus(in *trade.ModifyOrderStatusReq) (*trade.OKResp, error) { - // todo: add your logic here and delete this line + // 驗證資料 + if err := l.svcCtx.Validate.ValidateAll(&ModifyOrderQuery{ + BusinessID: in.GetBusinessId(), + Status: in.GetStatus(), + }); err != nil { + // 錯誤代碼 06-011-00 + return nil, ers.InvalidFormat(err.Error()) + } + + err := l.svcCtx.OrderUseCase.ModifyOrderStatus(l.ctx, &usecase.ModifyOrderQuery{ + Status: in.GetStatus(), + BusinessID: in.GetBusinessId(), + }) + if err != nil { + return nil, err + } return &trade.OKResp{}, nil } diff --git a/internal/logic/orderservice/order_status_timeout_logic.go b/internal/logic/orderservice/order_status_timeout_logic.go index 0c7528f..7282fb9 100644 --- a/internal/logic/orderservice/order_status_timeout_logic.go +++ b/internal/logic/orderservice/order_status_timeout_logic.go @@ -25,7 +25,10 @@ func NewOrderStatusTimeoutLogic(ctx context.Context, svcCtx *svc.ServiceContext) // OrderStatusTimeout 訂單超時任務/cron/order-status/timeout func (l *OrderStatusTimeoutLogic) OrderStatusTimeout(in *trade.OrderStatusTimeoutReq) (*trade.OKResp, error) { - // todo: add your logic here and delete this line + err := l.svcCtx.OrderUseCase.OrderStatusTimeout(l.ctx) + if err != nil { + return nil, err + } return &trade.OKResp{}, nil } diff --git a/internal/logic/orderservice/utils.go b/internal/logic/orderservice/utils.go new file mode 100644 index 0000000..53d4ef2 --- /dev/null +++ b/internal/logic/orderservice/utils.go @@ -0,0 +1,20 @@ +package orderservicelogic + +import ( + "github.com/shopspring/decimal" + "github.com/zeromicro/go-zero/core/logx" +) + +func decimalPtrFromString(val string) *decimal.Decimal { + if val == "" { + return nil + } + dec, err := decimal.NewFromString(val) + if err != nil { + logx.Errorf("Failed to convert string to decimal: %v", err) + + return nil + } + + return &dec +} diff --git a/internal/mock/lib/validate.go b/internal/mock/lib/validate.go new file mode 100644 index 0000000..dd852f8 --- /dev/null +++ b/internal/mock/lib/validate.go @@ -0,0 +1,73 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./validate.go +// +// Generated by this command: +// +// mockgen -source=./validate.go -destination=../../mock/lib/validate.go -package=lib +// + +// Package lib is a generated GoMock package. +package lib + +import ( + reflect "reflect" + + required "code.30cm.net/digimon/library-go/validator" + + gomock "go.uber.org/mock/gomock" +) + +// MockValidate is a mock of Validate interface. +type MockValidate struct { + ctrl *gomock.Controller + recorder *MockValidateMockRecorder +} + +// MockValidateMockRecorder is the mock recorder for MockValidate. +type MockValidateMockRecorder struct { + mock *MockValidate +} + +// NewMockValidate creates a new mock instance. +func NewMockValidate(ctrl *gomock.Controller) *MockValidate { + mock := &MockValidate{ctrl: ctrl} + mock.recorder = &MockValidateMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockValidate) EXPECT() *MockValidateMockRecorder { + return m.recorder +} + +// BindToValidator mocks base method. +func (m *MockValidate) BindToValidator(opts ...required.Option) error { + m.ctrl.T.Helper() + varargs := []any{} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "BindToValidator", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// BindToValidator indicates an expected call of BindToValidator. +func (mr *MockValidateMockRecorder) BindToValidator(opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BindToValidator", reflect.TypeOf((*MockValidate)(nil).BindToValidator), opts...) +} + +// ValidateAll mocks base method. +func (m *MockValidate) ValidateAll(obj any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateAll", obj) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateAll indicates an expected call of ValidateAll. +func (mr *MockValidateMockRecorder) ValidateAll(obj any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateAll", reflect.TypeOf((*MockValidate)(nil).ValidateAll), obj) +} diff --git a/internal/mock/model/order_model.go b/internal/mock/model/order_model.go new file mode 100644 index 0000000..0d22ccb --- /dev/null +++ b/internal/mock/model/order_model.go @@ -0,0 +1,177 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./internal/model/mongo/order_model.go +// +// Generated by this command: +// +// mockgen -source=./internal/model/mongo/order_model.go -destination=./internal/mock/model/order_model.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + mongo "app-cloudep-trade-service/internal/model/mongo" + context "context" + reflect "reflect" + + mongo0 "go.mongodb.org/mongo-driver/mongo" + gomock "go.uber.org/mock/gomock" +) + +// MockOrderModel is a mock of OrderModel interface. +type MockOrderModel struct { + ctrl *gomock.Controller + recorder *MockOrderModelMockRecorder +} + +// MockOrderModelMockRecorder is the mock recorder for MockOrderModel. +type MockOrderModelMockRecorder struct { + mock *MockOrderModel +} + +// NewMockOrderModel creates a new mock instance. +func NewMockOrderModel(ctrl *gomock.Controller) *MockOrderModel { + mock := &MockOrderModel{ctrl: ctrl} + mock.recorder = &MockOrderModelMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockOrderModel) EXPECT() *MockOrderModelMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockOrderModel) Delete(ctx context.Context, id string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, id) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockOrderModelMockRecorder) Delete(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockOrderModel)(nil).Delete), ctx, id) +} + +// DeleteByBusinessID mocks base method. +func (m *MockOrderModel) DeleteByBusinessID(ctx context.Context, id string) (*mongo0.UpdateResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteByBusinessID", ctx, id) + ret0, _ := ret[0].(*mongo0.UpdateResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteByBusinessID indicates an expected call of DeleteByBusinessID. +func (mr *MockOrderModelMockRecorder) DeleteByBusinessID(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteByBusinessID", reflect.TypeOf((*MockOrderModel)(nil).DeleteByBusinessID), ctx, id) +} + +// FindOne mocks base method. +func (m *MockOrderModel) FindOne(ctx context.Context, id string) (*mongo.Order, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOne", ctx, id) + ret0, _ := ret[0].(*mongo.Order) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOne indicates an expected call of FindOne. +func (mr *MockOrderModelMockRecorder) FindOne(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockOrderModel)(nil).FindOne), ctx, id) +} + +// FindOneBusinessID mocks base method. +func (m *MockOrderModel) FindOneBusinessID(ctx context.Context, id string) (*mongo.Order, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOneBusinessID", ctx, id) + ret0, _ := ret[0].(*mongo.Order) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOneBusinessID indicates an expected call of FindOneBusinessID. +func (mr *MockOrderModelMockRecorder) FindOneBusinessID(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneBusinessID", reflect.TypeOf((*MockOrderModel)(nil).FindOneBusinessID), ctx, id) +} + +// Insert mocks base method. +func (m *MockOrderModel) Insert(ctx context.Context, data *mongo.Order) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insert", ctx, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// Insert indicates an expected call of Insert. +func (mr *MockOrderModelMockRecorder) Insert(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockOrderModel)(nil).Insert), ctx, data) +} + +// ListOrder mocks base method. +func (m *MockOrderModel) ListOrder(ctx context.Context, req mongo.GetOrderListReq) ([]mongo.Order, int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListOrder", ctx, req) + ret0, _ := ret[0].([]mongo.Order) + ret1, _ := ret[1].(int64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// ListOrder indicates an expected call of ListOrder. +func (mr *MockOrderModelMockRecorder) ListOrder(ctx, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListOrder", reflect.TypeOf((*MockOrderModel)(nil).ListOrder), ctx, req) +} + +// Update mocks base method. +func (m *MockOrderModel) Update(ctx context.Context, data *mongo.Order) (*mongo0.UpdateResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, data) + ret0, _ := ret[0].(*mongo0.UpdateResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockOrderModelMockRecorder) Update(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockOrderModel)(nil).Update), ctx, data) +} + +// UpdateStatus mocks base method. +func (m *MockOrderModel) UpdateStatus(ctx context.Context, data mongo.UpdateStatusReq) (*mongo0.UpdateResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateStatus", ctx, data) + ret0, _ := ret[0].(*mongo0.UpdateResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateStatus indicates an expected call of UpdateStatus. +func (mr *MockOrderModelMockRecorder) UpdateStatus(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockOrderModel)(nil).UpdateStatus), ctx, data) +} + +// UpdateTimeoutOrder mocks base method. +func (m *MockOrderModel) UpdateTimeoutOrder(ctx context.Context, req mongo.UpdateTimeoutReq) (*mongo0.UpdateResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateTimeoutOrder", ctx, req) + ret0, _ := ret[0].(*mongo0.UpdateResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateTimeoutOrder indicates an expected call of UpdateTimeoutOrder. +func (mr *MockOrderModelMockRecorder) UpdateTimeoutOrder(ctx, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTimeoutOrder", reflect.TypeOf((*MockOrderModel)(nil).UpdateTimeoutOrder), ctx, req) +} diff --git a/internal/mock/model/order_model_gen.go b/internal/mock/model/order_model_gen.go new file mode 100644 index 0000000..30e4f36 --- /dev/null +++ b/internal/mock/model/order_model_gen.go @@ -0,0 +1,101 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./internal/model/mongo/order_model_gen.go +// +// Generated by this command: +// +// mockgen -source=./internal/model/mongo/order_model_gen.go -destination=./internal/mock/model/order_model_gen.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + mongo "app-cloudep-trade-service/internal/model/mongo" + context "context" + reflect "reflect" + + mongo0 "go.mongodb.org/mongo-driver/mongo" + gomock "go.uber.org/mock/gomock" +) + +// MockorderModel is a mock of orderModel interface. +type MockorderModel struct { + ctrl *gomock.Controller + recorder *MockorderModelMockRecorder +} + +// MockorderModelMockRecorder is the mock recorder for MockorderModel. +type MockorderModelMockRecorder struct { + mock *MockorderModel +} + +// NewMockorderModel creates a new mock instance. +func NewMockorderModel(ctrl *gomock.Controller) *MockorderModel { + mock := &MockorderModel{ctrl: ctrl} + mock.recorder = &MockorderModelMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockorderModel) EXPECT() *MockorderModelMockRecorder { + return m.recorder +} + +// Delete mocks base method. +func (m *MockorderModel) Delete(ctx context.Context, id string) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, id) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockorderModelMockRecorder) Delete(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockorderModel)(nil).Delete), ctx, id) +} + +// FindOne mocks base method. +func (m *MockorderModel) FindOne(ctx context.Context, id string) (*mongo.Order, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOne", ctx, id) + ret0, _ := ret[0].(*mongo.Order) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOne indicates an expected call of FindOne. +func (mr *MockorderModelMockRecorder) FindOne(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockorderModel)(nil).FindOne), ctx, id) +} + +// Insert mocks base method. +func (m *MockorderModel) Insert(ctx context.Context, data *mongo.Order) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insert", ctx, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// Insert indicates an expected call of Insert. +func (mr *MockorderModelMockRecorder) Insert(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockorderModel)(nil).Insert), ctx, data) +} + +// Update mocks base method. +func (m *MockorderModel) Update(ctx context.Context, data *mongo.Order) (*mongo0.UpdateResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, data) + ret0, _ := ret[0].(*mongo0.UpdateResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockorderModelMockRecorder) Update(ctx, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockorderModel)(nil).Update), ctx, data) +} diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go index 18b1085..96a4274 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -2,7 +2,8 @@ package svc import ( "app-cloudep-trade-service/internal/config" - model "app-cloudep-trade-service/internal/model/mongo" + duc "app-cloudep-trade-service/internal/domain/usecase" + "app-cloudep-trade-service/internal/usecase" ers "code.30cm.net/digimon/library-go/errs" "code.30cm.net/digimon/library-go/errs/code" @@ -10,21 +11,28 @@ import ( ) type ServiceContext struct { - Config config.Config - Validate vi.Validate - OrderModel model.OrderModel + Config config.Config + Validate vi.Validate + + OrderUseCase duc.OrderUseCase } func NewServiceContext(c config.Config) *ServiceContext { // TODO 改成 Trade ers.Scope = code.CloudEPOrder + om := MustOrderModel(c) + orderUseCase := usecase.NewOrderUseCase(usecase.OrderUseCaseParam{ + OrderModel: om, + }) + return &ServiceContext{ Config: c, Validate: vi.MustValidator( WithDecimalGt(), WithDecimalGte(), ), - OrderModel: MustOrderModel(c), + + OrderUseCase: orderUseCase, } } diff --git a/internal/usecase/order_test.go b/internal/usecase/order_test.go index 56ef42d..dde64d7 100644 --- a/internal/usecase/order_test.go +++ b/internal/usecase/order_test.go @@ -1,55 +1,73 @@ package usecase import ( + "app-cloudep-trade-service/internal/domain" "app-cloudep-trade-service/internal/domain/usecase" + mockmodel "app-cloudep-trade-service/internal/mock/model" model "app-cloudep-trade-service/internal/model/mongo" "context" + "errors" + "github.com/go-playground/assert/v2" + "github.com/shopspring/decimal" + "github.com/zeromicro/go-zero/core/stores/mon" + "go.uber.org/mock/gomock" + "time" + "reflect" "testing" ) -func TestNewOrderUseCase(t *testing.T) { - type args struct { - param OrderUseCaseParam - } - tests := []struct { - name string - args args - want usecase.OrderUseCase - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := NewOrderUseCase(tt.args.param); !reflect.DeepEqual(got, tt.want) { - t.Errorf("NewOrderUseCase() = %v, want %v", got, tt.want) - } - }) - } -} - func TestOrderUseCase_CancelOrder(t *testing.T) { - type fields struct { - OrderModel model.OrderModel + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockOrderModel := mockmodel.NewMockOrderModel(ctrl) + + orderUseCase := &OrderUseCase{ + OrderModel: mockOrderModel, } - type args struct { - ctx context.Context - param usecase.CancelOrderQuery + + cancelOrderQuery := usecase.CancelOrderQuery{ + BusinessID: "business_123", + Status: domain.OrderStatusCancelled, // 使用模擬的狀態 } + tests := []struct { - name string - fields fields - args args - wantErr bool + name string + mockSetup func() + param usecase.CancelOrderQuery + wantErr bool }{ - // TODO: Add test cases. + { + name: "successful order cancellation", + mockSetup: func() { + mockOrderModel.EXPECT().UpdateStatus(gomock.Any(), model.UpdateStatusReq{ + BusinessID: cancelOrderQuery.BusinessID, + Status: cancelOrderQuery.Status.ToInt64(), + }).Return(nil, nil) + }, + param: cancelOrderQuery, + wantErr: false, + }, + { + name: "failed order cancellation", + mockSetup: func() { + mockOrderModel.EXPECT().UpdateStatus(gomock.Any(), model.UpdateStatusReq{ + BusinessID: cancelOrderQuery.BusinessID, + Status: cancelOrderQuery.Status.ToInt64(), + }).Return(nil, errors.New("update error")) + }, + param: cancelOrderQuery, + wantErr: true, + }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - o := &OrderUseCase{ - OrderModel: tt.fields.OrderModel, - } - if err := o.CancelOrder(tt.args.ctx, tt.args.param); (err != nil) != tt.wantErr { + tt.mockSetup() // 設置每個測試用例的 mock 行為 + err := orderUseCase.CancelOrder(context.Background(), tt.param) + if (err != nil) != tt.wantErr { t.Errorf("CancelOrder() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -57,27 +75,77 @@ func TestOrderUseCase_CancelOrder(t *testing.T) { } func TestOrderUseCase_CreateOrder(t *testing.T) { - type fields struct { - OrderModel model.OrderModel + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + // 初始化 mock 依賴 + mockOrderModel := mockmodel.NewMockOrderModel(mockCtrl) + orderUseCase := &OrderUseCase{ + OrderModel: mockOrderModel, } - type args struct { - ctx context.Context - param usecase.CreateOrderReq + + now := time.Now().UTC().UnixNano() + + // 構建測試參數 + createOrderReq := usecase.CreateOrderReq{ + BusinessID: "business_123", + OrderType: domain.OrderTypeTest, + OrderStatus: domain.OrderStatusCreated, + Brand: "test_brand", + OrderUID: "user_123", + ReferenceID: "reference_123", + Count: decimal.NewFromInt(2), + OrderFee: decimal.NewFromFloat(0.01), + Amount: decimal.NewFromFloat(100.0), + ReferenceBrand: StringPtr("reference_brand"), + ReferenceUID: StringPtr("reference_uid"), + WalletStatus: 1, + ThreePartyStatus: Int64Ptr(2), + DirectionType: 1, + CryptoType: StringPtr("BTC"), + PaymentFiat: StringPtr("USD"), + PaymentTemplateID: StringPtr("template_001"), + OrderArrivalTime: Int64Ptr(now), + OrderPaymentTime: Int64Ptr(now), + UnpaidTimeoutSecond: Int64Ptr(3600), + ChainType: StringPtr("mainnet"), + TxHash: StringPtr("tx_hash_123"), + FromAddress: StringPtr("from_address"), + ToAddress: StringPtr("to_address"), + ChainFeeCrypto: StringPtr("ETH"), + Memo: StringPtr("test memo"), + OrderNote: StringPtr("test order note"), } + tests := []struct { - name string - fields fields - args args - wantErr bool + name string + mockSetup func() + param usecase.CreateOrderReq + wantErr bool }{ - // TODO: Add test cases. + { + name: "successful order creation", + mockSetup: func() { + mockOrderModel.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil) + }, + param: createOrderReq, + wantErr: false, + }, + { + name: "failed order creation", + mockSetup: func() { + mockOrderModel.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(errors.New("insert error")) + }, + param: createOrderReq, + wantErr: true, + }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - o := &OrderUseCase{ - OrderModel: tt.fields.OrderModel, - } - if err := o.CreateOrder(tt.args.ctx, tt.args.param); (err != nil) != tt.wantErr { + tt.mockSetup() // 設置每個測試用例的 mock 行為 + err := orderUseCase.CreateOrder(context.Background(), tt.param) + if (err != nil) != tt.wantErr { t.Errorf("CreateOrder() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -85,27 +153,50 @@ func TestOrderUseCase_CreateOrder(t *testing.T) { } func TestOrderUseCase_DeleteOrder(t *testing.T) { - type fields struct { - OrderModel model.OrderModel + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockOrderModel := mockmodel.NewMockOrderModel(ctrl) + + orderUseCase := &OrderUseCase{ + OrderModel: mockOrderModel, } - type args struct { - ctx context.Context - param usecase.DeleteOrderQuery + + // 構建測試參數 + deleteOrderQuery := usecase.DeleteOrderQuery{ + BusinessID: "business_123", } + tests := []struct { - name string - fields fields - args args - wantErr bool + name string + mockSetup func() + param usecase.DeleteOrderQuery + wantErr bool }{ - // TODO: Add test cases. + { + name: "successful order deletion", + mockSetup: func() { + mockOrderModel.EXPECT().DeleteByBusinessID(gomock.Any(), deleteOrderQuery.BusinessID).Return(nil, nil) + }, + param: deleteOrderQuery, + wantErr: false, + }, + { + name: "failed order deletion", + mockSetup: func() { + mockOrderModel.EXPECT().DeleteByBusinessID(gomock.Any(), deleteOrderQuery.BusinessID).Return(nil, errors.New("delete error")) + }, + param: deleteOrderQuery, + wantErr: true, + }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - o := &OrderUseCase{ - OrderModel: tt.fields.OrderModel, - } - if err := o.DeleteOrder(tt.args.ctx, tt.args.param); (err != nil) != tt.wantErr { + tt.mockSetup() // 設置每個測試用例的 mock 行為 + err := orderUseCase.DeleteOrder(context.Background(), tt.param) + if (err != nil) != tt.wantErr { t.Errorf("DeleteOrder() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -113,95 +204,254 @@ func TestOrderUseCase_DeleteOrder(t *testing.T) { } func TestOrderUseCase_GetOrder(t *testing.T) { - type fields struct { - OrderModel model.OrderModel + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + // 初始化 mock 依賴 + mockOrderModel := mockmodel.NewMockOrderModel(mockCtrl) + orderUseCase := &OrderUseCase{ + OrderModel: mockOrderModel, } - type args struct { - ctx context.Context - param usecase.GetOrderQuery + + getOrderQuery := usecase.GetOrderQuery{ + BusinessID: "business_123", } + + // 構建測試用例返回的 Order + mockOrder := &model.Order{ + UpdateTime: 123456789, + CreateTime: 123456789, + BusinessID: "business_123", + OrderType: domain.OrderTypeTest, + OrderStatus: domain.OrderStatusCreated, + Brand: "test_brand", + OrderUID: "user_123", + ReferenceID: "reference_123", + Count: decimal.NewFromInt(2), + OrderFee: decimal.NewFromFloat(0.01), + Amount: decimal.NewFromFloat(100.0), + ReferenceBrand: StringPtr("reference_brand"), + ReferenceUID: StringPtr("reference_uid"), + WalletStatus: 1, + ThreePartyStatus: Int64Ptr(2), + DirectionType: 1, + CryptoType: StringPtr("BTC"), + PaymentFiat: StringPtr("USD"), + PaymentTemplateID: StringPtr("template_001"), + OrderArrivalTime: Int64Ptr(123456789), + OrderPaymentTime: Int64Ptr(123456789), + UnpaidTimeoutSecond: Int64Ptr(3600), + ChainType: StringPtr("mainnet"), + TxHash: StringPtr("tx_hash_123"), + FromAddress: StringPtr("from_address"), + ToAddress: StringPtr("to_address"), + ChainFeeCrypto: StringPtr("ETH"), + Memo: StringPtr("test memo"), + OrderNote: StringPtr("test order note"), + } + tests := []struct { - name string - fields fields - args args - want *usecase.GetOrderResp - wantErr bool + name string + mockSetup func() + param usecase.GetOrderQuery + wantResp *usecase.GetOrderResp + wantErr bool }{ - // TODO: Add test cases. + { + name: "successful order retrieval", + mockSetup: func() { + mockOrderModel.EXPECT().FindOneBusinessID(gomock.Any(), getOrderQuery.BusinessID).Return(mockOrder, nil) + }, + param: getOrderQuery, + wantResp: &usecase.GetOrderResp{ // 构建预期响应 + UpdateTime: mockOrder.UpdateTime, + CreateTime: mockOrder.CreateTime, + BusinessId: mockOrder.BusinessID, + OrderType: mockOrder.OrderType, + OrderStatus: mockOrder.OrderStatus, + Brand: mockOrder.Brand, + OrderUid: mockOrder.OrderUID, + ReferenceId: mockOrder.ReferenceID, + Count: mockOrder.Count.String(), + OrderFee: mockOrder.OrderFee.String(), + Amount: mockOrder.Amount.String(), + ReferenceBrand: mockOrder.ReferenceBrand, + ReferenceUid: mockOrder.ReferenceUID, + WalletStatus: Int64Ptr(mockOrder.WalletStatus), + ThreePartyStatus: mockOrder.ThreePartyStatus, + DirectionType: Int64Ptr(mockOrder.DirectionType), + CryptoType: mockOrder.CryptoType, + PaymentFiat: mockOrder.PaymentFiat, + PaymentTemplateId: mockOrder.PaymentTemplateID, + OrderArrivalTime: mockOrder.OrderArrivalTime, + OrderPaymentTime: mockOrder.OrderPaymentTime, + UnpaidTimeoutSecond: mockOrder.UnpaidTimeoutSecond, + ChainType: mockOrder.ChainType, + TxHash: mockOrder.TxHash, + FromAddress: mockOrder.FromAddress, + ToAddress: mockOrder.ToAddress, + ChainFeeCrypto: mockOrder.ChainFeeCrypto, + Memo: mockOrder.Memo, + OrderNote: mockOrder.OrderNote, + }, + wantErr: false, + }, + { + name: "order not found", + mockSetup: func() { + mockOrderModel.EXPECT().FindOneBusinessID(gomock.Any(), getOrderQuery.BusinessID).Return(nil, mon.ErrNotFound) + }, + param: getOrderQuery, + wantResp: nil, + wantErr: true, + }, + { + name: "database error", + mockSetup: func() { + mockOrderModel.EXPECT().FindOneBusinessID(gomock.Any(), getOrderQuery.BusinessID).Return(nil, errors.New("database error")) + }, + param: getOrderQuery, + wantResp: nil, + wantErr: true, + }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - o := &OrderUseCase{ - OrderModel: tt.fields.OrderModel, - } - got, err := o.GetOrder(tt.args.ctx, tt.args.param) + tt.mockSetup() // 設置每個測試用例的 mock 行為 + resp, err := orderUseCase.GetOrder(context.Background(), tt.param) + if (err != nil) != tt.wantErr { t.Errorf("GetOrder() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("GetOrder() got = %v, want %v", got, tt.want) - } + assert.Equal(t, tt.wantResp, resp) }) } } func TestOrderUseCase_ListOrder(t *testing.T) { - type fields struct { - OrderModel model.OrderModel + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockOrderModel := mockmodel.NewMockOrderModel(mockCtrl) + orderUseCase := &OrderUseCase{ + OrderModel: mockOrderModel, } - type args struct { - ctx context.Context - param usecase.GetOrderListReq + + listOrderQuery := usecase.GetOrderListReq{ + PageIndex: 1, + PageSize: 10, + BusinessID: "business_123", + OrderStatus: []int64{int64(domain.OrderStatusCreated)}, } + + mockOrders := []model.Order{ + { + BusinessID: "business_123", + OrderUID: "user_123", + OrderStatus: domain.OrderStatusCreated, + // 其他欄位根據需要填充 + }, + } + mockTotal := int64(1) + tests := []struct { - name string - fields fields - args args - want *usecase.ListOrderResp - wantErr bool + name string + mockSetup func() + param usecase.GetOrderListReq + wantResp *usecase.ListOrderResp + wantErr bool }{ - // TODO: Add test cases. + { + name: "successful order listing", + mockSetup: func() { + mockOrderModel.EXPECT().ListOrder(gomock.Any(), gomock.Any()).Return(mockOrders, mockTotal, nil) + }, + param: listOrderQuery, + wantResp: &usecase.ListOrderResp{ + Data: orderUseCase.convertOrdersToResponses(mockOrders), + Page: &usecase.Pager{ + Total: mockTotal, + Index: listOrderQuery.PageIndex, + Size: listOrderQuery.PageSize, + }, + }, + wantErr: false, + }, + { + name: "order listing error", + mockSetup: func() { + mockOrderModel.EXPECT().ListOrder(gomock.Any(), gomock.Any()).Return(nil, int64(0), errors.New("listing error")) + }, + param: listOrderQuery, + wantResp: nil, + wantErr: true, + }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - o := &OrderUseCase{ - OrderModel: tt.fields.OrderModel, - } - got, err := o.ListOrder(tt.args.ctx, tt.args.param) + tt.mockSetup() + resp, err := orderUseCase.ListOrder(context.Background(), tt.param) if (err != nil) != tt.wantErr { t.Errorf("ListOrder() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("ListOrder() got = %v, want %v", got, tt.want) - } + assert.Equal(t, tt.wantResp, resp) }) } } func TestOrderUseCase_ModifyOrderStatus(t *testing.T) { - type fields struct { - OrderModel model.OrderModel + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockOrderModel := mockmodel.NewMockOrderModel(mockCtrl) + orderUseCase := &OrderUseCase{ + OrderModel: mockOrderModel, } - type args struct { - ctx context.Context - param *usecase.ModifyOrderQuery + + modifyOrderQuery := &usecase.ModifyOrderQuery{ + BusinessID: "business_123", + Status: int64(domain.OrderStatusCancelled), } + tests := []struct { - name string - fields fields - args args - wantErr bool + name string + mockSetup func() + param *usecase.ModifyOrderQuery + wantErr bool }{ - // TODO: Add test cases. + { + name: "successful order status modification", + mockSetup: func() { + mockOrderModel.EXPECT().UpdateStatus(gomock.Any(), model.UpdateStatusReq{ + BusinessID: modifyOrderQuery.BusinessID, + Status: modifyOrderQuery.Status, + }).Return(nil, nil) + }, + param: modifyOrderQuery, + wantErr: false, + }, + { + name: "order status modification error", + mockSetup: func() { + mockOrderModel.EXPECT().UpdateStatus(gomock.Any(), model.UpdateStatusReq{ + BusinessID: modifyOrderQuery.BusinessID, + Status: modifyOrderQuery.Status, + }).Return(nil, errors.New("update error")) + }, + param: modifyOrderQuery, + wantErr: true, + }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - o := &OrderUseCase{ - OrderModel: tt.fields.OrderModel, - } - if err := o.ModifyOrderStatus(tt.args.ctx, tt.args.param); (err != nil) != tt.wantErr { + tt.mockSetup() + err := orderUseCase.ModifyOrderStatus(context.Background(), tt.param) + if (err != nil) != tt.wantErr { t.Errorf("ModifyOrderStatus() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -209,26 +459,40 @@ func TestOrderUseCase_ModifyOrderStatus(t *testing.T) { } func TestOrderUseCase_OrderStatusTimeout(t *testing.T) { - type fields struct { - OrderModel model.OrderModel - } - type args struct { - ctx context.Context + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + mockOrderModel := mockmodel.NewMockOrderModel(mockCtrl) + orderUseCase := &OrderUseCase{ + OrderModel: mockOrderModel, } + tests := []struct { - name string - fields fields - args args - wantErr bool + name string + mockSetup func() + wantErr bool }{ - // TODO: Add test cases. + { + name: "successful timeout update", + mockSetup: func() { + mockOrderModel.EXPECT().UpdateTimeoutOrder(gomock.Any(), gomock.Any()).Return(nil, nil) + }, + wantErr: false, + }, + { + name: "timeout update error", + mockSetup: func() { + mockOrderModel.EXPECT().UpdateTimeoutOrder(gomock.Any(), gomock.Any()).Return(nil, errors.New("timeout error")) + }, + wantErr: true, + }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - o := &OrderUseCase{ - OrderModel: tt.fields.OrderModel, - } - if err := o.OrderStatusTimeout(tt.args.ctx); (err != nil) != tt.wantErr { + tt.mockSetup() + err := orderUseCase.OrderStatusTimeout(context.Background()) + if (err != nil) != tt.wantErr { t.Errorf("OrderStatusTimeout() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/usecase/utils_test.go b/internal/usecase/utils_test.go new file mode 100644 index 0000000..f4fa7e4 --- /dev/null +++ b/internal/usecase/utils_test.go @@ -0,0 +1,88 @@ +package usecase + +import ( + "github.com/go-playground/assert/v2" + "github.com/shopspring/decimal" + "testing" +) + +// 測試 Int64Ptr 函數 +func TestInt64Ptr(t *testing.T) { + tests := []struct { + name string + val int64 + want *int64 + }{ + { + name: "non-zero value", + val: 123, + want: func() *int64 { v := int64(123); return &v }(), + }, + { + name: "zero value", + val: 0, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Int64Ptr(tt.val) + assert.Equal(t, tt.want, got) + }) + } +} + +// 測試 DecimalToStringPtr 函數 +func TestDecimalToStringPtr(t *testing.T) { + tests := []struct { + name string + val *decimal.Decimal + want *string + }{ + { + name: "non-nil decimal", + val: func() *decimal.Decimal { d := decimal.NewFromFloat(123.45); return &d }(), + want: func() *string { s := "123.45"; return &s }(), + }, + { + name: "nil decimal", + val: nil, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := DecimalToStringPtr(tt.val) + assert.Equal(t, tt.want, got) + }) + } +} + +// 測試 StringPtr 函數 +func TestStringPtr(t *testing.T) { + tests := []struct { + name string + val string + want *string + }{ + { + name: "non-empty string", + val: "test", + want: func() *string { s := "test"; return &s }(), + }, + { + name: "empty string", + val: "", + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := StringPtr(tt.val) + assert.Equal(t, tt.want, got) + }) + } +}