feat(order): 支持代购订单和强充要求检查

- OrderService 新增代购订单支持
  - 强充要求检查(首次购买最低充值)
  - 代购订单支付限制(无需支付)
  - 强充金额验证
- 新增 OrderDTO 请求/响应结构
  - PurchaseCheckRequest/Response(购买预检)
  - CreatePurchaseOnBehalfRequest(代购订单创建)
- Order 模型新增支付方式
  - PaymentMethodOffline(线下支付,仅平台代购使用)
- OrderService 依赖注入扩展
  - 新增 SeriesAllocationStore、IotCardStore、DeviceStore
  - 支持强充要求检查逻辑
- 完整的集成测试覆盖(534 行)
  - 代购订单创建、强充验证、支付限制等场景

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-01-31 12:01:33 +08:00
parent 22f19377a5
commit 113b3edd69
4 changed files with 779 additions and 5 deletions

View File

@@ -72,3 +72,25 @@ type PayOrderParams struct {
ID uint `path:"id" description:"订单ID" required:"true"`
PayOrderRequest
}
type PurchaseCheckRequest struct {
OrderType string `json:"order_type" validate:"required,oneof=single_card device" required:"true" description:"订单类型 (single_card:单卡购买, device:设备购买)"`
ResourceID uint `json:"resource_id" validate:"required,min=1" required:"true" description:"资源ID (IoT卡ID或设备ID)"`
PackageIDs []uint `json:"package_ids" validate:"required,min=1,max=10,dive,min=1" required:"true" minItems:"1" maxItems:"10" description:"套餐ID列表"`
}
type PurchaseCheckResponse struct {
TotalPackageAmount int64 `json:"total_package_amount" description:"套餐总价(分)"`
NeedForceRecharge bool `json:"need_force_recharge" description:"是否需要强充"`
ForceRechargeAmount int64 `json:"force_recharge_amount" description:"强充金额(分)"`
ActualPayment int64 `json:"actual_payment" description:"实际支付金额(分)"`
WalletCredit int64 `json:"wallet_credit" description:"钱包到账金额(分)"`
Message string `json:"message" description:"提示信息"`
}
type CreatePurchaseOnBehalfRequest struct {
OrderType string `json:"order_type" validate:"required,oneof=single_card device" required:"true" description:"订单类型 (single_card:单卡购买, device:设备购买)"`
IotCardID *uint `json:"iot_card_id" validate:"required_if=OrderType single_card" description:"IoT卡ID(单卡购买时必填)"`
DeviceID *uint `json:"device_id" validate:"required_if=OrderType device" description:"设备ID(设备购买时必填)"`
PackageIDs []uint `json:"package_ids" validate:"required,min=1,max=10,dive,min=1" required:"true" minItems:"1" maxItems:"10" description:"套餐ID列表"`
}

View File

