hyapp-server/server/admin/internal/modules/payment/legacy_bill_source_test.go
2026-07-03 14:54:21 +08:00

191 lines
6.2 KiB
Go

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)
}
if dto.CreatedAtMS != createTime.UnixMilli() || dto.PaidSyncedAtMS != dto.CreatedAtMS {
t.Fatalf("unexpected timestamps: %+v", dto)
}
}
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"])
}
}