feat: 实现订单支付功能模块
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m36s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m36s
- 新增订单管理、支付回调、购买验证等核心服务 - 实现订单、订单项目的数据存储层和 API 接口 - 添加订单数据库迁移和 DTO 定义 - 更新 API 文档和路由配置 - 同步 3 个新规范到主规范库(订单管理、订单支付、套餐购买验证) - 完成 OpenSpec 变更归档 Ultraworked with Sisyphus
This commit is contained in:
420
internal/service/order/service.go
Normal file
420
internal/service/order/service.go
Normal file
@@ -0,0 +1,420 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/purchase_validation"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
orderStore *postgres.OrderStore
|
||||
orderItemStore *postgres.OrderItemStore
|
||||
walletStore *postgres.WalletStore
|
||||
purchaseValidationService *purchase_validation.Service
|
||||
allocationConfigStore *postgres.ShopSeriesAllocationConfigStore
|
||||
}
|
||||
|
||||
func New(
|
||||
db *gorm.DB,
|
||||
orderStore *postgres.OrderStore,
|
||||
orderItemStore *postgres.OrderItemStore,
|
||||
walletStore *postgres.WalletStore,
|
||||
purchaseValidationService *purchase_validation.Service,
|
||||
allocationConfigStore *postgres.ShopSeriesAllocationConfigStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
orderStore: orderStore,
|
||||
orderItemStore: orderItemStore,
|
||||
walletStore: walletStore,
|
||||
purchaseValidationService: purchaseValidationService,
|
||||
allocationConfigStore: allocationConfigStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyerType string, buyerID uint) (*dto.OrderResponse, error) {
|
||||
var validationResult *purchase_validation.PurchaseValidationResult
|
||||
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)
|
||||
} 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)
|
||||
} else {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "无效的订单类型")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
configVersion := s.snapshotCommissionConfig(ctx, validationResult.Allocation.ID)
|
||||
|
||||
order := &model.Order{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: userID,
|
||||
Updater: userID,
|
||||
},
|
||||
OrderNo: s.orderStore.GenerateOrderNo(),
|
||||
OrderType: req.OrderType,
|
||||
BuyerType: buyerType,
|
||||
BuyerID: buyerID,
|
||||
IotCardID: req.IotCardID,
|
||||
DeviceID: req.DeviceID,
|
||||
TotalAmount: validationResult.TotalPrice,
|
||||
PaymentStatus: model.PaymentStatusPending,
|
||||
CommissionStatus: model.CommissionStatusPending,
|
||||
CommissionConfigVersion: configVersion,
|
||||
}
|
||||
|
||||
var items []*model.OrderItem
|
||||
for _, pkg := range validationResult.Packages {
|
||||
item := &model.OrderItem{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: userID,
|
||||
Updater: userID,
|
||||
},
|
||||
PackageID: pkg.ID,
|
||||
PackageName: pkg.PackageName,
|
||||
Quantity: 1,
|
||||
UnitPrice: pkg.SuggestedRetailPrice,
|
||||
Amount: pkg.SuggestedRetailPrice,
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
if err := s.orderStore.Create(ctx, order, items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.buildOrderResponse(order, items), nil
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, id uint) (*dto.OrderResponse, error) {
|
||||
order, items, err := s.orderStore.GetByIDWithItems(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.buildOrderResponse(order, items), nil
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, req *dto.OrderListRequest, buyerType string, buyerID uint) (*dto.OrderListResponse, error) {
|
||||
page := req.Page
|
||||
pageSize := req.PageSize
|
||||
if page == 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize == 0 {
|
||||
pageSize = constants.DefaultPageSize
|
||||
}
|
||||
|
||||
opts := &store.QueryOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}
|
||||
|
||||
filters := map[string]any{
|
||||
"buyer_type": buyerType,
|
||||
"buyer_id": buyerID,
|
||||
}
|
||||
if req.PaymentStatus != nil {
|
||||
filters["payment_status"] = *req.PaymentStatus
|
||||
}
|
||||
if req.OrderType != "" {
|
||||
filters["order_type"] = req.OrderType
|
||||
}
|
||||
if req.OrderNo != "" {
|
||||
filters["order_no"] = req.OrderNo
|
||||
}
|
||||
if req.StartTime != nil {
|
||||
filters["start_time"] = req.StartTime
|
||||
}
|
||||
if req.EndTime != nil {
|
||||
filters["end_time"] = req.EndTime
|
||||
}
|
||||
|
||||
orders, total, err := s.orderStore.List(ctx, opts, filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var orderIDs []uint
|
||||
for _, o := range orders {
|
||||
orderIDs = append(orderIDs, o.ID)
|
||||
}
|
||||
|
||||
itemsMap := make(map[uint][]*model.OrderItem)
|
||||
if len(orderIDs) > 0 {
|
||||
allItems, err := s.orderItemStore.ListByOrderIDs(ctx, orderIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range allItems {
|
||||
itemsMap[item.OrderID] = append(itemsMap[item.OrderID], item)
|
||||
}
|
||||
}
|
||||
|
||||
var list []*dto.OrderResponse
|
||||
for _, o := range orders {
|
||||
list = append(list, s.buildOrderResponse(o, itemsMap[o.ID]))
|
||||
}
|
||||
|
||||
totalPages := int(total) / pageSize
|
||||
if int(total)%pageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
return &dto.OrderListResponse{
|
||||
List: list,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: totalPages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) Cancel(ctx context.Context, id uint, buyerType string, buyerID uint) error {
|
||||
order, err := s.orderStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if order.BuyerType != buyerType || order.BuyerID != buyerID {
|
||||
return errors.New(errors.CodeForbidden, "无权操作此订单")
|
||||
}
|
||||
|
||||
if order.PaymentStatus != model.PaymentStatusPending {
|
||||
return errors.New(errors.CodeInvalidStatus, "只能取消待支付的订单")
|
||||
}
|
||||
|
||||
return s.orderStore.UpdatePaymentStatus(ctx, id, model.PaymentStatusCancelled, nil)
|
||||
}
|
||||
|
||||
func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string, buyerID uint) error {
|
||||
order, err := s.orderStore.GetByID(ctx, orderID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if order.BuyerType != buyerType || order.BuyerID != buyerID {
|
||||
return errors.New(errors.CodeForbidden, "无权操作此订单")
|
||||
}
|
||||
|
||||
if order.PaymentStatus != model.PaymentStatusPending {
|
||||
return errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
|
||||
}
|
||||
|
||||
var resourceType string
|
||||
var resourceID uint
|
||||
|
||||
if buyerType == model.BuyerTypePersonal {
|
||||
if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil {
|
||||
resourceType = "iot_card"
|
||||
resourceID = *order.IotCardID
|
||||
} else if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil {
|
||||
resourceType = "device"
|
||||
resourceID = *order.DeviceID
|
||||
} else {
|
||||
return errors.New(errors.CodeInvalidParam, "无法确定钱包归属")
|
||||
}
|
||||
} else if buyerType == model.BuyerTypeAgent {
|
||||
resourceType = "shop"
|
||||
resourceID = buyerID
|
||||
} else {
|
||||
return errors.New(errors.CodeInvalidParam, "不支持的买家类型")
|
||||
}
|
||||
|
||||
wallet, err := s.walletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID, "main")
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeWalletNotFound, "钱包不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if wallet.Balance < order.TotalAmount {
|
||||
return errors.New(errors.CodeInsufficientBalance, "余额不足")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Model(&model.Wallet{}).
|
||||
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, order.TotalAmount, wallet.Version).
|
||||
Updates(map[string]any{
|
||||
"balance": gorm.Expr("balance - ?", order.TotalAmount),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突")
|
||||
}
|
||||
|
||||
if err := tx.Model(&model.Order{}).Where("id = ?", orderID).Updates(map[string]any{
|
||||
"payment_status": model.PaymentStatusPaid,
|
||||
"payment_method": model.PaymentMethodWallet,
|
||||
"paid_at": now,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.activatePackage(ctx, tx, order)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, paymentMethod string) error {
|
||||
order, err := s.orderStore.GetByOrderNo(ctx, orderNo)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeNotFound, "订单不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if order.PaymentStatus == model.PaymentStatusPaid {
|
||||
return nil
|
||||
}
|
||||
|
||||
if order.PaymentStatus != model.PaymentStatusPending {
|
||||
return errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.Order{}).Where("id = ?", order.ID).Updates(map[string]any{
|
||||
"payment_status": model.PaymentStatusPaid,
|
||||
"payment_method": paymentMethod,
|
||||
"paid_at": now,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.activatePackage(ctx, tx, order)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) activatePackage(ctx context.Context, tx *gorm.DB, order *model.Order) error {
|
||||
items, err := s.orderItemStore.ListByOrderID(ctx, order.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, item := range items {
|
||||
var pkg model.Package
|
||||
if err := tx.First(&pkg, item.PackageID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usage := &model.PackageUsage{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: order.Creator,
|
||||
Updater: order.Creator,
|
||||
},
|
||||
OrderID: order.ID,
|
||||
PackageID: item.PackageID,
|
||||
UsageType: order.OrderType,
|
||||
DataLimitMB: pkg.DataAmountMB,
|
||||
ActivatedAt: now,
|
||||
ExpiresAt: now.AddDate(0, pkg.DurationMonths, 0),
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil {
|
||||
usage.IotCardID = *order.IotCardID
|
||||
} else if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil {
|
||||
usage.DeviceID = *order.DeviceID
|
||||
}
|
||||
|
||||
if err := tx.Create(usage).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) snapshotCommissionConfig(ctx context.Context, allocationID uint) int {
|
||||
if s.allocationConfigStore == nil {
|
||||
return 0
|
||||
}
|
||||
config, err := s.allocationConfigStore.GetEffective(ctx, allocationID, time.Now())
|
||||
if err != nil || config == nil {
|
||||
return 0
|
||||
}
|
||||
return config.Version
|
||||
}
|
||||
|
||||
func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderItem) *dto.OrderResponse {
|
||||
var itemResponses []*dto.OrderItemResponse
|
||||
for _, item := range items {
|
||||
itemResponses = append(itemResponses, &dto.OrderItemResponse{
|
||||
ID: item.ID,
|
||||
PackageID: item.PackageID,
|
||||
PackageName: item.PackageName,
|
||||
Quantity: item.Quantity,
|
||||
UnitPrice: item.UnitPrice,
|
||||
Amount: item.Amount,
|
||||
})
|
||||
}
|
||||
|
||||
statusText := ""
|
||||
switch order.PaymentStatus {
|
||||
case model.PaymentStatusPending:
|
||||
statusText = "待支付"
|
||||
case model.PaymentStatusPaid:
|
||||
statusText = "已支付"
|
||||
case model.PaymentStatusCancelled:
|
||||
statusText = "已取消"
|
||||
case model.PaymentStatusRefunded:
|
||||
statusText = "已退款"
|
||||
}
|
||||
|
||||
return &dto.OrderResponse{
|
||||
ID: order.ID,
|
||||
OrderNo: order.OrderNo,
|
||||
OrderType: order.OrderType,
|
||||
BuyerType: order.BuyerType,
|
||||
BuyerID: order.BuyerID,
|
||||
IotCardID: order.IotCardID,
|
||||
DeviceID: order.DeviceID,
|
||||
TotalAmount: order.TotalAmount,
|
||||
PaymentMethod: order.PaymentMethod,
|
||||
PaymentStatus: order.PaymentStatus,
|
||||
PaymentStatusText: statusText,
|
||||
PaidAt: order.PaidAt,
|
||||
CommissionStatus: order.CommissionStatus,
|
||||
CommissionConfigVersion: order.CommissionConfigVersion,
|
||||
Items: itemResponses,
|
||||
CreatedAt: order.CreatedAt,
|
||||
UpdatedAt: order.UpdatedAt,
|
||||
}
|
||||
}
|
||||
430
internal/service/order/service_test.go
Normal file
430
internal/service/order/service_test.go
Normal file
@@ -0,0 +1,430 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/purchase_validation"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testEnv struct {
|
||||
ctx context.Context
|
||||
svc *Service
|
||||
card *model.IotCard
|
||||
device *model.Device
|
||||
pkg *model.Package
|
||||
shop *model.Shop
|
||||
wallet *model.Wallet
|
||||
allocation *model.ShopSeriesAllocation
|
||||
}
|
||||
|
||||
func setupOrderTestEnv(t *testing.T) *testEnv {
|
||||
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_ORDER",
|
||||
CarrierName: "测试运营商",
|
||||
CarrierType: constants.CarrierTypeCMCC,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, carrierStore.Create(ctx, carrier))
|
||||
|
||||
shop := &model.Shop{
|
||||
ShopName: "测试店铺ORDER",
|
||||
ShopCode: "TEST_SHOP_ORDER",
|
||||
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_ORDER",
|
||||
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,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, allocation))
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: "TEST_PKG_ORDER",
|
||||
PackageName: "测试套餐",
|
||||
SeriesID: series.ID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
DataAmountMB: 1024,
|
||||
SuggestedRetailPrice: 9900,
|
||||
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: "89860000000000000002",
|
||||
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))
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: "DEV_TEST_ORDER_001",
|
||||
ShopID: shopIDPtr,
|
||||
SeriesAllocationID: &allocation.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, deviceStore.Create(ctx, device))
|
||||
|
||||
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)
|
||||
|
||||
purchaseValidationSvc := purchase_validation.New(tx, iotCardStore, deviceStore, packageStore, seriesAllocationStore)
|
||||
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil)
|
||||
|
||||
userCtx := middleware.SetUserContext(ctx, &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: shop.ID,
|
||||
})
|
||||
|
||||
return &testEnv{
|
||||
ctx: userCtx,
|
||||
svc: orderSvc,
|
||||
card: card,
|
||||
device: device,
|
||||
pkg: pkg,
|
||||
shop: shop,
|
||||
wallet: wallet,
|
||||
allocation: allocation,
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderService_Create(t *testing.T) {
|
||||
env := setupOrderTestEnv(t)
|
||||
|
||||
t.Run("创建单卡订单成功", func(t *testing.T) {
|
||||
req := &dto.CreateOrderRequest{
|
||||
OrderType: model.OrderTypeSingleCard,
|
||||
IotCardID: &env.card.ID,
|
||||
PackageIDs: []uint{env.pkg.ID},
|
||||
}
|
||||
|
||||
resp, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, resp.ID)
|
||||
assert.Contains(t, resp.OrderNo, "ORD")
|
||||
assert.Equal(t, model.OrderTypeSingleCard, resp.OrderType)
|
||||
assert.Equal(t, model.BuyerTypeAgent, resp.BuyerType)
|
||||
assert.Equal(t, env.shop.ID, resp.BuyerID)
|
||||
assert.Equal(t, env.pkg.SuggestedRetailPrice, resp.TotalAmount)
|
||||
assert.Equal(t, model.PaymentStatusPending, resp.PaymentStatus)
|
||||
assert.Len(t, resp.Items, 1)
|
||||
})
|
||||
|
||||
t.Run("创建设备订单成功", func(t *testing.T) {
|
||||
req := &dto.CreateOrderRequest{
|
||||
OrderType: model.OrderTypeDevice,
|
||||
DeviceID: &env.device.ID,
|
||||
PackageIDs: []uint{env.pkg.ID},
|
||||
}
|
||||
|
||||
resp, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, resp.ID)
|
||||
assert.Equal(t, model.OrderTypeDevice, resp.OrderType)
|
||||
})
|
||||
|
||||
t.Run("单卡订单缺少卡ID", func(t *testing.T) {
|
||||
req := &dto.CreateOrderRequest{
|
||||
OrderType: model.OrderTypeSingleCard,
|
||||
PackageIDs: []uint{env.pkg.ID},
|
||||
}
|
||||
|
||||
_, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("设备订单缺少设备ID", func(t *testing.T) {
|
||||
req := &dto.CreateOrderRequest{
|
||||
OrderType: model.OrderTypeDevice,
|
||||
PackageIDs: []uint{env.pkg.ID},
|
||||
}
|
||||
|
||||
_, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOrderService_Get(t *testing.T) {
|
||||
env := setupOrderTestEnv(t)
|
||||
|
||||
req := &dto.CreateOrderRequest{
|
||||
OrderType: model.OrderTypeSingleCard,
|
||||
IotCardID: &env.card.ID,
|
||||
PackageIDs: []uint{env.pkg.ID},
|
||||
}
|
||||
created, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("获取订单成功", func(t *testing.T) {
|
||||
resp, err := env.svc.Get(env.ctx, created.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, created.OrderNo, resp.OrderNo)
|
||||
assert.Len(t, resp.Items, 1)
|
||||
})
|
||||
|
||||
t.Run("订单不存在", func(t *testing.T) {
|
||||
_, err := env.svc.Get(env.ctx, 99999)
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeNotFound, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOrderService_List(t *testing.T) {
|
||||
env := setupOrderTestEnv(t)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
req := &dto.CreateOrderRequest{
|
||||
OrderType: model.OrderTypeSingleCard,
|
||||
IotCardID: &env.card.ID,
|
||||
PackageIDs: []uint{env.pkg.ID},
|
||||
}
|
||||
_, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("列表查询", func(t *testing.T) {
|
||||
listReq := &dto.OrderListRequest{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
resp, err := env.svc.List(env.ctx, listReq, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, resp.Total, int64(3))
|
||||
assert.GreaterOrEqual(t, len(resp.List), 3)
|
||||
})
|
||||
|
||||
t.Run("按支付状态过滤", func(t *testing.T) {
|
||||
status := model.PaymentStatusPending
|
||||
listReq := &dto.OrderListRequest{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
PaymentStatus: &status,
|
||||
}
|
||||
resp, err := env.svc.List(env.ctx, listReq, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
for _, o := range resp.List {
|
||||
assert.Equal(t, model.PaymentStatusPending, o.PaymentStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestOrderService_Cancel(t *testing.T) {
|
||||
env := setupOrderTestEnv(t)
|
||||
|
||||
req := &dto.CreateOrderRequest{
|
||||
OrderType: model.OrderTypeSingleCard,
|
||||
IotCardID: &env.card.ID,
|
||||
PackageIDs: []uint{env.pkg.ID},
|
||||
}
|
||||
created, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("取消订单成功", func(t *testing.T) {
|
||||
err := env.svc.Cancel(env.ctx, created.ID, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
order, err := env.svc.Get(env.ctx, created.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, model.PaymentStatusCancelled, order.PaymentStatus)
|
||||
})
|
||||
|
||||
t.Run("订单不存在", func(t *testing.T) {
|
||||
err := env.svc.Cancel(env.ctx, 99999, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeNotFound, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("无权操作", func(t *testing.T) {
|
||||
newReq := &dto.CreateOrderRequest{
|
||||
OrderType: model.OrderTypeSingleCard,
|
||||
IotCardID: &env.card.ID,
|
||||
PackageIDs: []uint{env.pkg.ID},
|
||||
}
|
||||
newOrder, err := env.svc.Create(env.ctx, newReq, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = env.svc.Cancel(env.ctx, newOrder.ID, model.BuyerTypeAgent, 99999)
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeForbidden, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOrderService_WalletPay(t *testing.T) {
|
||||
env := setupOrderTestEnv(t)
|
||||
|
||||
t.Run("钱包支付成功", func(t *testing.T) {
|
||||
req := &dto.CreateOrderRequest{
|
||||
OrderType: model.OrderTypeSingleCard,
|
||||
IotCardID: &env.card.ID,
|
||||
PackageIDs: []uint{env.pkg.ID},
|
||||
}
|
||||
created, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = env.svc.WalletPay(env.ctx, created.ID, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
order, err := env.svc.Get(env.ctx, created.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, model.PaymentStatusPaid, order.PaymentStatus)
|
||||
assert.Equal(t, model.PaymentMethodWallet, order.PaymentMethod)
|
||||
assert.NotNil(t, order.PaidAt)
|
||||
})
|
||||
|
||||
t.Run("订单不存在", func(t *testing.T) {
|
||||
err := env.svc.WalletPay(env.ctx, 99999, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeNotFound, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("无权操作", func(t *testing.T) {
|
||||
req := &dto.CreateOrderRequest{
|
||||
OrderType: model.OrderTypeSingleCard,
|
||||
IotCardID: &env.card.ID,
|
||||
PackageIDs: []uint{env.pkg.ID},
|
||||
}
|
||||
created, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = env.svc.WalletPay(env.ctx, created.ID, model.BuyerTypeAgent, 99999)
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeForbidden, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("重复支付", func(t *testing.T) {
|
||||
req := &dto.CreateOrderRequest{
|
||||
OrderType: model.OrderTypeSingleCard,
|
||||
IotCardID: &env.card.ID,
|
||||
PackageIDs: []uint{env.pkg.ID},
|
||||
}
|
||||
created, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = env.svc.WalletPay(env.ctx, created.ID, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = env.svc.WalletPay(env.ctx, created.ID, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidStatus, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOrderService_HandlePaymentCallback(t *testing.T) {
|
||||
env := setupOrderTestEnv(t)
|
||||
|
||||
t.Run("支付回调成功", func(t *testing.T) {
|
||||
req := &dto.CreateOrderRequest{
|
||||
OrderType: model.OrderTypeSingleCard,
|
||||
IotCardID: &env.card.ID,
|
||||
PackageIDs: []uint{env.pkg.ID},
|
||||
}
|
||||
created, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = env.svc.HandlePaymentCallback(env.ctx, created.OrderNo, model.PaymentMethodWechat)
|
||||
require.NoError(t, err)
|
||||
|
||||
order, err := env.svc.Get(env.ctx, created.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, model.PaymentStatusPaid, order.PaymentStatus)
|
||||
assert.Equal(t, model.PaymentMethodWechat, order.PaymentMethod)
|
||||
})
|
||||
|
||||
t.Run("幂等处理-已支付订单", func(t *testing.T) {
|
||||
req := &dto.CreateOrderRequest{
|
||||
OrderType: model.OrderTypeSingleCard,
|
||||
IotCardID: &env.card.ID,
|
||||
PackageIDs: []uint{env.pkg.ID},
|
||||
}
|
||||
created, err := env.svc.Create(env.ctx, req, model.BuyerTypeAgent, env.shop.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = env.svc.HandlePaymentCallback(env.ctx, created.OrderNo, model.PaymentMethodAlipay)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = env.svc.HandlePaymentCallback(env.ctx, created.OrderNo, model.PaymentMethodAlipay)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("订单不存在", func(t *testing.T) {
|
||||
err := env.svc.HandlePaymentCallback(env.ctx, "NOT_EXISTS_ORDER_NO", model.PaymentMethodWechat)
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeNotFound, appErr.Code)
|
||||
})
|
||||
}
|
||||
159
internal/service/purchase_validation/service.go
Normal file
159
internal/service/purchase_validation/service.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package purchase_validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
packageStore *postgres.PackageStore
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
}
|
||||
|
||||
func New(
|
||||
db *gorm.DB,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
packageStore *postgres.PackageStore,
|
||||
seriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
packageStore: packageStore,
|
||||
seriesAllocationStore: seriesAllocationStore,
|
||||
}
|
||||
}
|
||||
|
||||
type PurchaseValidationResult struct {
|
||||
Card *model.IotCard
|
||||
Device *model.Device
|
||||
Packages []*model.Package
|
||||
TotalPrice int64
|
||||
Allocation *model.ShopSeriesAllocation
|
||||
}
|
||||
|
||||
func (s *Service) ValidateCardPurchase(ctx context.Context, cardID uint, packageIDs []uint) (*PurchaseValidationResult, error) {
|
||||
card, err := s.iotCardStore.GetByID(ctx, cardID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeIotCardNotFound, "IoT卡不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if card.SeriesAllocationID == nil || *card.SeriesAllocationID == 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "该卡未关联套餐系列,无法购买套餐")
|
||||
}
|
||||
|
||||
allocation, err := s.seriesAllocationStore.GetByID(ctx, *card.SeriesAllocationID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "套餐系列分配不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if allocation.Status != constants.StatusEnabled {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "套餐系列分配已禁用")
|
||||
}
|
||||
|
||||
packages, totalPrice, err := s.validatePackages(ctx, packageIDs, allocation.SeriesID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PurchaseValidationResult{
|
||||
Card: card,
|
||||
Packages: packages,
|
||||
TotalPrice: totalPrice,
|
||||
Allocation: allocation,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) ValidateDevicePurchase(ctx context.Context, deviceID uint, packageIDs []uint) (*PurchaseValidationResult, error) {
|
||||
device, err := s.deviceStore.GetByID(ctx, deviceID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "设备不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if device.SeriesAllocationID == nil || *device.SeriesAllocationID == 0 {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "该设备未关联套餐系列,无法购买套餐")
|
||||
}
|
||||
|
||||
allocation, err := s.seriesAllocationStore.GetByID(ctx, *device.SeriesAllocationID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "套餐系列分配不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if allocation.Status != constants.StatusEnabled {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "套餐系列分配已禁用")
|
||||
}
|
||||
|
||||
packages, totalPrice, err := s.validatePackages(ctx, packageIDs, allocation.SeriesID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PurchaseValidationResult{
|
||||
Device: device,
|
||||
Packages: packages,
|
||||
TotalPrice: totalPrice,
|
||||
Allocation: allocation,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) validatePackages(ctx context.Context, packageIDs []uint, seriesID uint) ([]*model.Package, int64, error) {
|
||||
if len(packageIDs) == 0 {
|
||||
return nil, 0, errors.New(errors.CodeInvalidParam, "请选择至少一个套餐")
|
||||
}
|
||||
|
||||
var packages []*model.Package
|
||||
var totalPrice int64
|
||||
|
||||
for _, pkgID := range packageIDs {
|
||||
pkg, err := s.packageStore.GetByID(ctx, pkgID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, 0, errors.New(errors.CodeInvalidParam, "套餐不存在")
|
||||
}
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if pkg.SeriesID != seriesID {
|
||||
return nil, 0, errors.New(errors.CodeInvalidParam, "套餐不在可购买范围内")
|
||||
}
|
||||
|
||||
if pkg.Status != constants.StatusEnabled {
|
||||
return nil, 0, errors.New(errors.CodeInvalidParam, "套餐已禁用")
|
||||
}
|
||||
|
||||
if pkg.ShelfStatus != constants.ShelfStatusOn {
|
||||
return nil, 0, errors.New(errors.CodeInvalidParam, "套餐已下架")
|
||||
}
|
||||
|
||||
packages = append(packages, pkg)
|
||||
totalPrice += pkg.SuggestedRetailPrice
|
||||
}
|
||||
|
||||
return packages, totalPrice, nil
|
||||
}
|
||||
|
||||
func (s *Service) GetPurchasePrice(ctx context.Context, pkg *model.Package, buyerType string) int64 {
|
||||
return pkg.SuggestedRetailPrice
|
||||
}
|
||||
179
internal/service/purchase_validation/service_test.go
Normal file
179
internal/service/purchase_validation/service_test.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package purchase_validation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func setupTestData(t *testing.T) (context.Context, *Service, *model.IotCard, *model.Device, *model.Package, *model.ShopSeriesAllocation) {
|
||||
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)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
carrier := &model.Carrier{
|
||||
CarrierCode: "TEST_CARRIER_PV",
|
||||
CarrierName: "测试运营商",
|
||||
CarrierType: constants.CarrierTypeCMCC,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, carrierStore.Create(ctx, carrier))
|
||||
|
||||
shop := &model.Shop{
|
||||
ShopName: "测试店铺PV",
|
||||
ShopCode: "TEST_SHOP_PV",
|
||||
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_PV",
|
||||
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,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, seriesAllocationStore.Create(ctx, allocation))
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: "TEST_PKG_PV",
|
||||
PackageName: "测试套餐",
|
||||
SeriesID: series.ID,
|
||||
SuggestedRetailPrice: 9900,
|
||||
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: "89860000000000000001",
|
||||
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))
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: "DEV_TEST_PV_001",
|
||||
ShopID: shopIDPtr,
|
||||
SeriesAllocationID: &allocation.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
}
|
||||
require.NoError(t, deviceStore.Create(ctx, device))
|
||||
|
||||
svc := New(tx, iotCardStore, deviceStore, packageStore, seriesAllocationStore)
|
||||
|
||||
return ctx, svc, card, device, pkg, allocation
|
||||
}
|
||||
|
||||
func TestPurchaseValidationService_ValidateCardPurchase(t *testing.T) {
|
||||
ctx, svc, card, _, pkg, _ := setupTestData(t)
|
||||
|
||||
t.Run("验证成功", func(t *testing.T) {
|
||||
result, err := svc.ValidateCardPurchase(ctx, card.ID, []uint{pkg.ID})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result.Card)
|
||||
assert.Equal(t, card.ID, result.Card.ID)
|
||||
assert.Len(t, result.Packages, 1)
|
||||
assert.Equal(t, pkg.SuggestedRetailPrice, result.TotalPrice)
|
||||
})
|
||||
|
||||
t.Run("卡不存在", func(t *testing.T) {
|
||||
_, err := svc.ValidateCardPurchase(ctx, 99999, []uint{pkg.ID})
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeIotCardNotFound, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("套餐列表为空", func(t *testing.T) {
|
||||
_, err := svc.ValidateCardPurchase(ctx, card.ID, []uint{})
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("套餐不存在", func(t *testing.T) {
|
||||
_, err := svc.ValidateCardPurchase(ctx, card.ID, []uint{99999})
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPurchaseValidationService_ValidateDevicePurchase(t *testing.T) {
|
||||
ctx, svc, _, device, pkg, _ := setupTestData(t)
|
||||
|
||||
t.Run("验证成功", func(t *testing.T) {
|
||||
result, err := svc.ValidateDevicePurchase(ctx, device.ID, []uint{pkg.ID})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result.Device)
|
||||
assert.Equal(t, device.ID, result.Device.ID)
|
||||
assert.Len(t, result.Packages, 1)
|
||||
assert.Equal(t, pkg.SuggestedRetailPrice, result.TotalPrice)
|
||||
})
|
||||
|
||||
t.Run("设备不存在", func(t *testing.T) {
|
||||
_, err := svc.ValidateDevicePurchase(ctx, 99999, []uint{pkg.ID})
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeNotFound, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("套餐列表为空", func(t *testing.T) {
|
||||
_, err := svc.ValidateDevicePurchase(ctx, device.ID, []uint{})
|
||||
require.Error(t, err)
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParam, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPurchaseValidationService_GetPurchasePrice(t *testing.T) {
|
||||
ctx, svc, _, _, pkg, _ := setupTestData(t)
|
||||
|
||||
t.Run("获取个人客户价格", func(t *testing.T) {
|
||||
price := svc.GetPurchasePrice(ctx, pkg, model.BuyerTypePersonal)
|
||||
assert.Equal(t, pkg.SuggestedRetailPrice, price)
|
||||
})
|
||||
|
||||
t.Run("获取代理商价格", func(t *testing.T) {
|
||||
price := svc.GetPurchasePrice(ctx, pkg, model.BuyerTypeAgent)
|
||||
assert.Equal(t, pkg.SuggestedRetailPrice, price)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user