2026-07-09 18:15:57 +08:00

355 lines
12 KiB
Go

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
}