diff --git a/generate/database/mongodb/20241006000001_order.up.mongo b/generate/database/mongodb/20241006000001_order.up.mongo index fb82665..e8510aa 100644 --- a/generate/database/mongodb/20241006000001_order.up.mongo +++ b/generate/database/mongodb/20241006000001_order.up.mongo @@ -1,19 +1,13 @@ use digimon_order; +db.order.createIndex({ "business_id": 1, "order_status": 1, "create_time": -1 }) +db.orders.createIndex({ "reference_id": 1, "reference_uid": 1, "business_id": 1, "uid": 1 }) +db.orders.createIndex({ "order_type": 1, "direction_type": 1, "order_status": 1 }) +db.orders.createIndex({ "create_time": -1, "update_time": -1, "order_arrival_time": -1, "order_payment_time": -1 }) + db.order.createIndex({ "uid": 1, "order_type": 1, "order_status": 1, - "create_time": 1, -}) - -db.order.createIndex({ - "business_id": 1, -}) - -// 查詢時全部要 1&2 有幾個類用幾個累才會中索引 -db.order.createIndex({ - "uid": 1, - "direction_type": 1, - "crypto_type": 1 + "create_time": -1, }) \ No newline at end of file diff --git a/generate/protobuf/order.proto b/generate/protobuf/order.proto index 0b04a0d..f70a722 100644 --- a/generate/protobuf/order.proto +++ b/generate/protobuf/order.proto @@ -152,8 +152,6 @@ service OrderService { rpc CreateOrder(CreateOrderReq) returns (OKResp); // CancelOrder 取消訂單 rpc CancelOrder(CancelOrderReq) returns (OKResp); - // ModifyOrder 修改訂單 - rpc ModifyOrder(ModifyOrderReq) returns (OKResp); // ModifyOrderStatus 修改訂單狀態 rpc ModifyOrderStatus(ModifyOrderStatusReq) returns (OKResp); // DeleteOrder 刪除訂單(軟刪除) diff --git a/go.mod b/go.mod index 72694e2..298332d 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,9 @@ 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/validator/v10 v10.22.0 github.com/shopspring/decimal v1.4.0 + github.com/stretchr/testify v1.9.0 github.com/zeromicro/go-zero v1.7.2 go.mongodb.org/mongo-driver v1.16.1 go.uber.org/mock v0.4.0 @@ -31,7 +33,6 @@ require ( github.com/go-openapi/swag v0.22.4 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.22.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -54,6 +55,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.20.2 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect diff --git a/internal/logic/orderservice/cancel_order_logic_test.go b/internal/logic/orderservice/cancel_order_logic_test.go new file mode 100644 index 0000000..03f53c0 --- /dev/null +++ b/internal/logic/orderservice/cancel_order_logic_test.go @@ -0,0 +1,109 @@ +package orderservicelogic + +import ( + "app-cloudep-order-server/gen_result/pb/order" + mocksvc "app-cloudep-order-server/internal/mock/lib" + model "app-cloudep-order-server/internal/model/mongo" + "app-cloudep-order-server/internal/svc" + "context" + "errors" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/mock/gomock" + "testing" + + mockmodel "app-cloudep-order-server/internal/mock/model" +) + +func TestCancelOrder(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockOrderModel := mockmodel.NewMockOrderModel(ctrl) + mockValidate := mocksvc.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + OrderModel: mockOrderModel, + Validate: mockValidate, + } + + // 測試數據 + cancelReq := &order.CancelOrderReq{ + BusinessId: "12345", + Status: 1, + } + + // 測試數據集 + tests := []struct { + name string + input *order.CancelOrderReq + prepare func() + expectErr bool + }{ + { + name: "成功取消訂單", + input: cancelReq, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬更新狀態成功 + mockOrderModel.EXPECT().UpdateStatus(gomock.Any(), model.UpdateStatusReq{ + BusinessID: "12345", + Status: 1, + }).Return(&mongo.UpdateResult{MatchedCount: 1}, nil).Times(1) + }, + expectErr: false, + }, + { + name: "訂單更新狀態失敗", + input: cancelReq, + prepare: func() { + // 模擬驗證通過 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬更新狀態失敗 + mockOrderModel.EXPECT().UpdateStatus(gomock.Any(), model.UpdateStatusReq{ + BusinessID: "12345", + Status: 1, + }).Return(nil, errors.New("update failed")).Times(1) + }, + expectErr: true, + }, + { + name: "驗證失敗", + input: cancelReq, + prepare: func() { + // 模擬驗證失敗 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("invalid input")).Times(1) + }, + expectErr: true, + }, + } + + // 執行測試 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 CancelOrderLogic + logic := CancelOrderLogic{ + svcCtx: svcCtx, + ctx: context.TODO(), + } + + // 執行 CancelOrder + resp, err := logic.CancelOrder(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, resp) + } else { + assert.NoError(t, err) + assert.NotNil(t, resp) + } + }) + } +} diff --git a/internal/logic/orderservice/create_order_logic_test.go b/internal/logic/orderservice/create_order_logic_test.go new file mode 100644 index 0000000..9f681fa --- /dev/null +++ b/internal/logic/orderservice/create_order_logic_test.go @@ -0,0 +1,406 @@ +package orderservicelogic + +import ( + "app-cloudep-order-server/gen_result/pb/order" + mocksvc "app-cloudep-order-server/internal/mock/lib" + mockmodel "app-cloudep-order-server/internal/mock/model" + "app-cloudep-order-server/internal/svc" + "context" + "errors" + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "testing" + "time" +) + +func TestDecimalPtrFromString(t *testing.T) { + tests := []struct { + name string + input string + expected *decimal.Decimal + }{ + { + name: "valid decimal string", + input: "10.5", + expected: decimalPtr("10.5"), // 使用輔助函數將字串轉換為指針 + }, + { + name: "empty string returns nil", + input: "", + expected: nil, + }, + { + name: "invalid decimal string returns nil", + input: "invalid-decimal", + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := decimalPtrFromString(tt.input) // 調用要測試的函數 + if tt.expected == nil { + assert.Nil(t, result) + } else { + assert.NotNil(t, result) + assert.True(t, tt.expected.Equal(*result), "Expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestBuildCreateOrderReq(t *testing.T) { + tests := []struct { + name string + input *order.CreateOrderReq + expectErr bool + expected *createOrderReq + }{ + { + name: "所有必填字段存在,可選字段為空", + input: &order.CreateOrderReq{ + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "10.5", + OrderFee: "0.5", + Amount: "100", + }, + expectErr: false, + expected: &createOrderReq{ + BusinessID: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUID: "UID123", + ReferenceID: "REF123", + Count: decimal.RequireFromString("10.5"), + OrderFee: decimal.RequireFromString("0.5"), + Amount: decimal.RequireFromString("100"), + }, + }, + { + name: "包含可選字段", + input: &order.CreateOrderReq{ + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "10.5", + OrderFee: "0.5", + Amount: "100", + ReferenceBrand: ptr("OtherBrand"), + WalletStatus: ptrInt64(1), + ThreePartyStatus: ptrInt64(2), + }, + expectErr: false, + expected: &createOrderReq{ + BusinessID: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUID: "UID123", + ReferenceID: "REF123", + Count: decimal.RequireFromString("10.5"), + OrderFee: decimal.RequireFromString("0.5"), + Amount: decimal.RequireFromString("100"), + ReferenceBrand: ptr("OtherBrand"), + WalletStatus: ptrInt64(1), + ThreePartyStatus: ptrInt64(2), + }, + }, + { + name: "無效的十進制字段", + input: &order.CreateOrderReq{ + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "invalid-decimal", // 無效的數值 + }, + expectErr: true, + }, + { + name: "空字符串的可選欄位應返回 nil", + input: &order.CreateOrderReq{ + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "10.5", + OrderFee: "0.5", + Amount: "100", + ThirdPartyFee: ptr(""), // 空字符串 + }, + expectErr: false, + expected: &createOrderReq{ + BusinessID: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUID: "UID123", + ReferenceID: "REF123", + Count: decimal.RequireFromString("10.5"), + OrderFee: decimal.RequireFromString("0.5"), + Amount: decimal.RequireFromString("100"), + ThirdPartyFee: nil, // 空字符串應返回 nil + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := buildCreateOrderReq(tt.input) + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestCreateOrder(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockOrderModel := mockmodel.NewMockOrderModel(ctrl) + mockValidate := mocksvc.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + OrderModel: mockOrderModel, + Validate: mockValidate, + } + + // 測試數據集 + tests := []struct { + name string + input *order.CreateOrderReq + prepare func() + expectErr bool + }{ + { + name: "成功建立訂單", + input: &order.CreateOrderReq{ + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "10.5", + OrderFee: "0.5", + Amount: "100", + }, + prepare: func() { + // 模擬驗證成功 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬資料庫插入成功 + mockOrderModel.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil).Times(1) + }, + expectErr: false, + }, + { + name: "建立訂單時驗證失敗", + input: &order.CreateOrderReq{ + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "10.5", + OrderFee: "0.5", + Amount: "100", + }, + prepare: func() { + // 模擬驗證失敗 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + }, + { + name: "插入資料庫失敗", + input: &order.CreateOrderReq{ + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "10.5", + OrderFee: "0.5", + Amount: "100", + }, + prepare: func() { + // 模擬驗證成功 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬插入資料庫失敗 + mockOrderModel.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(errors.New("insert failed")).Times(1) + }, + expectErr: true, + }, + { + name: "成功建立訂單,必填字段", + input: &order.CreateOrderReq{ + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "10.5", + OrderFee: "0.5", + Amount: "100", + }, + prepare: func() { + // 模擬驗證成功 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬資料庫插入成功 + mockOrderModel.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil).Times(1) + }, + expectErr: false, + }, + { + name: "成功建立訂單,可選字段有值", + input: &order.CreateOrderReq{ + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "10.5", + OrderFee: "0.5", + Amount: "100", + ReferenceBrand: ptr("OtherBrand"), + WalletStatus: ptrInt64(1), + CryptoType: ptr("BTC"), + PaymentFiat: ptr("USD"), + OrderArrivalTime: ptrInt64(time.Now().UTC().Unix()), + }, + prepare: func() { + // 模擬驗證成功 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬資料庫插入成功 + mockOrderModel.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil).Times(1) + }, + expectErr: false, + }, + { + name: "部分可選字段有值,其他為空", + input: &order.CreateOrderReq{ + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "10.5", + OrderFee: "0.5", + Amount: "100", + ReferenceBrand: ptr("OtherBrand"), + WalletStatus: ptrInt64(1), + CryptoType: nil, // 空 + PaymentFiat: nil, // 空 + }, + prepare: func() { + // 模擬驗證成功 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬資料庫插入成功 + mockOrderModel.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil).Times(1) + }, + expectErr: false, + }, + { + name: "無效的十進制數字", + input: &order.CreateOrderReq{ + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "invalid", // 無效的數值 + OrderFee: "0.5", + Amount: "100", + }, + prepare: func() { + // 無需調用資料庫操作,因為會在驗證或轉換時失敗 + }, + expectErr: true, + }, + { + name: "可選欄位為空字符串", + input: &order.CreateOrderReq{ + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "10.5", + OrderFee: "0.5", + Amount: "100", + ThirdPartyFee: ptr(""), // 空字符串 + }, + prepare: func() { + // 模擬驗證成功 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬資料庫插入成功 + mockOrderModel.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil).Times(1) + }, + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 CreateOrderLogic + logic := NewCreateOrderLogic(context.TODO(), svcCtx) + + // 執行 CreateOrder + resp, err := logic.CreateOrder(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, resp) + } else { + assert.NoError(t, err) + assert.NotNil(t, resp) + } + }) + } +} + +// ================================ 輔助函數 ================================ +func ptr(s string) *string { + return &s +} + +func ptrInt64(i int64) *int64 { + return &i +} + +// decimalPtr 是一個輔助函數,用來將字串轉換為 *decimal.Decimal 指針 +func decimalPtr(val string) *decimal.Decimal { + d, _ := decimal.NewFromString(val) + return &d +} diff --git a/internal/logic/orderservice/delete_order_logic_test.go b/internal/logic/orderservice/delete_order_logic_test.go new file mode 100644 index 0000000..0d71124 --- /dev/null +++ b/internal/logic/orderservice/delete_order_logic_test.go @@ -0,0 +1,96 @@ +package orderservicelogic + +import ( + "app-cloudep-order-server/gen_result/pb/order" + mocksvc "app-cloudep-order-server/internal/mock/lib" + mockmodel "app-cloudep-order-server/internal/mock/model" + "app-cloudep-order-server/internal/svc" + "context" + "errors" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "testing" +) + +func TestDeleteOrder(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockOrderModel := mockmodel.NewMockOrderModel(ctrl) + mockValidate := mocksvc.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + OrderModel: mockOrderModel, + Validate: mockValidate, + } + + // 測試數據集 + tests := []struct { + name string + input *order.DeleteOrderReq + prepare func() + expectErr bool + }{ + { + name: "成功刪除訂單", + input: &order.DeleteOrderReq{ + BusinessId: "B123", + }, + prepare: func() { + // 模擬驗證成功 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬資料庫刪除成功 + mockOrderModel.EXPECT().DeleteByBusinessID(gomock.Any(), "B123").Return(nil, nil).Times(1) + }, + expectErr: false, + }, + { + name: "驗證失敗", + input: &order.DeleteOrderReq{ + BusinessId: "B123", + }, + prepare: func() { + // 模擬驗證失敗 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + }, + { + name: "刪除資料庫失敗", + input: &order.DeleteOrderReq{ + BusinessId: "B123", + }, + prepare: func() { + // 模擬驗證成功 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬資料庫刪除失敗 + mockOrderModel.EXPECT().DeleteByBusinessID(gomock.Any(), "B123").Return(nil, errors.New("delete failed")).Times(1) + }, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 DeleteOrderLogic + logic := NewDeleteOrderLogic(context.TODO(), svcCtx) + + // 執行 DeleteOrder + resp, err := logic.DeleteOrder(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, resp) + } else { + assert.NoError(t, err) + assert.NotNil(t, resp) + } + }) + } +} diff --git a/internal/logic/orderservice/get_order_logic.go b/internal/logic/orderservice/get_order_logic.go index 779acb2..2cf4838 100644 --- a/internal/logic/orderservice/get_order_logic.go +++ b/internal/logic/orderservice/get_order_logic.go @@ -3,6 +3,7 @@ package orderservicelogic import ( "app-cloudep-order-server/gen_result/pb/order" "context" + "fmt" ers "code.30cm.net/digimon/library-go/errs" "github.com/shopspring/decimal" @@ -45,6 +46,7 @@ func (l *GetOrderLogic) GetOrder(in *order.GetOrderReq) (*order.GetOrderResp, er if err != nil { return nil, err } + fmt.Println(o) return &order.GetOrderResp{ UpdateTime: o.UpdateTime, @@ -58,37 +60,55 @@ func (l *GetOrderLogic) GetOrder(in *order.GetOrderReq) (*order.GetOrderResp, er Count: o.Count.String(), OrderFee: o.OrderFee.String(), Amount: o.Amount.String(), - // 下面的是未來擴充用,加密貨幣用,或者幣別轉換用,普通訂單用不到 - ReferenceBrand: o.ReferenceBrand, - ReferenceUid: o.ReferenceUID, - WalletStatus: o.WalletStatus, - ThreePartyStatus: o.ThreePartyStatus, - DirectionType: o.DirectionType, - CryptoType: o.CryptoType, - ThirdPartyFee: decimalToString(*o.ThirdPartyFee), - CryptoToUsdtRate: decimalToString(*o.CryptoToUSDTRate), - FiatToUsdRate: decimalToString(*o.FiatToUSDRate), - FeeCryptoToUsdtRate: decimalToString(*o.FeeCryptoToUSDTRate), - UsdtToCryptoTypeRate: decimalToString(*o.USDTToCryptoTypeRate), - PaymentFiat: o.PaymentFiat, - PaymentUnitPrice: decimalToString(*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: decimalToString(*o.ChainFee), - ChainFeeCrypto: o.ChainFeeCrypto, - Memo: o.Memo, - OrderNote: o.OrderNote, + // 下面的為未來擴充用的欄位 + ReferenceBrand: nilString(o.ReferenceBrand), + ReferenceUid: nilString(o.ReferenceUID), + WalletStatus: nilInt64(o.WalletStatus), + ThreePartyStatus: nilInt64(o.ThreePartyStatus), + DirectionType: nilInt64(o.DirectionType), + CryptoType: nilString(o.CryptoType), + ThirdPartyFee: decimalToString(o.ThirdPartyFee), + CryptoToUsdtRate: decimalToString(o.CryptoToUSDTRate), + FiatToUsdRate: decimalToString(o.FiatToUSDRate), + FeeCryptoToUsdtRate: decimalToString(o.FeeCryptoToUSDTRate), + UsdtToCryptoTypeRate: decimalToString(o.USDTToCryptoTypeRate), + PaymentFiat: nilString(o.PaymentFiat), + PaymentUnitPrice: decimalToString(o.PaymentUnitPrice), + PaymentTemplateId: nilString(o.PaymentTemplateID), + OrderArrivalTime: nilInt64(o.OrderArrivalTime), + OrderPaymentTime: nilInt64(o.OrderPaymentTime), + UnpaidTimeoutSecond: nilInt64(o.UnpaidTimeoutSecond), + ChainType: nilString(o.ChainType), + TxHash: nilString(o.TxHash), + FromAddress: nilString(o.FromAddress), + ToAddress: nilString(o.ToAddress), + ChainFee: decimalToString(o.ChainFee), + ChainFeeCrypto: nilString(o.ChainFeeCrypto), + Memo: nilString(o.Memo), + OrderNote: nilString(o.OrderNote), }, nil } -func decimalToString(amount decimal.Decimal) *string { +func decimalToString(amount *decimal.Decimal) *string { + if amount == nil { + return nil + } a := amount.String() return &a } + +func nilString(s *string) *string { + if s == nil { + return nil + } + return s +} + +func nilInt64(i *int64) *int64 { + if i == nil { + return nil + } + + return i +} diff --git a/internal/logic/orderservice/get_order_logic_test.go b/internal/logic/orderservice/get_order_logic_test.go new file mode 100644 index 0000000..eea74a0 --- /dev/null +++ b/internal/logic/orderservice/get_order_logic_test.go @@ -0,0 +1,127 @@ +package orderservicelogic + +import ( + "app-cloudep-order-server/gen_result/pb/order" + mocksvc "app-cloudep-order-server/internal/mock/lib" + mockmodel "app-cloudep-order-server/internal/mock/model" + model "app-cloudep-order-server/internal/model/mongo" + "app-cloudep-order-server/internal/svc" + "context" + "errors" + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "testing" +) + +func TestGetOrder(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockOrderModel := mockmodel.NewMockOrderModel(ctrl) + mockValidate := mocksvc.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + OrderModel: mockOrderModel, + Validate: mockValidate, + } + + // 模擬返回的訂單數據 + mockOrder := &model.Order{ + UpdateTime: 1630000000, + CreateTime: 1620000000, + BusinessID: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUID: "UID123", + ReferenceID: "REF123", + Count: decimal.RequireFromString("10.5"), + OrderFee: decimal.RequireFromString("0.5"), + Amount: decimal.RequireFromString("100"), + } + + // 測試數據集 + tests := []struct { + name string + input *order.GetOrderReq + prepare func() + expectErr bool + expected *order.GetOrderResp + }{ + { + name: "成功取得訂單", + input: &order.GetOrderReq{ + BusinessId: "B123", + }, + prepare: func() { + // 模擬驗證成功 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬查詢訂單成功 + mockOrderModel.EXPECT().FindOneBusinessID(gomock.Any(), "B123").Return(mockOrder, nil).Times(1) + }, + expectErr: false, + expected: &order.GetOrderResp{ + UpdateTime: 1630000000, + CreateTime: 1620000000, + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "10.5", + OrderFee: "0.5", + Amount: "100", + }, + }, + { + name: "驗證失敗", + input: &order.GetOrderReq{ + BusinessId: "B123", + }, + prepare: func() { + // 模擬驗證失敗 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("validation failed")).Times(1) + }, + expectErr: true, + }, + { + name: "查詢訂單失敗", + input: &order.GetOrderReq{ + BusinessId: "B123", + }, + prepare: func() { + // 模擬驗證成功 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬查詢訂單失敗 + mockOrderModel.EXPECT().FindOneBusinessID(gomock.Any(), "B123").Return(nil, errors.New("order not found")).Times(1) + }, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 設置測試環境 + tt.prepare() + + // 初始化 GetOrderLogic + logic := NewGetOrderLogic(context.TODO(), svcCtx) + + // 執行 GetOrder + resp, err := logic.GetOrder(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, resp) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, resp) + } + }) + } +} diff --git a/internal/logic/orderservice/list_order_logic.go b/internal/logic/orderservice/list_order_logic.go index ebe68e7..7e2826a 100644 --- a/internal/logic/orderservice/list_order_logic.go +++ b/internal/logic/orderservice/list_order_logic.go @@ -160,3 +160,8 @@ func ConvertOrdersToGetOrderResp(orders []model.Order) []*order.GetOrderResp { return res } + +// Helper functions +func ptrString(s string) *string { + return &s +} diff --git a/internal/logic/orderservice/list_order_logic_test.go b/internal/logic/orderservice/list_order_logic_test.go new file mode 100644 index 0000000..fc59707 --- /dev/null +++ b/internal/logic/orderservice/list_order_logic_test.go @@ -0,0 +1,458 @@ +package orderservicelogic + +import ( + "app-cloudep-order-server/gen_result/pb/order" + mocksvc "app-cloudep-order-server/internal/mock/lib" + mockmodel "app-cloudep-order-server/internal/mock/model" + model "app-cloudep-order-server/internal/model/mongo" + "app-cloudep-order-server/internal/svc" + "context" + "errors" + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "testing" +) + +func TestConvertOrdersToGetOrderResp(t *testing.T) { + tests := []struct { + name string + input []model.Order + expected []*order.GetOrderResp + }{ + { + name: "空訂單列表", + input: []model.Order{}, + expected: []*order.GetOrderResp{}, + }, + { + name: "單筆訂單", + input: []model.Order{ + { + BusinessID: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUID: "UID123", + ReferenceID: "REF123", + Count: decimal.RequireFromString("10.5"), + OrderFee: decimal.RequireFromString("0.5"), + Amount: decimal.RequireFromString("100"), + }, + }, + expected: []*order.GetOrderResp{ + { + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "10.5", + OrderFee: "0.5", + Amount: "100", + }, + }, + }, + { + name: "多筆訂單", + input: []model.Order{ + { + BusinessID: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUID: "UID123", + ReferenceID: "REF123", + Count: decimal.RequireFromString("10.5"), + OrderFee: decimal.RequireFromString("0.5"), + Amount: decimal.RequireFromString("100"), + }, + { + BusinessID: "B456", + OrderType: 2, + OrderStatus: 3, + Brand: "OtherBrand", + OrderUID: "UID456", + ReferenceID: "REF456", + Count: decimal.RequireFromString("20"), + OrderFee: decimal.RequireFromString("1"), + Amount: decimal.RequireFromString("200"), + }, + }, + expected: []*order.GetOrderResp{ + { + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "10.5", + OrderFee: "0.5", + Amount: "100", + }, + { + BusinessId: "B456", + OrderType: 2, + OrderStatus: 3, + Brand: "OtherBrand", + OrderUid: "UID456", + ReferenceId: "REF456", + Count: "20", + OrderFee: "1", + Amount: "200", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 執行 ConvertOrdersToGetOrderResp 函數 + result := ConvertOrdersToGetOrderResp(tt.input) + + // 驗證結果 + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestOptionalString(t *testing.T) { + t.Run("非 nil 字串", func(t *testing.T) { + str := "hello" + result := optionalString(&str) + assert.NotNil(t, result) + assert.Equal(t, &str, result) + }) + + t.Run("nil 字串", func(t *testing.T) { + result := optionalString(nil) + assert.Nil(t, result) + }) +} + +func TestOptionalInt64(t *testing.T) { + t.Run("非 nil int64", func(t *testing.T) { + num := int64(123) + result := optionalInt64(&num) + assert.NotNil(t, result) + assert.Equal(t, &num, result) + }) + + t.Run("nil int64", func(t *testing.T) { + result := optionalInt64(nil) + assert.Nil(t, result) + }) +} + +func TestOptionalDecimalToString(t *testing.T) { + t.Run("非 nil decimal", func(t *testing.T) { + dec := decimal.NewFromInt(123) + expected := "123" + result := optionalDecimalToString(&dec) + assert.NotNil(t, result) + assert.Equal(t, &expected, result) + }) + + t.Run("nil decimal", func(t *testing.T) { + result := optionalDecimalToString(nil) + assert.Nil(t, result) + }) +} + +func TestConvertOrderToGetOrderResp(t *testing.T) { + tests := []struct { + name string + input model.Order + expected *order.GetOrderResp + }{ + { + name: "所有欄位都有值", + input: model.Order{ + BusinessID: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUID: "UID123", + ReferenceID: "REF123", + Count: decimal.RequireFromString("10.5"), + OrderFee: decimal.RequireFromString("0.5"), + Amount: decimal.RequireFromString("100"), + ReferenceBrand: ptrString("OtherBrand"), + ReferenceUID: ptrString("REFUID"), + WalletStatus: ptrInt64(1), + ThreePartyStatus: ptrInt64(2), + DirectionType: ptrInt64(1), + CryptoType: ptrString("BTC"), + ThirdPartyFee: decimalPtrFromString("0.01"), + CryptoToUSDTRate: decimalPtrFromString("50000"), + FiatToUSDRate: decimalPtrFromString("1"), + FeeCryptoToUSDTRate: decimalPtrFromString("0.02"), + USDTToCryptoTypeRate: decimalPtrFromString("0.00002"), + PaymentFiat: ptrString("USD"), + PaymentUnitPrice: decimalPtrFromString("50000"), + PaymentTemplateID: ptrString("TEMPLATE123"), + OrderArrivalTime: ptrInt64(1630000000), + OrderPaymentTime: ptrInt64(1620000000), + UnpaidTimeoutSecond: ptrInt64(3600), + ChainType: ptrString("ETH"), + TxHash: ptrString("0xABC123"), + FromAddress: ptrString("0xFROM123"), + ToAddress: ptrString("0xTO123"), + ChainFee: decimalPtrFromString("0.001"), + ChainFeeCrypto: ptrString("ETH"), + Memo: ptrString("Test Memo"), + OrderNote: ptrString("Test Note"), + CreateTime: 1620000000, + UpdateTime: 1630000000, + }, + expected: &order.GetOrderResp{ + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "10.5", + OrderFee: "0.5", + Amount: "100", + ReferenceBrand: ptrString("OtherBrand"), + ReferenceUid: ptrString("REFUID"), + WalletStatus: ptrInt64(1), + ThreePartyStatus: ptrInt64(2), + DirectionType: ptrInt64(1), + CryptoType: ptrString("BTC"), + ThirdPartyFee: ptrString("0.01"), + CryptoToUsdtRate: ptrString("50000"), + FiatToUsdRate: ptrString("1"), + FeeCryptoToUsdtRate: ptrString("0.02"), + UsdtToCryptoTypeRate: ptrString("0.00002"), + PaymentFiat: ptrString("USD"), + PaymentUnitPrice: ptrString("50000"), + PaymentTemplateId: ptrString("TEMPLATE123"), + OrderArrivalTime: ptrInt64(1630000000), + OrderPaymentTime: ptrInt64(1620000000), + UnpaidTimeoutSecond: ptrInt64(3600), + ChainType: ptrString("ETH"), + TxHash: ptrString("0xABC123"), + FromAddress: ptrString("0xFROM123"), + ToAddress: ptrString("0xTO123"), + ChainFee: ptrString("0.001"), + ChainFeeCrypto: ptrString("ETH"), + Memo: ptrString("Test Memo"), + OrderNote: ptrString("Test Note"), + CreateTime: 1620000000, + UpdateTime: 1630000000, + }, + }, + { + name: "部分欄位為 nil", + input: model.Order{ + BusinessID: "B456", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUID: "UID456", + ReferenceID: "REF456", + Count: decimal.RequireFromString("10"), + OrderFee: decimal.RequireFromString("1"), + Amount: decimal.RequireFromString("100"), + ReferenceBrand: nil, + ReferenceUID: nil, + WalletStatus: nil, + ThreePartyStatus: nil, + DirectionType: nil, + CryptoType: nil, + ThirdPartyFee: nil, + CryptoToUSDTRate: nil, + FiatToUSDRate: nil, + FeeCryptoToUSDTRate: nil, + USDTToCryptoTypeRate: nil, + PaymentFiat: nil, + PaymentUnitPrice: nil, + PaymentTemplateID: nil, + OrderArrivalTime: nil, + OrderPaymentTime: nil, + UnpaidTimeoutSecond: nil, + ChainType: nil, + TxHash: nil, + FromAddress: nil, + ToAddress: nil, + ChainFee: nil, + ChainFeeCrypto: nil, + Memo: nil, + OrderNote: nil, + CreateTime: 1620000000, + UpdateTime: 1630000000, + }, + expected: &order.GetOrderResp{ + BusinessId: "B456", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID456", + ReferenceId: "REF456", + Count: "10", + OrderFee: "1", + Amount: "100", + ReferenceBrand: nil, + ReferenceUid: nil, + WalletStatus: nil, + ThreePartyStatus: nil, + DirectionType: nil, + CryptoType: nil, + ThirdPartyFee: nil, + CryptoToUsdtRate: nil, + FiatToUsdRate: nil, + FeeCryptoToUsdtRate: nil, + UsdtToCryptoTypeRate: nil, + PaymentFiat: nil, + PaymentUnitPrice: nil, + PaymentTemplateId: nil, + OrderArrivalTime: nil, + OrderPaymentTime: nil, + UnpaidTimeoutSecond: nil, + ChainType: nil, + TxHash: nil, + FromAddress: nil, + ToAddress: nil, + ChainFee: nil, + ChainFeeCrypto: nil, + Memo: nil, + OrderNote: nil, + CreateTime: 1620000000, + UpdateTime: 1630000000, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 執行 ConvertOrderToGetOrderResp 函數 + result := ConvertOrderToGetOrderResp(tt.input) + + // 驗證結果 + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestListOrder(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // 初始化 mock 依賴 + mockOrderModel := mockmodel.NewMockOrderModel(ctrl) + mockValidate := mocksvc.NewMockValidate(ctrl) + + // 初始化服務上下文 + svcCtx := &svc.ServiceContext{ + OrderModel: mockOrderModel, + Validate: mockValidate, + } + + // 測試數據集 + tests := []struct { + name string + input *order.ListOrderReq + prepare func() + expectErr bool + expectedRes *order.ListOrderResp + }{ + { + name: "成功取得訂單列表", + input: &order.ListOrderReq{ + PageIndex: 1, + PageSize: 10, + }, + prepare: func() { + // 模擬驗證成功 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬返回訂單列表 + mockOrderModel.EXPECT().ListOrder(gomock.Any(), gomock.Any()).Return([]model.Order{ + { + BusinessID: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUID: "UID123", + ReferenceID: "REF123", + Count: decimal.RequireFromString("10.5"), + OrderFee: decimal.RequireFromString("0.5"), + Amount: decimal.RequireFromString("100"), + }, + }, int64(1), nil).Times(1) + }, + expectErr: false, + expectedRes: &order.ListOrderResp{ + Data: []*order.GetOrderResp{ + { + BusinessId: "B123", + OrderType: 1, + OrderStatus: 2, + Brand: "TestBrand", + OrderUid: "UID123", + ReferenceId: "REF123", + Count: "10.5", + OrderFee: "0.5", + Amount: "100", + }, + }, + Page: &order.Pager{ + Total: 1, + Index: 1, + Size: 10, + }, + }, + }, + { + name: "訂單查詢失敗", + input: &order.ListOrderReq{ + PageIndex: 1, + PageSize: 10, + }, + prepare: func() { + // 模擬驗證成功 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(nil).Times(1) + // 模擬返回查詢錯誤 + mockOrderModel.EXPECT().ListOrder(gomock.Any(), gomock.Any()).Return(nil, int64(0), errors.New("query failed")).Times(1) + }, + expectErr: true, + }, + { + name: "分頁錯誤", + input: &order.ListOrderReq{ + PageIndex: 0, + PageSize: 10, + }, + prepare: func() { + // 模擬驗證失敗,因為 PageIndex 為 0 + mockValidate.EXPECT().ValidateAll(gomock.Any()).Return(errors.New("invalid pagination")).Times(1) + }, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 準備測試環境 + tt.prepare() + + // 初始化 ListOrderLogic + logic := NewListOrderLogic(context.TODO(), svcCtx) + + // 執行 ListOrder + resp, err := logic.ListOrder(tt.input) + + // 驗證結果 + if tt.expectErr { + assert.Error(t, err) + assert.Nil(t, resp) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedRes, resp) + } + }) + } +} diff --git a/internal/logic/orderservice/modify_order_logic.go b/internal/logic/orderservice/modify_order_logic.go deleted file mode 100644 index 1c2e399..0000000 --- a/internal/logic/orderservice/modify_order_logic.go +++ /dev/null @@ -1,31 +0,0 @@ -package orderservicelogic - -import ( - "app-cloudep-order-server/gen_result/pb/order" - "context" - - "app-cloudep-order-server/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type ModifyOrderLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewModifyOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ModifyOrderLogic { - return &ModifyOrderLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -// ModifyOrder 修改訂單 -func (l *ModifyOrderLogic) ModifyOrder(in *order.ModifyOrderReq) (*order.OKResp, error) { - // todo: add your logic here and delete this line - - return &order.OKResp{}, nil -} 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/model/mongo/order_model.go b/internal/model/mongo/order_model.go index d931bbc..f60bfd5 100644 --- a/internal/model/mongo/order_model.go +++ b/internal/model/mongo/order_model.go @@ -143,8 +143,10 @@ func (m *customOrderModel) DeleteByBusinessID(ctx context.Context, id string) (* func (m *defaultOrderModel) FindOneBusinessID(ctx context.Context, id string) (*Order, error) { var data Order + filter := bson.M{"delete_time": bson.M{"$in": []any{0, nil}}} + filter["business_id"] = id - err := m.conn.FindOne(ctx, &data, bson.M{"business_id": id}) + err := m.conn.FindOne(ctx, &data, filter) switch { case err == nil: return &data, nil diff --git a/internal/server/orderservice/order_service_server.go b/internal/server/orderservice/order_service_server.go index f362241..830e1a8 100644 --- a/internal/server/orderservice/order_service_server.go +++ b/internal/server/orderservice/order_service_server.go @@ -34,11 +34,6 @@ func (s *OrderServiceServer) CancelOrder(ctx context.Context, in *order.CancelOr return l.CancelOrder(in) } -// ModifyOrder 修改訂單 -func (s *OrderServiceServer) ModifyOrder(ctx context.Context, in *order.ModifyOrderReq) (*order.OKResp, error) { - l := orderservicelogic.NewModifyOrderLogic(ctx, s.svcCtx) - return l.ModifyOrder(in) -} // ModifyOrderStatus 修改訂單狀態 func (s *OrderServiceServer) ModifyOrderStatus(ctx context.Context, in *order.ModifyOrderStatusReq) (*order.OKResp, error) {