修复 code review 问题:DTO 内部字段、错误消息、重复代码与测试覆盖
- 删除 ExpiryBaseOverrideSet 字段的 description 标签(json:"-" 内部字段不进文档) - ValidateExpiryBaseOverride 两处 CodeInvalidParam 补充中文错误消息 - 提取 initPackageExpiryBaseFields 消除 toResponse/toResponseWithAllocation 中的重复初始化 - 删除 order/service.go 中"行业卡永远直接激活"过时注释 - 补充 T01 系列授权 HTTP 集成测试(Create + ManagePackages 两个入口) - 补充 T03 C 端购买和平台代购(无分配)快照集成测试 - 补充 T04 from_purchase 默认值、卡未实名仍立即激活的自动购包测试 - 新增 testutil.NewRedisClient 供激活接续测试使用 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
169
internal/task/auto_purchase_terms_integration_test.go
Normal file
169
internal/task/auto_purchase_terms_integration_test.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/internal/testutil"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TestAutoPurchasePersistsTermsSnapshotsAndRealnameDecision 验证自动购包复用快照并遵守实名起算规则。
|
||||
func TestAutoPurchasePersistsTermsSnapshotsAndRealnameDecision(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
defaultBase string
|
||||
override *string
|
||||
realnameStatus int
|
||||
expectedBase string
|
||||
expectedStatus int
|
||||
expectedPending bool
|
||||
}{
|
||||
{name: "跟随默认等待实名", defaultBase: constants.PackageExpiryBaseFromActivation, realnameStatus: constants.RealNameStatusNotVerified, expectedBase: constants.PackageExpiryBaseFromActivation, expectedStatus: constants.PackageUsageStatusPending, expectedPending: true},
|
||||
{name: "覆盖购买即生效", defaultBase: constants.PackageExpiryBaseFromActivation, override: taskStringPointer(constants.PackageExpiryBaseFromPurchase), realnameStatus: constants.RealNameStatusNotVerified, expectedBase: constants.PackageExpiryBaseFromPurchase, expectedStatus: constants.PackageUsageStatusActive},
|
||||
{name: "覆盖实名但已实名", defaultBase: constants.PackageExpiryBaseFromPurchase, override: taskStringPointer(constants.PackageExpiryBaseFromActivation), realnameStatus: constants.RealNameStatusVerified, expectedBase: constants.PackageExpiryBaseFromActivation, expectedStatus: constants.PackageUsageStatusActive},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
tx := testutil.NewPostgresTransaction(t)
|
||||
card := createAutoPurchaseCard(t, tx, testCase.realnameStatus)
|
||||
pkg := createAutoPurchasePackage(t, tx, constants.PackageTypeFormal, testCase.defaultBase, 31)
|
||||
shopID := uint(time.Now().UnixNano() & 0x7fffffff)
|
||||
if testCase.override != nil {
|
||||
allocation := &model.ShopPackageAllocation{ShopID: shopID, PackageID: pkg.ID, CostPrice: 1, RetailPrice: 2, Status: constants.StatusEnabled, ShelfStatus: 1, ExpiryBaseOverride: testCase.override}
|
||||
if err := tx.Create(allocation).Error; err != nil {
|
||||
t.Fatalf("创建套餐覆盖配置失败:%v", err)
|
||||
}
|
||||
}
|
||||
handler := &AutoPurchaseHandler{db: tx, shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(tx), logger: zap.NewNop()}
|
||||
order := newAutoPurchaseOrder(card.ID, &shopID)
|
||||
if err := handler.activateMainPackage(context.Background(), tx, order, pkg, constants.AssetWalletResourceTypeIotCard, card.ID, time.Now()); err != nil {
|
||||
t.Fatalf("自动购包创建主套餐失败:%v", err)
|
||||
}
|
||||
var usage model.PackageUsage
|
||||
if err := tx.Where("order_id = ? AND package_id = ?", order.ID, pkg.ID).First(&usage).Error; err != nil {
|
||||
t.Fatalf("查询自动购包记录失败:%v", err)
|
||||
}
|
||||
if usage.ExpiryBaseSnapshot != testCase.expectedBase || usage.CalendarTypeSnapshot != constants.PackageCalendarTypeByDay || usage.DurationDaysSnapshot != 31 || usage.Status != testCase.expectedStatus || usage.PendingRealnameActivation != testCase.expectedPending {
|
||||
t.Fatalf("自动购包快照或实名决策错误:%+v", usage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAutoPurchaseDefaultFromPurchaseActivatesImmediatelyWithoutRealname 验证套餐默认为 from_purchase 时自动购包不需要实名即激活。
|
||||
func TestAutoPurchaseDefaultFromPurchaseActivatesImmediatelyWithoutRealname(t *testing.T) {
|
||||
tx := testutil.NewPostgresTransaction(t)
|
||||
// 卡未实名,但套餐默认 from_purchase,应立即激活
|
||||
card := createAutoPurchaseCard(t, tx, constants.RealNameStatusNotVerified)
|
||||
pkg := createAutoPurchasePackage(t, tx, constants.PackageTypeFormal, constants.PackageExpiryBaseFromPurchase, 14)
|
||||
handler := &AutoPurchaseHandler{db: tx, shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(tx), logger: zap.NewNop()}
|
||||
order := newAutoPurchaseOrder(card.ID, nil)
|
||||
if err := handler.activateMainPackage(context.Background(), tx, order, pkg, constants.AssetWalletResourceTypeIotCard, card.ID, time.Now()); err != nil {
|
||||
t.Fatalf("from_purchase 默认值自动购包失败:%v", err)
|
||||
}
|
||||
var usage model.PackageUsage
|
||||
if err := tx.Where("order_id = ? AND package_id = ?", order.ID, pkg.ID).First(&usage).Error; err != nil {
|
||||
t.Fatalf("查询自动购包记录失败:%v", err)
|
||||
}
|
||||
if usage.ExpiryBaseSnapshot != constants.PackageExpiryBaseFromPurchase || usage.DurationDaysSnapshot != 14 {
|
||||
t.Fatalf("from_purchase 默认值快照错误:%+v", usage)
|
||||
}
|
||||
if usage.Status != constants.PackageUsageStatusActive || usage.PendingRealnameActivation {
|
||||
t.Fatalf("from_purchase 默认值未实名仍应立即激活:status=%d pendingRealname=%v", usage.Status, usage.PendingRealnameActivation)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAutoPurchaseMainAndAddonAreIdempotentByBusinessFact 验证主套餐、加油包和重复消费依赖数据库事实去重。
|
||||
func TestAutoPurchaseMainAndAddonAreIdempotentByBusinessFact(t *testing.T) {
|
||||
tx := testutil.NewPostgresTransaction(t)
|
||||
card := createAutoPurchaseCard(t, tx, constants.RealNameStatusVerified)
|
||||
formal := createAutoPurchasePackage(t, tx, constants.PackageTypeFormal, constants.PackageExpiryBaseFromPurchase, 30)
|
||||
addon := createAutoPurchasePackage(t, tx, constants.PackageTypeAddon, constants.PackageExpiryBaseFromActivation, 7)
|
||||
handler := &AutoPurchaseHandler{db: tx, shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(tx), logger: zap.NewNop()}
|
||||
formalOrder := newAutoPurchaseOrder(card.ID, nil)
|
||||
addonOrder := newAutoPurchaseOrder(card.ID, nil)
|
||||
now := time.Now()
|
||||
if err := handler.activatePackages(context.Background(), tx, formalOrder, []*model.Package{formal}, now); err != nil {
|
||||
t.Fatalf("首次自动创建主套餐失败:%v", err)
|
||||
}
|
||||
if err := handler.activatePackages(context.Background(), tx, formalOrder, []*model.Package{formal}, now); err != nil {
|
||||
t.Fatalf("重复消费应按业务事实幂等:%v", err)
|
||||
}
|
||||
if err := handler.activatePackages(context.Background(), tx, addonOrder, []*model.Package{addon}, now.Add(time.Minute)); err != nil {
|
||||
t.Fatalf("自动创建加油包失败:%v", err)
|
||||
}
|
||||
var usages []model.PackageUsage
|
||||
if err := tx.Where("order_id IN ?", []uint{formalOrder.ID, addonOrder.ID}).Order("order_id").Find(&usages).Error; err != nil {
|
||||
t.Fatalf("查询自动购包记录失败:%v", err)
|
||||
}
|
||||
if len(usages) != 2 {
|
||||
t.Fatalf("重复消费只应形成一组记录,实际 %d", len(usages))
|
||||
}
|
||||
for _, usage := range usages {
|
||||
if usage.ExpiryBaseSnapshot == "" || usage.CalendarTypeSnapshot == "" {
|
||||
t.Fatalf("自动购包不得产生空快照:%+v", usage)
|
||||
}
|
||||
}
|
||||
if usages[1].MasterUsageID == nil {
|
||||
t.Fatalf("加油包必须关联主套餐:%+v", usages[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestAutoPurchaseTransactionRollbackOnInvalidAddonTerms 验证同一事务中后续套餐失败时不留下部分使用记录。
|
||||
func TestAutoPurchaseTransactionRollbackOnInvalidAddonTerms(t *testing.T) {
|
||||
tx := testutil.NewPostgresTransaction(t)
|
||||
card := createAutoPurchaseCard(t, tx, constants.RealNameStatusVerified)
|
||||
formal := createAutoPurchasePackage(t, tx, constants.PackageTypeFormal, constants.PackageExpiryBaseFromPurchase, 30)
|
||||
invalidAddon := createAutoPurchasePackage(t, tx, constants.PackageTypeAddon, "invalid", 7)
|
||||
handler := &AutoPurchaseHandler{db: tx, shopPackageAllocationStore: postgres.NewShopPackageAllocationStore(tx), logger: zap.NewNop()}
|
||||
order := newAutoPurchaseOrder(card.ID, nil)
|
||||
err := tx.Transaction(func(inner *gorm.DB) error {
|
||||
return handler.activatePackages(context.Background(), inner, order, []*model.Package{formal, invalidAddon}, time.Now())
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("非法加油包计时条款必须使业务事务失败")
|
||||
}
|
||||
var count int64
|
||||
if err := tx.Model(&model.PackageUsage{}).Where("order_id = ?", order.ID).Count(&count).Error; err != nil || count != 0 {
|
||||
t.Fatalf("事务失败不应留下部分主套餐:count=%d err=%v", count, err)
|
||||
}
|
||||
}
|
||||
|
||||
func createAutoPurchaseCard(t *testing.T, tx *gorm.DB, realnameStatus int) *model.IotCard {
|
||||
t.Helper()
|
||||
suffix := strconv.FormatInt(time.Now().UnixNano()%100000000000000000, 10)
|
||||
iccid := "88" + suffix
|
||||
if len(iccid) < 19 {
|
||||
iccid += "0000000000000000000"[:19-len(iccid)]
|
||||
}
|
||||
iccid = iccid[:19]
|
||||
card := &model.IotCard{ICCID: iccid, ICCID19: iccid, CarrierID: 1, RealNameStatus: realnameStatus, AssetStatus: constants.AssetStatusInStock, Generation: 1}
|
||||
if err := tx.Create(card).Error; err != nil {
|
||||
t.Fatalf("创建自动购包测试卡失败:%v", err)
|
||||
}
|
||||
return card
|
||||
}
|
||||
|
||||
func createAutoPurchasePackage(t *testing.T, tx *gorm.DB, packageType, expiryBase string, durationDays int) *model.Package {
|
||||
t.Helper()
|
||||
suffix := strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
pkg := &model.Package{PackageCode: "UR55-AUTO-" + suffix, PackageName: "UR55自动购包测试套餐", PackageType: packageType, DurationMonths: 1, DurationDays: durationDays, CalendarType: constants.PackageCalendarTypeByDay, ExpiryBase: expiryBase, Status: constants.StatusEnabled, ShelfStatus: 1, DataResetCycle: "monthly"}
|
||||
if err := tx.Create(pkg).Error; err != nil {
|
||||
t.Fatalf("创建自动购包测试套餐失败:%v", err)
|
||||
}
|
||||
return pkg
|
||||
}
|
||||
|
||||
func newAutoPurchaseOrder(cardID uint, sellerShopID *uint) *model.Order {
|
||||
orderID := uint(time.Now().UnixNano() & 0x7fffffff)
|
||||
return &model.Order{Model: gorm.Model{ID: orderID}, OrderNo: "UR55-AUTO-" + strconv.FormatUint(uint64(orderID), 10), OrderType: model.OrderTypeSingleCard, IotCardID: &cardID, SellerShopID: sellerShopID, TotalAmount: 100, Generation: 1}
|
||||
}
|
||||
|
||||
func taskStringPointer(value string) *string { return &value }
|
||||
Reference in New Issue
Block a user