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 }