package payment import ( "context" "testing" "time" "hyapp-admin-server/internal/config" "go.mongodb.org/mongo-driver/v2/bson" ) func yumiBillSourceConfig() config.FinanceBillSourceConfig { return config.FinanceBillSourceConfig{ Enabled: true, AppCode: "yumi", AppName: "Yumi", SysOrigin: "LIKEI", MongoDatabase: "test", MongoCollection: "in_app_purchase_details", RequestTimeout: time.Second, } } func TestLegacyRechargeBillDTOMapsGooglePurchase(t *testing.T) { amount, err := bson.ParseDecimal128("164.99") if err != nil { t.Fatalf("parse amount: %v", err) } amountUSD, err := bson.ParseDecimal128("2.99") if err != nil { t.Fatalf("parse amount usd: %v", err) } createTime := time.UnixMilli(1_751_000_000_000).UTC() dto := legacyRechargeBillDTO(yumiBillSourceConfig(), legacyPurchaseDocument{ ID: "175100000000012345", OrderID: "GPA.1234-5678-9012-34567", AcceptUserID: 42, CountryCode: "TR", Currency: "try", Amount: amount, AmountUSD: amountUSD, Status: "SUCCESS", Factory: legacyPurchaseFactory{Platform: "Android", FactoryCode: "GOOGLE"}, Products: []legacyPurchaseProduct{ {Name: "GOLD", Content: "560000"}, {Name: "PROPS", Content: "AVATAR_FRAME"}, }, CreateTime: createTime, }) if dto.AppCode != "yumi" || dto.TransactionID != "175100000000012345" { t.Fatalf("unexpected identity: %+v", dto) } if dto.RechargeType != "google_play_recharge" || dto.ProviderCode != "google" { t.Fatalf("unexpected channel mapping: %+v", dto) } if dto.Status != "succeeded" || dto.ExternalRef != "GPA.1234-5678-9012-34567" { t.Fatalf("unexpected status/external ref: %+v", dto) } if dto.CoinAmount != 560000 { t.Fatalf("coin amount = %d, want 560000", dto.CoinAmount) } if dto.USDMinorAmount != 299 { t.Fatalf("usd minor = %d, want 299", dto.USDMinorAmount) } if dto.UserPaidCurrencyCode != "TRY" || dto.UserPaidAmountMicro != 164_990_000 { t.Fatalf("unexpected user paid: %+v", dto) } // 谷歌账单初始必须是未同步(0),等 Orders API 实付缓存回填后才算同步完成。 if dto.CreatedAtMS != createTime.UnixMilli() || dto.PaidSyncedAtMS != 0 { t.Fatalf("unexpected timestamps: %+v", dto) } mifapayDTO := legacyRechargeBillDTO(yumiBillSourceConfig(), legacyPurchaseDocument{ ID: "175100000000054321", Factory: legacyPurchaseFactory{Platform: "H5", FactoryCode: "MIFA_PAY"}, CreateTime: createTime, }) if mifapayDTO.RechargeType != "mifapay" || mifapayDTO.PaidSyncedAtMS != mifapayDTO.CreatedAtMS { t.Fatalf("unexpected mifapay mapping: %+v", mifapayDTO) } } func TestLegacyRechargeTypeMapping(t *testing.T) { cases := map[string]string{ "GOOGLE": "google_play_recharge", "APPLE": "apple_recharge", "HUAWEI": "huawei_recharge", "TELEGRAM": "telegram_recharge", "PAYER_MAX": "web_recharge", "": "web_recharge", } for factoryCode, want := range cases { if got := legacyRechargeType(factoryCode); got != want { t.Fatalf("legacyRechargeType(%q) = %q, want %q", factoryCode, got, want) } } } func TestLegacyDecimalScaledHandlesTypes(t *testing.T) { decimal, err := bson.ParseDecimal128("6.99") if err != nil { t.Fatalf("parse decimal: %v", err) } if got := legacyDecimalToMinor(decimal); got != 699 { t.Fatalf("decimal minor = %d, want 699", got) } if got := legacyDecimalToMicro("54.99"); got != 54_990_000 { t.Fatalf("string micro = %d, want 54990000", got) } if got := legacyDecimalToMinor(int64(3)); got != 300 { t.Fatalf("int64 minor = %d, want 300", got) } if got := legacyDecimalToMinor(nil); got != 0 { t.Fatalf("nil minor = %d, want 0", got) } if got := legacyDecimalToMinor("not-a-number"); got != 0 { t.Fatalf("invalid minor = %d, want 0", got) } } type staticLegacyRegionResolver struct { codes []string handled bool } func (r staticLegacyRegionResolver) CountryCodesForRegion(context.Context, string, int64) ([]string, bool, error) { return r.codes, r.handled, nil } func TestLegacyBillFilterBuildsQuery(t *testing.T) { source := &MongoRechargeBillSource{ config: yumiBillSourceConfig(), regionResolvers: []legacyRegionCountryResolver{staticLegacyRegionResolver{codes: []string{"tr", "SA"}, handled: true}}, } filter, regionCodes, matchable, err := source.billFilter(context.Background(), legacyRechargeBillQuery{ Keyword: "GPA.1", RegionID: 9_000_000_000_000_000_001, StartAtMS: 1_000, EndAtMS: 2_000, }) if err != nil || !matchable { t.Fatalf("billFilter err=%v matchable=%v", err, matchable) } if filter["sysOrigin"] != "LIKEI" || filter["status"] != "SUCCESS" { t.Fatalf("unexpected base filter: %v", filter) } timeRange, ok := filter["createTime"].(bson.M) if !ok || !timeRange["$gte"].(time.Time).Equal(time.UnixMilli(1_000)) || !timeRange["$lt"].(time.Time).Equal(time.UnixMilli(2_000)) { t.Fatalf("unexpected time range: %v", filter["createTime"]) } // likei 账单不带国家码;区域条件必须以国家码清单返回,由聚合阶段按付款用户资料匹配。 if len(regionCodes) != 2 || regionCodes[0] != "TR" || regionCodes[1] != "SA" { t.Fatalf("unexpected region codes: %v", regionCodes) } if _, ok := filter["countryCode"]; ok { t.Fatalf("bill countryCode filter should not be used: %v", filter) } if _, ok := filter["$or"]; !ok { t.Fatalf("keyword filter missing: %v", filter) } } func TestLegacyBillFilterUnresolvedRegionMatchesNothing(t *testing.T) { source := &MongoRechargeBillSource{config: yumiBillSourceConfig()} _, _, matchable, err := source.billFilter(context.Background(), legacyRechargeBillQuery{RegionID: 123}) if err != nil { t.Fatalf("billFilter err=%v", err) } if matchable { t.Fatalf("unresolved region should not be matchable") } } func TestLegacyRegionLookupStages(t *testing.T) { if stages := legacyRegionLookupStages(nil); len(stages) != 0 { t.Fatalf("empty codes should not add stages: %v", stages) } stages := legacyRegionLookupStages([]string{"TR", "SA"}) if len(stages) != 3 { t.Fatalf("expected lookup/addFields/match stages, got %d", len(stages)) } lookup, ok := stages[0][0].Value.(bson.M) if !ok || lookup["from"] != legacyUserProfileCollection || lookup["localField"] != "acceptUserId" || lookup["foreignField"] != "_id" { t.Fatalf("unexpected lookup stage: %v", stages[0]) } match, ok := stages[2][0].Value.(bson.M) if !ok { t.Fatalf("unexpected match stage: %v", stages[2]) } condition, ok := match["legacyBillCountry"].(bson.M) if !ok { t.Fatalf("match stage missing country condition: %v", match) } codes, ok := condition["$in"].([]string) if !ok || len(codes) != 2 || codes[0] != "TR" || codes[1] != "SA" { t.Fatalf("unexpected region codes in match: %v", condition["$in"]) } }