@@ -63,9 +63,10 @@ const (
// 支付方式常量
const (
PaymentMethodWallet = "wallet" // 钱包支付
PaymentMethodWechat = "wechat" // 微信支付
PaymentMethodAlipay = "alipay" // 支付宝支付
PaymentMethodWallet = "wallet" // 钱包支付
PaymentMethodWechat = "wechat" // 微信支付
PaymentMethodAlipay = "alipay" // 支付宝支付
PaymentMethodOffline = "offline" // 线下支付(仅平台代购使用)
)
// 支付状态常量

View File

@@ -27,6 +27,9 @@ type Service struct {
walletStore *postgres.WalletStore
purchaseValidationService *purchase_validation.Service
allocationConfigStore *postgres.ShopSeriesAllocationConfigStore
seriesAllocationStore *postgres.ShopSeriesAllocationStore
iotCardStore *postgres.IotCardStore
deviceStore *postgres.DeviceStore
wechatPayment wechat.PaymentServiceInterface
queueClient *queue.Client
logger *zap.Logger
@@ -39,6 +42,9 @@ func New(
walletStore *postgres.WalletStore,
purchaseValidationService *purchase_validation.Service,
allocationConfigStore *postgres.ShopSeriesAllocationConfigStore,
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
iotCardStore *postgres.IotCardStore,
deviceStore *postgres.DeviceStore,
wechatPayment wechat.PaymentServiceInterface,
queueClient *queue.Client,
logger *zap.Logger,
@@ -50,6 +56,9 @@ func New(
walletStore: walletStore,
purchaseValidationService: purchaseValidationService,
allocationConfigStore: allocationConfigStore,
seriesAllocationStore: seriesAllocationStore,
iotCardStore: iotCardStore,
deviceStore: deviceStore,
wechatPayment: wechatPayment,
queueClient: queueClient,
logger: logger,
@@ -78,6 +87,11 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
return nil, err
}
forceRechargeCheck := s.checkForceRechargeRequirement(validationResult)
if forceRechargeCheck.NeedForceRecharge && validationResult.TotalPrice < forceRechargeCheck.ForceRechargeAmount {
return nil, errors.New(errors.CodeForceRechargeRequired, "首次购买需满足最低充值要求")
}
userID := middleware.GetUserIDFromContext(ctx)
configVersion := s.snapshotCommissionConfig(ctx, validationResult.Allocation.ID)
@@ -254,6 +268,10 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
return errors.New(errors.CodeForbidden, "无权操作此订单")
}
if order.IsPurchaseOnBehalf {
return errors.New(errors.CodeInvalidStatus, "代购订单无需支付")
}
var resourceType string
var resourceID uint
@@ -646,3 +664,206 @@ func (s *Service) WechatPayH5(ctx context.Context, orderID uint, sceneInfo *dto.
H5URL: result.H5URL,
}, nil
}
type ForceRechargeRequirement struct {
NeedForceRecharge bool
ForceRechargeAmount int64
TriggerType string
}
func (s *Service) checkForceRechargeRequirement(result *purchase_validation.PurchaseValidationResult) *ForceRechargeRequirement {
if result.Allocation == nil {
return &ForceRechargeRequirement{NeedForceRecharge: false}
}
allocation := result.Allocation
if !allocation.EnableOneTimeCommission {
return &ForceRechargeRequirement{NeedForceRecharge: false}
}
var firstCommissionPaid bool
if result.Card != nil {
firstCommissionPaid = result.Card.FirstCommissionPaid
} else if result.Device != nil {
firstCommissionPaid = result.Device.FirstCommissionPaid
}
if firstCommissionPaid {
return &ForceRechargeRequirement{NeedForceRecharge: false}
}
if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerSingleRecharge {
return &ForceRechargeRequirement{
NeedForceRecharge: true,
ForceRechargeAmount: allocation.OneTimeCommissionThreshold,
TriggerType: model.OneTimeCommissionTriggerSingleRecharge,
}
}
if allocation.EnableForceRecharge {
forceAmount := allocation.ForceRechargeAmount
if forceAmount == 0 {
forceAmount = allocation.OneTimeCommissionThreshold
}
return &ForceRechargeRequirement{
NeedForceRecharge: true,
ForceRechargeAmount: forceAmount,
TriggerType: model.OneTimeCommissionTriggerAccumulatedRecharge,
}
}
return &ForceRechargeRequirement{NeedForceRecharge: false}
}
func (s *Service) GetPurchaseCheck(ctx context.Context, req *dto.PurchaseCheckRequest) (*dto.PurchaseCheckResponse, error) {
var validationResult *purchase_validation.PurchaseValidationResult
var err error
if req.OrderType == model.OrderTypeSingleCard {
validationResult, err = s.purchaseValidationService.ValidateCardPurchase(ctx, req.ResourceID, req.PackageIDs)
} else if req.OrderType == model.OrderTypeDevice {
validationResult, err = s.purchaseValidationService.ValidateDevicePurchase(ctx, req.ResourceID, req.PackageIDs)
} else {
return nil, errors.New(errors.CodeInvalidParam, "无效的订单类型")
}
if err != nil {
return nil, err
}
forceRechargeCheck := s.checkForceRechargeRequirement(validationResult)
response := &dto.PurchaseCheckResponse{
TotalPackageAmount: validationResult.TotalPrice,
NeedForceRecharge: forceRechargeCheck.NeedForceRecharge,
ForceRechargeAmount: forceRechargeCheck.ForceRechargeAmount,
ActualPayment: validationResult.TotalPrice,
WalletCredit: validationResult.TotalPrice,
}
if forceRechargeCheck.NeedForceRecharge {
if validationResult.TotalPrice < forceRechargeCheck.ForceRechargeAmount {
response.ActualPayment = forceRechargeCheck.ForceRechargeAmount
response.WalletCredit = forceRechargeCheck.ForceRechargeAmount
response.Message = "首次购买需满足最低充值要求"
}
}
return response, nil
}
func (s *Service) CreatePurchaseOnBehalf(ctx context.Context, req *dto.CreatePurchaseOnBehalfRequest, operatorID uint) (*dto.OrderResponse, error) {
var validationResult *purchase_validation.PurchaseValidationResult
var resourceShopID *uint
var err error
if req.OrderType == model.OrderTypeSingleCard {
if req.IotCardID == nil {
return nil, errors.New(errors.CodeInvalidParam, "单卡购买必须指定IoT卡ID")
}
validationResult, err = s.purchaseValidationService.ValidateCardPurchase(ctx, *req.IotCardID, req.PackageIDs)
if err != nil {
return nil, err
}
if validationResult.Card != nil {
resourceShopID = validationResult.Card.ShopID
}
} else if req.OrderType == model.OrderTypeDevice {
if req.DeviceID == nil {
return nil, errors.New(errors.CodeInvalidParam, "设备购买必须指定设备ID")
}
validationResult, err = s.purchaseValidationService.ValidateDevicePurchase(ctx, *req.DeviceID, req.PackageIDs)
if err != nil {
return nil, err
}
if validationResult.Device != nil {
resourceShopID = validationResult.Device.ShopID
}
} else {
return nil, errors.New(errors.CodeInvalidParam, "无效的订单类型")
}
if resourceShopID == nil || *resourceShopID == 0 {
return nil, errors.New(errors.CodeInvalidParam, "资源未分配给代理商,无法代购")
}
buyerID := *resourceShopID
buyerAllocation, err := s.seriesAllocationStore.GetByShopAndSeries(ctx, buyerID, validationResult.Allocation.SeriesID)
if err != nil {
return nil, errors.New(errors.CodeInvalidParam, "买家没有该套餐系列的分配配置")
}
buyerCostPrice := utils.CalculateCostPrice(buyerAllocation, validationResult.TotalPrice)
configVersion := s.snapshotCommissionConfig(ctx, validationResult.Allocation.ID)
var seriesID *uint
var sellerShopID *uint
if validationResult.Allocation != nil {
seriesID = &validationResult.Allocation.SeriesID
sellerShopID = &validationResult.Allocation.ShopID
}
now := time.Now()
order := &model.Order{
BaseModel: model.BaseModel{
Creator: operatorID,
Updater: operatorID,
},
OrderNo: s.orderStore.GenerateOrderNo(),
OrderType: req.OrderType,
BuyerType: model.BuyerTypeAgent,
BuyerID: buyerID,
IotCardID: req.IotCardID,
DeviceID: req.DeviceID,
TotalAmount: buyerCostPrice,
PaymentMethod: model.PaymentMethodOffline,
PaymentStatus: model.PaymentStatusPaid,
PaidAt: &now,
CommissionStatus: model.CommissionStatusPending,
CommissionConfigVersion: configVersion,
SeriesID: seriesID,
SellerShopID: sellerShopID,
SellerCostPrice: buyerCostPrice,
IsPurchaseOnBehalf: true,
}
var items []*model.OrderItem
for _, pkg := range validationResult.Packages {
item := &model.OrderItem{
BaseModel: model.BaseModel{
Creator: operatorID,
Updater: operatorID,
},
PackageID: pkg.ID,
PackageName: pkg.PackageName,
Quantity: 1,
UnitPrice: pkg.SuggestedRetailPrice,
Amount: pkg.SuggestedRetailPrice,
}
items = append(items, item)
}
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(order).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建代购订单失败")
}
for _, item := range items {
item.OrderID = order.ID
}
if err := tx.CreateInBatches(items, 100).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单明细失败")
}
return s.activatePackage(ctx, tx, order)
})
if err != nil {
return nil, err
}
s.enqueueCommissionCalculation(ctx, order.ID)
return s.buildOrderResponse(order, items), nil
}

