201 lines
6.7 KiB
Go
201 lines
6.7 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)
|
||
}
|
||
// 谷歌账单初始必须是未同步(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"])
|
||
}
|
||
}
|