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:
@@ -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列表"`
|
||||
}
|
||||
|
||||
@@ -63,9 +63,10 @@ const (
|
||||
|
||||
// 支付方式常量
|
||||
const (
|
||||
PaymentMethodWallet = "wallet" // 钱包支付
|
||||
PaymentMethodWechat = "wechat" // 微信支付
|
||||
PaymentMethodAlipay = "alipay" // 支付宝支付
|
||||
PaymentMethodWallet = "wallet" // 钱包支付
|
||||
PaymentMethodWechat = "wechat" // 微信支付
|
||||
PaymentMethodAlipay = "alipay" // 支付宝支付
|
||||
PaymentMethodOffline = "offline" // 线下支付(仅平台代购使用)
|
||||
)
|
||||
|
||||
// 支付状态常量
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user