View File

@@ -126,7 +126,7 @@ func setupOrderTestEnv(t *testing.T) *testEnv {
purchaseValidationSvc := purchase_validation.New(tx, iotCardStore, deviceStore, packageStore, seriesAllocationStore)
logger := zap.NewNop()
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil, nil, nil, logger)
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil, seriesAllocationStore, iotCardStore, deviceStore, nil, nil, logger)
userCtx := middleware.SetUserContext(ctx, &middleware.UserContextInfo{
UserID: 1,
@@ -536,7 +536,7 @@ func TestOrderService_IdempotencyAndConcurrency(t *testing.T) {
purchaseValidationSvc := purchase_validation.New(tx, iotCardStore, deviceStore, packageStore, seriesAllocationStore)
logger := zap.NewNop()
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil, nil, nil, logger)
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil, seriesAllocationStore, iotCardStore, deviceStore, nil, nil, logger)
userCtx := middleware.SetUserContext(ctx, &middleware.UserContextInfo{
UserID: 1,
@@ -620,3 +620,533 @@ func TestOrderService_IdempotencyAndConcurrency(t *testing.T) {
assert.Equal(t, int64(0), usageCount)
})
}
func TestOrderService_ForceRechargeValidation(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
iotCardStore := postgres.NewIotCardStore(tx, rdb)
deviceStore := postgres.NewDeviceStore(tx, rdb)
packageStore := postgres.NewPackageStore(tx)
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
carrierStore := postgres.NewCarrierStore(tx)
shopStore := postgres.NewShopStore(tx, rdb)
orderStore := postgres.NewOrderStore(tx, rdb)
orderItemStore := postgres.NewOrderItemStore(tx, rdb)
walletStore := postgres.NewWalletStore(tx, rdb)
ctx := context.Background()
carrier := &model.Carrier{
CarrierCode: "TEST_CARRIER_FR",
CarrierName: "测试运营商强充",
CarrierType: constants.CarrierTypeCMCC,
Status: constants.StatusEnabled,
}
require.NoError(t, carrierStore.Create(ctx, carrier))
shop := &model.Shop{
ShopName: "测试店铺FR",
ShopCode: "TEST_SHOP_FR",
Level: 1,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, shopStore.Create(ctx, shop))
series := &model.PackageSeries{
SeriesCode: "TEST_SERIES_FR",
SeriesName: "测试套餐系列强充",
Description: "测试用",
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, packageSeriesStore.Create(ctx, series))
allocation := &model.ShopSeriesAllocation{
ShopID: shop.ID,
SeriesID: series.ID,
Status: constants.StatusEnabled,
EnableOneTimeCommission: true,
OneTimeCommissionTrigger: model.OneTimeCommissionTriggerSingleRecharge,
OneTimeCommissionThreshold: 20000,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, seriesAllocationStore.Create(ctx, allocation))
cheapPkg := &model.Package{
PackageCode: "TEST_PKG_CHEAP",
PackageName: "便宜套餐",
SeriesID: series.ID,
PackageType: "formal",
DurationMonths: 1,
DataAmountMB: 512,
SuggestedRetailPrice: 5000,
Status: constants.StatusEnabled,
ShelfStatus: constants.ShelfStatusOn,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, packageStore.Create(ctx, cheapPkg))
expensivePkg := &model.Package{
PackageCode: "TEST_PKG_EXP",
PackageName: "高价套餐",
SeriesID: series.ID,
PackageType: "formal",
DurationMonths: 12,
DataAmountMB: 10240,
SuggestedRetailPrice: 25000,
Status: constants.StatusEnabled,
ShelfStatus: constants.ShelfStatusOn,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, packageStore.Create(ctx, expensivePkg))
shopIDPtr := &shop.ID
card := &model.IotCard{
ICCID: "89860000000000000FR1",
ShopID: shopIDPtr,
CarrierID: carrier.ID,
SeriesAllocationID: &allocation.ID,
Status: constants.StatusEnabled,
FirstCommissionPaid: false,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, iotCardStore.Create(ctx, card))
purchaseValidationSvc := purchase_validation.New(tx, iotCardStore, deviceStore, packageStore, seriesAllocationStore)
logger := zap.NewNop()
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil, seriesAllocationStore, iotCardStore, deviceStore, nil, nil, logger)
userCtx := middleware.SetUserContext(ctx, &middleware.UserContextInfo{
UserID: 1,
UserType: constants.UserTypeAgent,
ShopID: shop.ID,
})
t.Run("强充验证-金额不足拒绝", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{cheapPkg.ID},
}
_, err := orderSvc.Create(userCtx, req, model.BuyerTypeAgent, shop.ID)
require.Error(t, err)
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeForceRechargeRequired, appErr.Code)
})
t.Run("强充验证-金额足够通过", func(t *testing.T) {
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{expensivePkg.ID},
}
resp, err := orderSvc.Create(userCtx, req, model.BuyerTypeAgent, shop.ID)
require.NoError(t, err)
assert.NotZero(t, resp.ID)
assert.Equal(t, expensivePkg.SuggestedRetailPrice, resp.TotalAmount)
})
t.Run("已付佣金-跳过强充验证", func(t *testing.T) {
card2 := &model.IotCard{
ICCID: "89860000000000000FR2",
ShopID: shopIDPtr,
CarrierID: carrier.ID,
SeriesAllocationID: &allocation.ID,
Status: constants.StatusEnabled,
FirstCommissionPaid: true,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, iotCardStore.Create(ctx, card2))
req := &dto.CreateOrderRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &card2.ID,
PackageIDs: []uint{cheapPkg.ID},
}
resp, err := orderSvc.Create(userCtx, req, model.BuyerTypeAgent, shop.ID)
require.NoError(t, err)
assert.NotZero(t, resp.ID)
})
}
func TestOrderService_GetPurchaseCheck(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
iotCardStore := postgres.NewIotCardStore(tx, rdb)
deviceStore := postgres.NewDeviceStore(tx, rdb)
packageStore := postgres.NewPackageStore(tx)
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
carrierStore := postgres.NewCarrierStore(tx)
shopStore := postgres.NewShopStore(tx, rdb)
orderStore := postgres.NewOrderStore(tx, rdb)
orderItemStore := postgres.NewOrderItemStore(tx, rdb)
walletStore := postgres.NewWalletStore(tx, rdb)
ctx := context.Background()
carrier := &model.Carrier{
CarrierCode: "TEST_CARRIER_PC",
CarrierName: "测试运营商预检",
CarrierType: constants.CarrierTypeCMCC,
Status: constants.StatusEnabled,
}
require.NoError(t, carrierStore.Create(ctx, carrier))
shop := &model.Shop{
ShopName: "测试店铺PC",
ShopCode: "TEST_SHOP_PC",
Level: 1,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, shopStore.Create(ctx, shop))
series := &model.PackageSeries{
SeriesCode: "TEST_SERIES_PC",
SeriesName: "测试套餐系列预检",
Description: "测试用",
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, packageSeriesStore.Create(ctx, series))
allocation := &model.ShopSeriesAllocation{
ShopID: shop.ID,
SeriesID: series.ID,
Status: constants.StatusEnabled,
EnableOneTimeCommission: true,
OneTimeCommissionTrigger: model.OneTimeCommissionTriggerSingleRecharge,
OneTimeCommissionThreshold: 10000,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, seriesAllocationStore.Create(ctx, allocation))
pkg := &model.Package{
PackageCode: "TEST_PKG_PC",
PackageName: "测试套餐预检",
SeriesID: series.ID,
PackageType: "formal",
DurationMonths: 1,
DataAmountMB: 1024,
SuggestedRetailPrice: 5000,
Status: constants.StatusEnabled,
ShelfStatus: constants.ShelfStatusOn,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, packageStore.Create(ctx, pkg))
shopIDPtr := &shop.ID
card := &model.IotCard{
ICCID: "89860000000000000PC1",
ShopID: shopIDPtr,
CarrierID: carrier.ID,
SeriesAllocationID: &allocation.ID,
Status: constants.StatusEnabled,
FirstCommissionPaid: false,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, iotCardStore.Create(ctx, card))
purchaseValidationSvc := purchase_validation.New(tx, iotCardStore, deviceStore, packageStore, seriesAllocationStore)
logger := zap.NewNop()
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil, seriesAllocationStore, iotCardStore, deviceStore, nil, nil, logger)
t.Run("预检-需要强充", func(t *testing.T) {
req := &dto.PurchaseCheckRequest{
OrderType: model.OrderTypeSingleCard,
ResourceID: card.ID,
PackageIDs: []uint{pkg.ID},
}
resp, err := orderSvc.GetPurchaseCheck(ctx, req)
require.NoError(t, err)
assert.Equal(t, pkg.SuggestedRetailPrice, resp.TotalPackageAmount)
assert.True(t, resp.NeedForceRecharge)
assert.Equal(t, allocation.OneTimeCommissionThreshold, resp.ForceRechargeAmount)
assert.Equal(t, allocation.OneTimeCommissionThreshold, resp.ActualPayment)
assert.NotEmpty(t, resp.Message)
})
t.Run("预检-无需强充", func(t *testing.T) {
card2 := &model.IotCard{
ICCID: "89860000000000000PC2",
ShopID: shopIDPtr,
CarrierID: carrier.ID,
SeriesAllocationID: &allocation.ID,
Status: constants.StatusEnabled,
FirstCommissionPaid: true,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, iotCardStore.Create(ctx, card2))
req := &dto.PurchaseCheckRequest{
OrderType: model.OrderTypeSingleCard,
ResourceID: card2.ID,
PackageIDs: []uint{pkg.ID},
}
resp, err := orderSvc.GetPurchaseCheck(ctx, req)
require.NoError(t, err)
assert.Equal(t, pkg.SuggestedRetailPrice, resp.TotalPackageAmount)
assert.False(t, resp.NeedForceRecharge)
assert.Equal(t, pkg.SuggestedRetailPrice, resp.ActualPayment)
assert.Empty(t, resp.Message)
})
}
func TestOrderService_CreatePurchaseOnBehalf(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
iotCardStore := postgres.NewIotCardStore(tx, rdb)
deviceStore := postgres.NewDeviceStore(tx, rdb)
packageStore := postgres.NewPackageStore(tx)
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
carrierStore := postgres.NewCarrierStore(tx)
shopStore := postgres.NewShopStore(tx, rdb)
orderStore := postgres.NewOrderStore(tx, rdb)
orderItemStore := postgres.NewOrderItemStore(tx, rdb)
walletStore := postgres.NewWalletStore(tx, rdb)
ctx := context.Background()
carrier := &model.Carrier{
CarrierCode: "TEST_CARRIER_POB",
CarrierName: "测试运营商代购",
CarrierType: constants.CarrierTypeCMCC,
Status: constants.StatusEnabled,
}
require.NoError(t, carrierStore.Create(ctx, carrier))
shop := &model.Shop{
ShopName: "测试店铺POB",
ShopCode: "TEST_SHOP_POB",
Level: 1,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, shopStore.Create(ctx, shop))
series := &model.PackageSeries{
SeriesCode: "TEST_SERIES_POB",
SeriesName: "测试套餐系列代购",
Description: "测试用",
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, packageSeriesStore.Create(ctx, series))
allocation := &model.ShopSeriesAllocation{
ShopID: shop.ID,
SeriesID: series.ID,
AllocatorShopID: 0,
BaseCommissionMode: model.CommissionModePercent,
BaseCommissionValue: 100,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, seriesAllocationStore.Create(ctx, allocation))
pkg := &model.Package{
PackageCode: "TEST_PKG_POB",
PackageName: "测试套餐代购",
SeriesID: series.ID,
PackageType: "formal",
DurationMonths: 1,
DataAmountMB: 1024,
SuggestedRetailPrice: 10000,
Status: constants.StatusEnabled,
ShelfStatus: constants.ShelfStatusOn,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, packageStore.Create(ctx, pkg))
shopIDPtr := &shop.ID
card := &model.IotCard{
ICCID: "89860000000000000POB",
ShopID: shopIDPtr,
CarrierID: carrier.ID,
SeriesAllocationID: &allocation.ID,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, iotCardStore.Create(ctx, card))
purchaseValidationSvc := purchase_validation.New(tx, iotCardStore, deviceStore, packageStore, seriesAllocationStore)
logger := zap.NewNop()
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil, seriesAllocationStore, iotCardStore, deviceStore, nil, nil, logger)
t.Run("代购订单创建成功", func(t *testing.T) {
req := &dto.CreatePurchaseOnBehalfRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{pkg.ID},
}
resp, err := orderSvc.CreatePurchaseOnBehalf(ctx, req, 1)
require.NoError(t, err)
assert.NotZero(t, resp.ID)
assert.Equal(t, model.PaymentStatusPaid, resp.PaymentStatus)
assert.Equal(t, model.PaymentMethodOffline, resp.PaymentMethod)
assert.Equal(t, model.BuyerTypeAgent, resp.BuyerType)
assert.Equal(t, shop.ID, resp.BuyerID)
assert.NotNil(t, resp.PaidAt)
expectedCostPrice := pkg.SuggestedRetailPrice - (pkg.SuggestedRetailPrice * allocation.BaseCommissionValue / 1000)
assert.Equal(t, expectedCostPrice, resp.TotalAmount)
var usageCount int64
err = tx.Model(&model.PackageUsage{}).Where("order_id = ?", resp.ID).Count(&usageCount).Error
require.NoError(t, err)
assert.Equal(t, int64(1), usageCount)
})
t.Run("代购订单-资源未分配拒绝", func(t *testing.T) {
cardNoShop := &model.IotCard{
ICCID: "89860000000000NOSHOP",
ShopID: nil,
CarrierID: carrier.ID,
SeriesAllocationID: &allocation.ID,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, iotCardStore.Create(ctx, cardNoShop))
req := &dto.CreatePurchaseOnBehalfRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &cardNoShop.ID,
PackageIDs: []uint{pkg.ID},
}
_, err := orderSvc.CreatePurchaseOnBehalf(ctx, req, 1)
require.Error(t, err)
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
})
}
func TestOrderService_WalletPay_PurchaseOnBehalf(t *testing.T) {
tx := testutils.NewTestTransaction(t)
rdb := testutils.GetTestRedis(t)
testutils.CleanTestRedisKeys(t, rdb)
iotCardStore := postgres.NewIotCardStore(tx, rdb)
deviceStore := postgres.NewDeviceStore(tx, rdb)
packageStore := postgres.NewPackageStore(tx)
seriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
packageSeriesStore := postgres.NewPackageSeriesStore(tx)
carrierStore := postgres.NewCarrierStore(tx)
shopStore := postgres.NewShopStore(tx, rdb)
orderStore := postgres.NewOrderStore(tx, rdb)
orderItemStore := postgres.NewOrderItemStore(tx, rdb)
walletStore := postgres.NewWalletStore(tx, rdb)
ctx := context.Background()
carrier := &model.Carrier{
CarrierCode: "TEST_CARRIER_WP",
CarrierName: "测试运营商WP",
CarrierType: constants.CarrierTypeCMCC,
Status: constants.StatusEnabled,
}
require.NoError(t, carrierStore.Create(ctx, carrier))
shop := &model.Shop{
ShopName: "测试店铺WP",
ShopCode: "TEST_SHOP_WP",
Level: 1,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, shopStore.Create(ctx, shop))
series := &model.PackageSeries{
SeriesCode: "TEST_SERIES_WP",
SeriesName: "测试套餐系列WP",
Description: "测试用",
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, packageSeriesStore.Create(ctx, series))
allocation := &model.ShopSeriesAllocation{
ShopID: shop.ID,
SeriesID: series.ID,
AllocatorShopID: 0,
BaseCommissionMode: model.CommissionModePercent,
BaseCommissionValue: 100,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, seriesAllocationStore.Create(ctx, allocation))
pkg := &model.Package{
PackageCode: "TEST_PKG_WP",
PackageName: "测试套餐WP",
SeriesID: series.ID,
PackageType: "formal",
DurationMonths: 1,
DataAmountMB: 1024,
SuggestedRetailPrice: 10000,
Status: constants.StatusEnabled,
ShelfStatus: constants.ShelfStatusOn,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, packageStore.Create(ctx, pkg))
wallet := &model.Wallet{
ResourceType: "shop",
ResourceID: shop.ID,
WalletType: "main",
Balance: 100000,
Version: 1,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, tx.Create(wallet).Error)
shopIDPtr := &shop.ID
card := &model.IotCard{
ICCID: "89860000000000000WP1",
ShopID: shopIDPtr,
CarrierID: carrier.ID,
SeriesAllocationID: &allocation.ID,
Status: constants.StatusEnabled,
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
}
require.NoError(t, iotCardStore.Create(ctx, card))
purchaseValidationSvc := purchase_validation.New(tx, iotCardStore, deviceStore, packageStore, seriesAllocationStore)
logger := zap.NewNop()
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil, seriesAllocationStore, iotCardStore, deviceStore, nil, nil, logger)
t.Run("代购订单无法进行钱包支付", func(t *testing.T) {
req := &dto.CreatePurchaseOnBehalfRequest{
OrderType: model.OrderTypeSingleCard,
IotCardID: &card.ID,
PackageIDs: []uint{pkg.ID},
}
created, err := orderSvc.CreatePurchaseOnBehalf(ctx, req, 1)
require.NoError(t, err)
err = orderSvc.WalletPay(ctx, created.ID, model.BuyerTypeAgent, shop.ID)
require.Error(t, err)
appErr, ok := err.(*errors.AppError)
require.True(t, ok)
assert.Equal(t, errors.CodeInvalidStatus, appErr.Code)
})
}