package financeorder import ( "context" "strings" "testing" "hyapp-admin-server/internal/model" "hyapp-admin-server/internal/modules/shared" walletv1 "hyapp.local/api/proto/wallet/v1" ) func TestNormalizeCreateOrderRequiresExplicitUSDTChain(t *testing.T) { _, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{ AppCode: "lalu", TargetUserID: "10001", USDAmount: 12.34, CoinAmount: 1200, ProviderCode: "usdt", ExternalOrderNo: "0xabc", }) if err == nil || !strings.Contains(err.Error(), "必须选择链") { t.Fatalf("expected explicit usdt chain error, got %v", err) } input, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{ AppCode: "lalu", TargetUserID: "10001", USDAmount: 12.34, CoinAmount: 1200, ProviderCode: "usdt", Chain: "bsc", ExternalOrderNo: "0xabc", }) if err != nil { t.Fatalf("normalize usdt order failed: %v", err) } if input.ProviderCode != "usdt" || input.Chain != "BSC" || input.USDMinorAmount != 1234 { t.Fatalf("normalized input mismatch: %+v", input) } } func TestNormalizeCreateOrderRequiresProviderCurrencyAmount(t *testing.T) { for _, providerCode := range []string{"mifapay", "v5pay"} { _, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{ AppCode: "lalu", TargetUserID: "10001", USDAmount: 12.34, CoinAmount: 1200, ProviderCode: providerCode, ExternalOrderNo: providerCode + "-order", }) if err == nil || !strings.Contains(err.Error(), "三方币种") { t.Fatalf("expected %s missing currency error, got %v", providerCode, err) } input, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{ AppCode: "lalu", TargetUserID: "10001", USDAmount: 12.34, CoinAmount: 1200, ProviderCode: providerCode, ExternalOrderNo: providerCode + "-order", ProviderCurrencyCode: " php ", ProviderAmountMinor: 123400, ProviderPayType: "GCASH", }) if err != nil { t.Fatalf("normalize %s with provider currency amount failed: %v", providerCode, err) } if input.ProviderCurrencyCode != "PHP" || input.ProviderAmountMinor != 123400 || input.ProviderPayType != "GCASH" { t.Fatalf("provider currency amount should be normalized for %s: %+v", providerCode, input) } } } func TestNormalizeReceiptVerificationCarriesProviderCurrencyAmount(t *testing.T) { input, err := normalizeReceiptVerificationRequest(verifyCoinSellerRechargeReceiptRequest{ AppCode: "lalu", USDAmount: 10, ProviderCode: "v5pay", ExternalOrderNo: "V5-001", ProviderCountryCode: " ph ", ProviderCurrencyCode: " php ", ProviderAmountMinor: 560000, ProviderPayType: "GCASH", OrderDate: "2026-07-09", }) if err != nil { t.Fatalf("normalize receipt verification failed: %v", err) } if input.USDMinorAmount != 1000 || input.ProviderCountryCode != "PH" || input.ProviderCurrencyCode != "PHP" || input.ProviderAmountMinor != 560000 || input.ProviderPayType != "GCASH" { t.Fatalf("provider verification input mismatch: %+v", input) } if input.OrderDateMS <= 0 { t.Fatalf("order date should be normalized into millis: %+v", input) } _, err = normalizeReceiptVerificationRequest(verifyCoinSellerRechargeReceiptRequest{ AppCode: "lalu", ProviderCode: "mifapay", ExternalOrderNo: "MIFA-001", }) if err == nil || !strings.Contains(err.Error(), "三方币种") { t.Fatalf("expected missing provider currency error, got %v", err) } } func TestNormalizeCreateOrderAllowsZeroCoinAmount(t *testing.T) { input, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{ AppCode: "lalu", TargetUserID: "10001", USDAmount: 12.34, CoinAmount: 0, ProviderCode: "mifapay", ExternalOrderNo: "MIFA-001", ProviderCurrencyCode: "PHP", ProviderAmountMinor: 1234, Remark: " 补单,不发金币 ", }) if err != nil { t.Fatalf("zero coin makeup order should be accepted: %v", err) } if input.CoinAmount != 0 || input.USDMinorAmount != 1234 || input.Remark != "补单,不发金币" { t.Fatalf("normalized zero coin order mismatch: %+v", input) } } func TestExecuteGrantSkipsWalletAndLegacyForZeroCoinMakeupOrder(t *testing.T) { service := NewService(nil, nil, nil) result, err := service.executeGrant(context.Background(), shared.Actor{UserID: 7}, model.CoinSellerRechargeOrder{ ID: 42, AppCode: "aslan", TargetUserID: 10001, CoinAmount: 0, }, "req-1") if err != nil { t.Fatalf("zero coin makeup order should not require wallet or legacy writer: %v", err) } if result.CommandID != "admin-coin-seller-recharge:42" || result.TransactionID != "" || result.AmountDelta != 0 { t.Fatalf("zero coin grant result mismatch: %+v", result) } } func TestExecuteGrantUsesLegacyWriterForAslan(t *testing.T) { writer := &fakeLegacyCoinSellerRechargeWriter{ appCode: "aslan", grantResult: legacyRechargeGrantResult{ TransactionID: "legacy-tx-1", BalanceAfter: 6200, }, } service := NewService(nil, nil, nil, WithLegacyCoinSellerRechargeWriters(writer)) result, err := service.executeGrant(context.Background(), shared.Actor{UserID: 7}, model.CoinSellerRechargeOrder{ ID: 99, AppCode: "aslan", TargetUserID: 10001, ExternalOrderNo: "V5-001", ProviderCode: "v5pay", USDAmount: "10.00", CoinAmount: 1200, }, "req-1") if err != nil { t.Fatalf("aslan legacy grant failed: %v", err) } if writer.grantCalls != 1 || writer.grantInput.UserID != 10001 || writer.grantInput.ExternalOrderNo != "V5-001" || writer.grantInput.OperatorUserID != 7 { t.Fatalf("legacy writer grant input mismatch: calls=%d input=%+v", writer.grantCalls, writer.grantInput) } if result.CommandID != "admin-coin-seller-recharge:99" || result.TransactionID != "legacy-tx-1" || result.AmountDelta != 1200 || result.BalanceAfter != 6200 { t.Fatalf("legacy grant result mismatch: %+v", result) } } func TestExecuteGrantRequiresLegacyWriterForYumi(t *testing.T) { service := NewService(nil, nil, nil) _, err := service.executeGrant(context.Background(), shared.Actor{UserID: 7}, model.CoinSellerRechargeOrder{ ID: 100, AppCode: "yumi", TargetUserID: 20002, ExternalOrderNo: "MIFA-001", ProviderCode: "mifapay", USDAmount: "12.34", CoinAmount: 1000, }, "req-1") if err == nil || !strings.Contains(err.Error(), "legacy coin seller recharge writer is not configured") { t.Fatalf("expected yumi legacy writer configuration error, got %v", err) } } func TestResolveCoinSellerTargetUsesLegacyWriterForAslan(t *testing.T) { writer := &fakeLegacyCoinSellerRechargeWriter{ appCode: "aslan", resolveResult: coinSellerTarget{ UserID: 10001, DisplayUserID: "agent001", CountryID: 63, }, } service := NewService(nil, nil, nil, WithLegacyCoinSellerRechargeWriters(writer)) target, err := service.resolveCoinSellerTarget(context.Background(), "req-1", "aslan", "agent001") if err != nil { t.Fatalf("resolve aslan legacy target failed: %v", err) } if writer.resolveKeyword != "agent001" || target.UserID != 10001 || target.DisplayUserID != "agent001" || target.CountryID != 63 { t.Fatalf("legacy resolve mismatch: keyword=%q target=%+v", writer.resolveKeyword, target) } } func TestVerifyReceiptMatchesOrderAmounts(t *testing.T) { order := model.CoinSellerRechargeOrder{ ExternalOrderNo: "merchant-1", USDMinorAmount: 1234, ProviderCurrencyCode: "USD", ProviderAmountMinor: 0, ProviderPayloadJSON: "", TargetDisplayUserID: "10001", TargetCountryID: 1, TargetRegionID: 2, ProviderCountryCode: "SA", ProviderPayType: "CARD", ProviderOrderID: "merchant-1", ProviderStatus: "paid", WalletTransactionID: "", WalletAssetType: "", WalletAmountDelta: 0, WalletBalanceAfter: 0, OperatorName: "ops", VerifiedByName: "", GrantedByName: "", ReceiveAddress: "", FailureReason: "", WalletCommandID: "", TargetUserID: 10001, ProviderCode: "mifapay", Chain: "", USDAmount: "12.34", CoinAmount: 1200, Status: model.CoinSellerRechargeOrderStatusCreated, VerifyStatus: model.CoinSellerRechargeVerifyStatusPending, GrantStatus: model.CoinSellerRechargeGrantStatusPending, OperatorUserID: 7, CreatedAtMS: 1, UpdatedAtMS: 1, } if err := verifyReceiptMatchesOrder(order, &walletv1.VerifyCoinSellerRechargeReceiptResponse{ ExternalOrderNo: "merchant-1", CurrencyCode: "USD", ProviderAmountMinor: 1234, }); err != nil { t.Fatalf("expected USD amount to match: %v", err) } if err := verifyReceiptMatchesOrder(order, &walletv1.VerifyCoinSellerRechargeReceiptResponse{ ExternalOrderNo: "merchant-1", CurrencyCode: "USD", ProviderAmountMinor: 1200, }); err == nil { t.Fatalf("expected USD amount mismatch") } } func TestVerifyReceiptMatchesNonUSDProviderAmount(t *testing.T) { order := model.CoinSellerRechargeOrder{ ExternalOrderNo: "v5-1", USDMinorAmount: 1000, ProviderCurrencyCode: "IDR", ProviderAmountMinor: 15000000, ProviderCode: "v5pay", TargetDisplayUserID: "10001", TargetUserID: 10001, USDAmount: "10.00", CoinAmount: 1000, Status: model.CoinSellerRechargeOrderStatusCreated, VerifyStatus: model.CoinSellerRechargeVerifyStatusPending, GrantStatus: model.CoinSellerRechargeGrantStatusPending, OperatorUserID: 7, } if err := verifyReceiptMatchesOrder(order, &walletv1.VerifyCoinSellerRechargeReceiptResponse{ ExternalOrderNo: "v5-1", CurrencyCode: "IDR", ProviderAmountMinor: 15000000, }); err != nil { t.Fatalf("expected provider amount to match: %v", err) } order.ProviderAmountMinor = 0 if err := verifyReceiptMatchesOrder(order, &walletv1.VerifyCoinSellerRechargeReceiptResponse{ ExternalOrderNo: "v5-1", CurrencyCode: "IDR", ProviderAmountMinor: 15000000, }); err != nil { t.Fatalf("non-USD provider amount should be an audit snapshot when operator did not fill local minor amount: %v", err) } } func TestVerifyReceiptMatchesInputRequiresUSDExactMatch(t *testing.T) { input := normalizedCreateOrderInput{ ExternalOrderNo: "usdt-tx", USDMinorAmount: 1234, ProviderCode: "usdt", Chain: "TRON", } if err := verifyReceiptMatchesInput(input, coinSellerRechargeReceiptVerificationDTO{ ExternalOrderNo: "usdt-tx", CurrencyCode: "USDT", ProviderAmountMinor: 1234, }); err != nil { t.Fatalf("expected usdt amount to match: %v", err) } if err := verifyReceiptMatchesInput(input, coinSellerRechargeReceiptVerificationDTO{ ExternalOrderNo: "usdt-tx", CurrencyCode: "USDT", ProviderAmountMinor: 1200, }); err == nil { t.Fatalf("expected usdt amount mismatch") } } func TestReceiptProviderCodeForUSDTChains(t *testing.T) { if got := receiptProviderCode(model.CoinSellerRechargeOrder{ProviderCode: "usdt", Chain: "TRON"}); got != "usdt_trc20" { t.Fatalf("tron provider code mismatch: %s", got) } if got := receiptProviderCode(model.CoinSellerRechargeOrder{ProviderCode: "usdt", Chain: "BSC"}); got != "usdt_bep20" { t.Fatalf("bsc provider code mismatch: %s", got) } } type fakeLegacyCoinSellerRechargeWriter struct { appCode string resolveKeyword string resolveResult coinSellerTarget resolveErr error grantCalls int grantInput legacyRechargeGrantInput grantResult legacyRechargeGrantResult grantErr error } func (f *fakeLegacyCoinSellerRechargeWriter) AppCode() string { return f.appCode } func (f *fakeLegacyCoinSellerRechargeWriter) Close() error { return nil } func (f *fakeLegacyCoinSellerRechargeWriter) ResolveCoinSeller(_ context.Context, keyword string) (coinSellerTarget, error) { f.resolveKeyword = keyword return f.resolveResult, f.resolveErr } func (f *fakeLegacyCoinSellerRechargeWriter) GrantCoinSellerRecharge(_ context.Context, input legacyRechargeGrantInput) (legacyRechargeGrantResult, error) { f.grantCalls++ f.grantInput = input return f.grantResult, f.grantErr }