355 lines
12 KiB
Go
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
|
|
}
|