package exchange import ( "context" "fmt" "testing" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" customerBindingSvc "github.com/break/junhong_cmp_fiber/internal/service/customer_binding" "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" ) // TestResolveAssetByIdentifierUsesAuthoritativeSnapshot 验证任意受支持标识都生成权威换货快照。 func TestResolveAssetByIdentifierUsesAuthoritativeSnapshot(t *testing.T) { tx := testutil.NewPostgresTransaction(t) service := &Service{ iotCardStore: postgres.NewIotCardStore(tx, nil), deviceStore: postgres.NewDeviceStore(tx, nil), } ctx := context.Background() card := &model.IotCard{ICCID: "89860012345678901234", ICCID19: "8986001234567890123", MSISDN: "13800138000", VirtualNo: "UR45-CARD", AssetStatus: constants.AssetStatusInStock} if err := tx.Create(card).Error; err != nil { t.Fatalf("创建测试卡失败:%v", err) } for _, identifier := range []string{card.ICCID, card.MSISDN, card.VirtualNo} { asset, err := service.resolveAssetByIdentifier(ctx, constants.ExchangeAssetTypeIotCard, identifier) if err != nil { t.Fatalf("通过 %s 解析测试卡失败:%v", identifier, err) } if asset.Identifier != card.ICCID { t.Fatalf("卡快照应为 ICCID,输入 %s,实际 %s", identifier, asset.Identifier) } } device := &model.Device{VirtualNo: "UR45-DEVICE", IMEI: "860000000000001", SN: "UR45-SN-1", AssetStatus: constants.AssetStatusInStock} if err := tx.Create(device).Error; err != nil { t.Fatalf("创建设备失败:%v", err) } asset, err := service.resolveAssetByIdentifier(ctx, constants.ExchangeAssetTypeDevice, device.IMEI) if err != nil { t.Fatalf("解析设备失败:%v", err) } if asset.Identifier != device.VirtualNo { t.Fatalf("设备应优先保存虚拟号,实际 %s", asset.Identifier) } for _, testCase := range []struct { device *model.Device expected string }{ {device: &model.Device{IMEI: "860000000000002", SN: "UR45-SN-2"}, expected: "860000000000002"}, {device: &model.Device{SN: "UR45-SN-3"}, expected: "UR45-SN-3"}, } { if actual := newResolvedDeviceAsset(testCase.device).Identifier; actual != testCase.expected { t.Fatalf("设备快照优先级错误,期望 %s,实际 %s", testCase.expected, actual) } } } // TestExchangeWriteEntrypointsPersistAuthoritativeCardSnapshots 验证三个写入入口持久化卡的权威 ICCID。 func TestExchangeWriteEntrypointsPersistAuthoritativeCardSnapshots(t *testing.T) { testCases := []struct { name string identifier func(*model.IotCard) string }{ {name: "ICCID", identifier: func(card *model.IotCard) string { return card.ICCID }}, {name: "接入号", identifier: func(card *model.IotCard) string { return card.MSISDN }}, {name: "虚拟号", identifier: func(card *model.IotCard) string { return card.VirtualNo }}, } for index, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { tx := testutil.NewPostgresTransaction(t) service := newExchangeSnapshotTestService(tx) oldCard := createExchangeSnapshotCard(t, tx, index*10+1) newCard := createExchangeSnapshotCard(t, tx, index*10+2) direct, err := service.Create(context.Background(), &dto.CreateExchangeRequest{ OldAssetType: constants.ExchangeAssetTypeIotCard, OldIdentifier: testCase.identifier(oldCard), FlowType: constants.ExchangeFlowTypeDirect, NewIdentifier: testCase.identifier(newCard), ExchangeReason: "UR45 卡快照测试", }) if err != nil { t.Fatalf("创建直接换货失败:%v", err) } if direct.OldAssetIdentifier != oldCard.ICCID || direct.NewAssetIdentifier != newCard.ICCID { t.Fatalf("直接换货卡快照错误:old=%s new=%s", direct.OldAssetIdentifier, direct.NewAssetIdentifier) } shippingOld := createExchangeSnapshotCard(t, tx, index*10+3) shippingNew := createExchangeSnapshotCard(t, tx, index*10+4) shipping, err := service.Create(context.Background(), &dto.CreateExchangeRequest{ OldAssetType: constants.ExchangeAssetTypeIotCard, OldIdentifier: testCase.identifier(shippingOld), FlowType: constants.ExchangeFlowTypeShipping, ExchangeReason: "UR45 物流换货快照测试", }) if err != nil { t.Fatalf("创建物流换货失败:%v", err) } if shipping.OldAssetIdentifier != shippingOld.ICCID { t.Fatalf("物流换货旧卡快照应为 ICCID,实际 %s", shipping.OldAssetIdentifier) } if err = tx.Model(&model.ExchangeOrder{}).Where("id = ?", shipping.ID).Updates(map[string]any{ "status": constants.ExchangeStatusPendingShip, "recipient_name": "测试用户", "recipient_phone": "13800138000", "recipient_address": "测试地址", }).Error; err != nil { t.Fatalf("建立待发货测试前置状态失败:%v", err) } shipped, err := service.Ship(context.Background(), shipping.ID, &dto.ExchangeShipRequest{ ExpressCompany: "测试快递", ExpressNo: "UR45-EXPRESS", NewIdentifier: testCase.identifier(shippingNew), }) if err != nil { t.Fatalf("物流换货发货失败:%v", err) } if shipped.NewAssetIdentifier != shippingNew.ICCID { t.Fatalf("物流换货新卡快照应为 ICCID,实际 %s", shipped.NewAssetIdentifier) } }) } } // TestExchangeWriteEntrypointsPersistPreferredDeviceSnapshots 验证设备输入标识不影响稳定快照优先级。 func TestExchangeWriteEntrypointsPersistPreferredDeviceSnapshots(t *testing.T) { testCases := []struct { name string identifier func(*model.Device) string }{ {name: "虚拟号", identifier: func(device *model.Device) string { return device.VirtualNo }}, {name: "IMEI", identifier: func(device *model.Device) string { return device.IMEI }}, {name: "SN", identifier: func(device *model.Device) string { return device.SN }}, } for index, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { tx := testutil.NewPostgresTransaction(t) service := newExchangeSnapshotTestService(tx) oldDevice := createExchangeSnapshotDevice(t, tx, index*10+1, true, true) newDevice := createExchangeSnapshotDevice(t, tx, index*10+2, true, true) order, err := service.Create(context.Background(), &dto.CreateExchangeRequest{ OldAssetType: constants.ExchangeAssetTypeDevice, OldIdentifier: testCase.identifier(oldDevice), FlowType: constants.ExchangeFlowTypeDirect, NewIdentifier: testCase.identifier(newDevice), ExchangeReason: "UR45 设备快照测试", }) if err != nil { t.Fatalf("创建设备直接换货失败:%v", err) } if order.OldAssetIdentifier != oldDevice.VirtualNo || order.NewAssetIdentifier != newDevice.VirtualNo { t.Fatalf("设备快照应优先使用虚拟号:old=%s new=%s", order.OldAssetIdentifier, order.NewAssetIdentifier) } persisted, err := service.Get(context.Background(), order.ID) if err != nil || persisted.OldAssetIdentifier != oldDevice.VirtualNo || persisted.NewAssetIdentifier != newDevice.VirtualNo { t.Fatalf("详情未读回设备权威快照:order=%+v err=%v", persisted, err) } }) } for index, testCase := range testCases { t.Run("物流"+testCase.name, func(t *testing.T) { tx := testutil.NewPostgresTransaction(t) service := newExchangeSnapshotTestService(tx) oldDevice := createExchangeSnapshotDevice(t, tx, 80+index*10+1, true, true) newDevice := createExchangeSnapshotDevice(t, tx, 80+index*10+2, true, true) shipping, err := service.Create(context.Background(), &dto.CreateExchangeRequest{ OldAssetType: constants.ExchangeAssetTypeDevice, OldIdentifier: testCase.identifier(oldDevice), FlowType: constants.ExchangeFlowTypeShipping, ExchangeReason: "UR45 设备物流快照测试", }) if err != nil { t.Fatalf("创建设备物流换货失败:%v", err) } if shipping.OldAssetIdentifier != oldDevice.VirtualNo { t.Fatalf("物流创建设备旧快照应使用虚拟号,实际 %s", shipping.OldAssetIdentifier) } if err = tx.Model(&model.ExchangeOrder{}).Where("id = ?", shipping.ID).Update("status", constants.ExchangeStatusPendingShip).Error; err != nil { t.Fatalf("建立待发货状态失败:%v", err) } shipped, err := service.Ship(context.Background(), shipping.ID, &dto.ExchangeShipRequest{ ExpressCompany: "测试快递", ExpressNo: "UR45-DEVICE-EXPRESS", NewIdentifier: testCase.identifier(newDevice), }) if err != nil { t.Fatalf("设备物流换货发货失败:%v", err) } if shipped.OldAssetIdentifier != oldDevice.VirtualNo || shipped.NewAssetIdentifier != newDevice.VirtualNo { t.Fatalf("设备物流快照应使用虚拟号:old=%s new=%s", shipped.OldAssetIdentifier, shipped.NewAssetIdentifier) } }) } } func newExchangeSnapshotTestService(tx *gorm.DB) *Service { iotCardStore := postgres.NewIotCardStore(tx, nil) deviceStore := postgres.NewDeviceStore(tx, nil) return New( tx, postgres.NewExchangeOrderStore(tx), iotCardStore, deviceStore, postgres.NewAssetWalletStore(tx, nil), postgres.NewAssetWalletTransactionStore(tx, nil), postgres.NewPackageUsageStore(tx, nil), postgres.NewPackageUsageDailyRecordStore(tx, nil), postgres.NewResourceTagStore(tx), customerBindingSvc.New(tx, iotCardStore, deviceStore), zap.NewNop(), ) } func createExchangeSnapshotCard(t *testing.T, tx *gorm.DB, suffix int) *model.IotCard { t.Helper() iccid := fmt.Sprintf("8986000000000000%04d", suffix) card := &model.IotCard{ ICCID: iccid, ICCID19: iccid[:19], MSISDN: fmt.Sprintf("1390000%04d", suffix), VirtualNo: fmt.Sprintf("UR45-CARD-%04d", suffix), AssetStatus: constants.AssetStatusInStock, } if err := tx.Create(card).Error; err != nil { t.Fatalf("创建测试卡失败:%v", err) } return card } func createExchangeSnapshotDevice(t *testing.T, tx *gorm.DB, suffix int, withVirtualNo, withIMEI bool) *model.Device { t.Helper() device := &model.Device{SN: fmt.Sprintf("UR45-SN-%04d", suffix), AssetStatus: constants.AssetStatusInStock} if withVirtualNo { device.VirtualNo = fmt.Sprintf("UR45-DEVICE-%04d", suffix) } if withIMEI { device.IMEI = fmt.Sprintf("86000000000%04d", suffix) } if err := tx.Create(device).Error; err != nil { t.Fatalf("创建设备失败:%v", err) } return device }