730 lines
29 KiB
Go
730 lines
29 KiB
Go
package wallet_test
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"hyapp/pkg/xerr"
|
|
"hyapp/services/wallet-service/internal/domain/ledger"
|
|
walletservice "hyapp/services/wallet-service/internal/service/wallet"
|
|
)
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptMifaPayReturnsPaidSnapshot(t *testing.T) {
|
|
mifaPay := &fakeMifaPayClient{
|
|
queryResp: walletservice.MifaPayQueryOrderResponse{
|
|
OrderID: "receipt-mifa-1",
|
|
ProviderOrderID: "mb_receipt_mifa_1",
|
|
Status: "1",
|
|
Amount: "125000",
|
|
Currency: "PHP",
|
|
PayType: "GCASH",
|
|
RawJSON: `{"status":"1"}`,
|
|
},
|
|
}
|
|
svc := walletservice.New(nil)
|
|
svc.SetMifaPayClient(mifaPay)
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderMifaPay,
|
|
ExternalOrderNo: "receipt-mifa-1",
|
|
OrderDateMS: 1767225600000,
|
|
ProviderCurrencyCode: "php",
|
|
ProviderAmountMinor: 125000,
|
|
ProviderPayType: "gcash",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt mifapay failed: %v", err)
|
|
}
|
|
if !receipt.Verified || receipt.ProviderOrderID != "mb_receipt_mifa_1" || receipt.ProviderAmountMinor != 125000 || receipt.CurrencyCode != "PHP" || receipt.RawJSON == "" {
|
|
t.Fatalf("mifapay receipt snapshot mismatch: %+v", receipt)
|
|
}
|
|
if mifaPay.queryReq.OrderID != "receipt-mifa-1" || mifaPay.queryReq.OrderCreatedAtMS != 1767225600000 {
|
|
t.Fatalf("mifapay query request mismatch: %+v", mifaPay.queryReq)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptMifaPayRejectsAmountMismatch(t *testing.T) {
|
|
mifaPay := &fakeMifaPayClient{
|
|
queryResp: walletservice.MifaPayQueryOrderResponse{
|
|
OrderID: "receipt-mifa-amount",
|
|
Status: "1",
|
|
Amount: "125000",
|
|
Currency: "PHP",
|
|
RawJSON: `{"status":"1"}`,
|
|
},
|
|
}
|
|
svc := walletservice.New(nil)
|
|
svc.SetMifaPayClient(mifaPay)
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderMifaPay,
|
|
ExternalOrderNo: "receipt-mifa-amount",
|
|
OrderDateMS: 1767225600000,
|
|
ProviderCurrencyCode: "PHP",
|
|
ProviderAmountMinor: 125001,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt mifapay amount mismatch failed: %v", err)
|
|
}
|
|
if receipt.Verified || receipt.FailureReason != "mifapay amount mismatch" {
|
|
t.Fatalf("mifapay amount mismatch should return failed snapshot, got %+v", receipt)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptMifaPayConvertsINRMajorAmount(t *testing.T) {
|
|
mifaPay := &fakeMifaPayClient{
|
|
queryResp: walletservice.MifaPayQueryOrderResponse{
|
|
OrderID: "receipt-mifa-inr",
|
|
Status: "1",
|
|
Amount: "100",
|
|
Currency: "INR",
|
|
RawJSON: `{"status":"1","amount":"100","currency":"INR"}`,
|
|
},
|
|
}
|
|
svc := walletservice.New(nil)
|
|
svc.SetMifaPayClient(mifaPay)
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderMifaPay,
|
|
ExternalOrderNo: "receipt-mifa-inr",
|
|
OrderDateMS: 1767225600000,
|
|
ProviderCurrencyCode: "INR",
|
|
ProviderAmountMinor: 10000,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt mifapay INR failed: %v", err)
|
|
}
|
|
if !receipt.Verified || receipt.ProviderAmountMinor != 10000 {
|
|
t.Fatalf("mifapay INR whole-major amount should verify as local minor amount, got %+v", receipt)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptMifaPayRejectsInvalidAmountWhenExpected(t *testing.T) {
|
|
mifaPay := &fakeMifaPayClient{
|
|
queryResp: walletservice.MifaPayQueryOrderResponse{
|
|
OrderID: "receipt-mifa-bad-amount",
|
|
Status: "1",
|
|
Amount: "bad",
|
|
Currency: "PHP",
|
|
RawJSON: `{"status":"1","amount":"bad"}`,
|
|
},
|
|
}
|
|
svc := walletservice.New(nil)
|
|
svc.SetMifaPayClient(mifaPay)
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderMifaPay,
|
|
ExternalOrderNo: "receipt-mifa-bad-amount",
|
|
OrderDateMS: 1767225600000,
|
|
ProviderCurrencyCode: "PHP",
|
|
ProviderAmountMinor: 125000,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt mifapay invalid amount failed: %v", err)
|
|
}
|
|
if receipt.Verified || receipt.FailureReason != "mifapay amount is invalid" {
|
|
t.Fatalf("mifapay invalid amount should return failed snapshot, got %+v", receipt)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptMifaPayRejectsInvalidAmountWithoutExpected(t *testing.T) {
|
|
mifaPay := &fakeMifaPayClient{
|
|
queryResp: walletservice.MifaPayQueryOrderResponse{
|
|
OrderID: "receipt-mifa-empty-amount",
|
|
Status: "1",
|
|
Amount: "",
|
|
Currency: "PHP",
|
|
RawJSON: `{"status":"1","amount":""}`,
|
|
},
|
|
}
|
|
svc := walletservice.New(nil)
|
|
svc.SetMifaPayClient(mifaPay)
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderMifaPay,
|
|
ExternalOrderNo: "receipt-mifa-empty-amount",
|
|
OrderDateMS: 1767225600000,
|
|
ProviderCurrencyCode: "PHP",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt mifapay empty amount failed: %v", err)
|
|
}
|
|
if receipt.Verified || receipt.FailureReason != "mifapay amount is invalid" {
|
|
t.Fatalf("mifapay empty amount should return failed snapshot, got %+v", receipt)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptMifaPayWithoutDateUsesRecentLookup(t *testing.T) {
|
|
mifaPay := &fakeMifaPayClient{
|
|
querySeq: []fakeMifaPayQueryResult{
|
|
{err: xerr.New(xerr.NotFound, "mifapay order not found")},
|
|
{resp: walletservice.MifaPayQueryOrderResponse{
|
|
OrderID: "receipt-mifa-no-date",
|
|
ProviderOrderID: "mb_receipt_mifa_no_date",
|
|
Status: "1",
|
|
Amount: "125000",
|
|
Currency: "PHP",
|
|
PayType: "GCASH",
|
|
RawJSON: `{"status":"1"}`,
|
|
}},
|
|
},
|
|
}
|
|
svc := walletservice.New(nil)
|
|
svc.SetMifaPayClient(mifaPay)
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderMifaPay,
|
|
ExternalOrderNo: "receipt-mifa-no-date",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt mifapay without date failed: %v", err)
|
|
}
|
|
if !receipt.Verified || receipt.ProviderOrderID != "mb_receipt_mifa_no_date" {
|
|
t.Fatalf("mifapay no-date receipt snapshot mismatch: %+v", receipt)
|
|
}
|
|
if len(mifaPay.queryReqs) != 2 {
|
|
t.Fatalf("mifapay no-date lookup should continue after not found, got %+v", mifaPay.queryReqs)
|
|
}
|
|
firstDate := time.UnixMilli(mifaPay.queryReqs[0].OrderCreatedAtMS).UTC()
|
|
secondDate := time.UnixMilli(mifaPay.queryReqs[1].OrderCreatedAtMS).UTC()
|
|
if firstDate.Hour() != 0 || secondDate.Hour() != 0 || !sameUTCDate(secondDate, firstDate.AddDate(0, 0, -1)) {
|
|
t.Fatalf("mifapay no-date lookup should walk backwards by UTC date: %+v", mifaPay.queryReqs)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptMifaPayWithoutDateReturnsUnpaidSnapshot(t *testing.T) {
|
|
mifaPay := &fakeMifaPayClient{
|
|
querySeq: []fakeMifaPayQueryResult{
|
|
{err: xerr.New(xerr.NotFound, "mifapay order not found")},
|
|
{resp: walletservice.MifaPayQueryOrderResponse{
|
|
OrderID: "receipt-mifa-unpaid",
|
|
Status: "0",
|
|
Amount: "125000",
|
|
Currency: "PHP",
|
|
RawJSON: `{"status":"0"}`,
|
|
}},
|
|
},
|
|
}
|
|
svc := walletservice.New(nil)
|
|
svc.SetMifaPayClient(mifaPay)
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderMifaPay,
|
|
ExternalOrderNo: "receipt-mifa-unpaid",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt mifapay unpaid failed: %v", err)
|
|
}
|
|
if receipt.Verified || receipt.FailureReason != "mifapay order status 0" {
|
|
t.Fatalf("mifapay unpaid should return failed snapshot, got %+v", receipt)
|
|
}
|
|
if len(mifaPay.queryReqs) != 2 {
|
|
t.Fatalf("mifapay unpaid should stop once an order is found, got %+v", mifaPay.queryReqs)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptV5PayRejectsCurrencyMismatchWithoutWriting(t *testing.T) {
|
|
v5Pay := &fakeV5PayClient{
|
|
queryResp: walletservice.V5PayQueryOrderResponse{
|
|
OrderID: "receipt-v5-1",
|
|
ProviderOrderID: "tx_receipt_v5_1",
|
|
Status: "2",
|
|
Amount: "48.00",
|
|
Currency: "TRY",
|
|
ProductType: "BANK_TRANSFER",
|
|
RawJSON: `{"status":"2"}`,
|
|
},
|
|
}
|
|
svc := walletservice.New(nil)
|
|
svc.SetV5PayClient(v5Pay)
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderV5Pay,
|
|
ExternalOrderNo: "receipt-v5-1",
|
|
ProviderCountryCode: "TR",
|
|
ProviderCurrencyCode: "PHP",
|
|
ProviderAmountMinor: 4800,
|
|
ProviderPayType: "BANK_TRANSFER",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt v5pay failed: %v", err)
|
|
}
|
|
if receipt.Verified || receipt.FailureReason != "v5pay currency mismatch" || receipt.ProviderAmountMinor != 4800 {
|
|
t.Fatalf("v5pay mismatch should return a failed snapshot, got %+v", receipt)
|
|
}
|
|
if v5Pay.queryReq.CountryCode != "TR" || v5Pay.queryReq.CurrencyCode != "PHP" {
|
|
t.Fatalf("v5pay query request should use caller supplied country/currency: %+v", v5Pay.queryReq)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptV5PayRejectsAmountMismatch(t *testing.T) {
|
|
v5Pay := &fakeV5PayClient{
|
|
queryResp: walletservice.V5PayQueryOrderResponse{
|
|
OrderID: "receipt-v5-amount",
|
|
ProviderOrderID: "tx_receipt_v5_amount",
|
|
Status: "2",
|
|
Amount: "48.00",
|
|
Currency: "TRY",
|
|
ProductType: "BANK_TRANSFER",
|
|
RawJSON: `{"status":"2"}`,
|
|
},
|
|
}
|
|
svc := walletservice.New(nil)
|
|
svc.SetV5PayClient(v5Pay)
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderV5Pay,
|
|
ExternalOrderNo: "receipt-v5-amount",
|
|
ProviderCountryCode: "TR",
|
|
ProviderCurrencyCode: "TRY",
|
|
ProviderAmountMinor: 4700,
|
|
ProviderPayType: "BANK_TRANSFER",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt v5pay amount mismatch failed: %v", err)
|
|
}
|
|
if receipt.Verified || receipt.FailureReason != "v5pay amount mismatch" || receipt.ProviderAmountMinor != 4800 {
|
|
t.Fatalf("v5pay amount mismatch should return a failed snapshot, got %+v", receipt)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptV5PayRejectsInvalidAmountWhenExpected(t *testing.T) {
|
|
v5Pay := &fakeV5PayClient{
|
|
queryResp: walletservice.V5PayQueryOrderResponse{
|
|
OrderID: "receipt-v5-bad-amount",
|
|
ProviderOrderID: "tx_receipt_v5_bad_amount",
|
|
Status: "2",
|
|
Amount: "bad",
|
|
Currency: "TRY",
|
|
ProductType: "BANK_TRANSFER",
|
|
RawJSON: `{"status":"2","amount":"bad"}`,
|
|
},
|
|
}
|
|
svc := walletservice.New(nil)
|
|
svc.SetV5PayClient(v5Pay)
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderV5Pay,
|
|
ExternalOrderNo: "receipt-v5-bad-amount",
|
|
ProviderCountryCode: "TR",
|
|
ProviderCurrencyCode: "TRY",
|
|
ProviderAmountMinor: 4800,
|
|
ProviderPayType: "BANK_TRANSFER",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt v5pay invalid amount failed: %v", err)
|
|
}
|
|
if receipt.Verified || receipt.FailureReason != "v5pay amount is invalid" {
|
|
t.Fatalf("v5pay invalid amount should return a failed snapshot, got %+v", receipt)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptV5PayRejectsInvalidAmountWithoutExpected(t *testing.T) {
|
|
v5Pay := &fakeV5PayClient{
|
|
queryResp: walletservice.V5PayQueryOrderResponse{
|
|
OrderID: "receipt-v5-empty-amount",
|
|
ProviderOrderID: "tx_receipt_v5_empty_amount",
|
|
Status: "2",
|
|
Amount: "",
|
|
Currency: "TRY",
|
|
ProductType: "BANK_TRANSFER",
|
|
RawJSON: `{"status":"2","amount":""}`,
|
|
},
|
|
}
|
|
svc := walletservice.New(nil)
|
|
svc.SetV5PayClient(v5Pay)
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderV5Pay,
|
|
ExternalOrderNo: "receipt-v5-empty-amount",
|
|
ProviderCountryCode: "TR",
|
|
ProviderCurrencyCode: "TRY",
|
|
ProviderPayType: "BANK_TRANSFER",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt v5pay empty amount failed: %v", err)
|
|
}
|
|
if receipt.Verified || receipt.FailureReason != "v5pay amount is invalid" {
|
|
t.Fatalf("v5pay empty amount should return failed snapshot, got %+v", receipt)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptV5PayConvertsWholeMajorAmount(t *testing.T) {
|
|
v5Pay := &fakeV5PayClient{
|
|
queryResp: walletservice.V5PayQueryOrderResponse{
|
|
OrderID: "receipt-v5-whole-major",
|
|
ProviderOrderID: "tx_receipt_v5_whole_major",
|
|
Status: "2",
|
|
Amount: "100",
|
|
Currency: "TRY",
|
|
ProductType: "BANK_TRANSFER",
|
|
RawJSON: `{"status":"2","amount":"100"}`,
|
|
},
|
|
}
|
|
svc := walletservice.New(nil)
|
|
svc.SetV5PayClient(v5Pay)
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderV5Pay,
|
|
ExternalOrderNo: "receipt-v5-whole-major",
|
|
ProviderCountryCode: "TR",
|
|
ProviderCurrencyCode: "TRY",
|
|
ProviderAmountMinor: 10000,
|
|
ProviderPayType: "BANK_TRANSFER",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt v5pay whole major amount failed: %v", err)
|
|
}
|
|
if !receipt.Verified || receipt.ProviderAmountMinor != 10000 {
|
|
t.Fatalf("v5pay whole major amount should verify as local minor amount, got %+v", receipt)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptV5PayWithoutCountryCurrencyUsesEnabledCandidates(t *testing.T) {
|
|
repository := &fakeReceiptRepository{channels: []ledger.ThirdPartyPaymentChannel{{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderV5Pay,
|
|
Status: ledger.ThirdPartyPaymentStatusActive,
|
|
Methods: []ledger.ThirdPartyPaymentMethod{
|
|
{ProviderCode: ledger.PaymentProviderV5Pay, CountryCode: "TR", CurrencyCode: "TRY", Status: ledger.ThirdPartyPaymentStatusActive},
|
|
{ProviderCode: ledger.PaymentProviderV5Pay, CountryCode: "PH", CurrencyCode: "PHP", Status: ledger.ThirdPartyPaymentStatusActive},
|
|
},
|
|
}}}
|
|
v5Pay := &fakeV5PayClient{
|
|
querySeq: []fakeV5PayQueryResult{
|
|
{err: xerr.New(xerr.NotFound, "v5pay order not found")},
|
|
{resp: walletservice.V5PayQueryOrderResponse{
|
|
OrderID: "receipt-v5-no-hints",
|
|
ProviderOrderID: "tx_receipt_v5_no_hints",
|
|
Status: "2",
|
|
Amount: "48.00",
|
|
Currency: "PHP",
|
|
ProductType: "GCASH",
|
|
RawJSON: `{"status":"2"}`,
|
|
}},
|
|
},
|
|
}
|
|
svc := walletservice.New(repository)
|
|
svc.SetV5PayClient(v5Pay)
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderV5Pay,
|
|
ExternalOrderNo: "receipt-v5-no-hints",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt v5pay no hints failed: %v", err)
|
|
}
|
|
if !receipt.Verified || receipt.ProviderOrderID != "tx_receipt_v5_no_hints" {
|
|
t.Fatalf("v5pay no-hints receipt snapshot mismatch: %+v", receipt)
|
|
}
|
|
if !repository.querySeen || repository.query.ProviderCode != ledger.PaymentProviderV5Pay || repository.query.IncludeDisabledMethods {
|
|
t.Fatalf("v5pay no-hints lookup should enumerate active V5Pay methods: %+v", repository.query)
|
|
}
|
|
if len(v5Pay.queryReqs) != 2 || v5Pay.queryReqs[0].CountryCode != "TR" || v5Pay.queryReqs[0].CurrencyCode != "TRY" || v5Pay.queryReqs[1].CountryCode != "PH" || v5Pay.queryReqs[1].CurrencyCode != "PHP" {
|
|
t.Fatalf("v5pay no-hints lookup should use enabled country/currency candidates: %+v", v5Pay.queryReqs)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptV5PayWithoutCountryCurrencyNotFound(t *testing.T) {
|
|
repository := &fakeReceiptRepository{channels: []ledger.ThirdPartyPaymentChannel{{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderV5Pay,
|
|
Status: ledger.ThirdPartyPaymentStatusActive,
|
|
Methods: []ledger.ThirdPartyPaymentMethod{
|
|
{ProviderCode: ledger.PaymentProviderV5Pay, CountryCode: "TR", CurrencyCode: "TRY", Status: ledger.ThirdPartyPaymentStatusActive},
|
|
{ProviderCode: ledger.PaymentProviderV5Pay, CountryCode: "PH", CurrencyCode: "PHP", Status: ledger.ThirdPartyPaymentStatusActive},
|
|
},
|
|
}}}
|
|
v5Pay := &fakeV5PayClient{querySeq: []fakeV5PayQueryResult{
|
|
{err: xerr.New(xerr.NotFound, "v5pay order not found")},
|
|
{err: xerr.New(xerr.NotFound, "v5pay order not found")},
|
|
}}
|
|
svc := walletservice.New(repository)
|
|
svc.SetV5PayClient(v5Pay)
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderV5Pay,
|
|
ExternalOrderNo: "receipt-v5-missing",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt v5pay not found failed: %v", err)
|
|
}
|
|
if receipt.Verified || receipt.FailureReason != "v5pay order not found for enabled country/currency candidates" {
|
|
t.Fatalf("v5pay missing order should return failed snapshot, got %+v", receipt)
|
|
}
|
|
if len(v5Pay.queryReqs) != 2 {
|
|
t.Fatalf("v5pay missing order should exhaust enabled candidates, got %+v", v5Pay.queryReqs)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptUSDTBEP20UsesReadOnlyClient(t *testing.T) {
|
|
bsc := &fakeTronUSDTClient{rawJSON: `{"receipt":true}`}
|
|
svc := walletservice.New(nil)
|
|
svc.SetBSCUSDTClient(bsc)
|
|
svc.SetExternalRechargeConfig(walletservice.ExternalRechargeConfig{
|
|
USDTBEP20Enabled: true,
|
|
USDTBEP20Address: "0xPlatformBEP20ReceiveAddress",
|
|
})
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderUSDTBEP20,
|
|
ExternalOrderNo: "0xreceiptbep20",
|
|
USDMinorAmount: 150,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt usdt_bep20 failed: %v", err)
|
|
}
|
|
if !receipt.Verified || receipt.Status != "confirmed" || receipt.Chain != "bep20" || receipt.ProviderAmountMinor != 150 || receipt.ReceiveAddress != "0xPlatformBEP20ReceiveAddress" {
|
|
t.Fatalf("bep20 receipt snapshot mismatch: %+v", receipt)
|
|
}
|
|
if bsc.lastTxHash != "0xreceiptbep20" || bsc.lastToAddress != "0xPlatformBEP20ReceiveAddress" || bsc.lastAmountMinor != 150 {
|
|
t.Fatalf("bep20 client request mismatch: %+v", bsc)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptUSDTBEP20DisabledFailsClosed(t *testing.T) {
|
|
svc := walletservice.New(nil)
|
|
_, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderUSDTBEP20,
|
|
ExternalOrderNo: "0xreceiptbep20",
|
|
USDMinorAmount: 150,
|
|
})
|
|
if !xerr.IsCode(err, xerr.Unavailable) || !strings.Contains(err.Error(), "not configured") {
|
|
t.Fatalf("disabled bep20 verification should fail closed, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptUSDTTRC20HashUsesTronGrid(t *testing.T) {
|
|
tron := &fakeTronUSDTClient{rawJSON: `{"chain":true}`}
|
|
binance := &fakeBinanceClient{err: xerr.New(xerr.Internal, "binance should not be called")}
|
|
svc := walletservice.New(nil)
|
|
svc.SetTronUSDTClient(tron)
|
|
svc.SetBinanceClient(binance)
|
|
svc.SetExternalRechargeConfig(walletservice.ExternalRechargeConfig{
|
|
USDTTRC20Enabled: true,
|
|
USDTTRC20Address: "TPlatformReceiveAddress",
|
|
})
|
|
txHash := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "lalu",
|
|
ProviderCode: ledger.PaymentProviderUSDTTRC20,
|
|
ExternalOrderNo: txHash,
|
|
USDMinorAmount: 50000,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt usdt_trc20 hash failed: %v", err)
|
|
}
|
|
if !receipt.Verified || receipt.Status != "confirmed" || receipt.RawJSON != `{"chain":true}` {
|
|
t.Fatalf("trc20 hash receipt snapshot mismatch: %+v", receipt)
|
|
}
|
|
if tron.lastTxHash != txHash || tron.lastToAddress != "TPlatformReceiveAddress" || tron.lastAmountMinor != 50000 {
|
|
t.Fatalf("trc20 hash should use TronGrid client: %+v", tron)
|
|
}
|
|
if binance.calls != 0 {
|
|
t.Fatalf("chain tx hash must not call Binance, calls=%d", binance.calls)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptUSDTTRC20OffchainUsesBinance(t *testing.T) {
|
|
tron := &fakeTronUSDTClient{err: xerr.New(xerr.Internal, "tron should not be called")}
|
|
binance := &fakeBinanceClient{record: walletservice.BinanceDepositRecord{
|
|
TxID: "Off-chain transfer 388611987194",
|
|
Coin: ledger.PaidCurrencyUSDT,
|
|
Network: "TRX",
|
|
Status: 1,
|
|
Address: "TNwqCkF8cnM4XiwjnbbKTfdZvFDqVoa1TN",
|
|
Amount: "500.00000000",
|
|
AmountMinor: 50000,
|
|
RawJSON: `{"txId":"Off-chain transfer 388611987194","status":1}`,
|
|
}}
|
|
svc := walletservice.New(nil)
|
|
svc.SetTronUSDTClient(tron)
|
|
svc.SetBinanceClient(binance)
|
|
svc.SetExternalRechargeConfig(walletservice.ExternalRechargeConfig{
|
|
USDTTRC20Enabled: true,
|
|
USDTTRC20Address: "TGlobalFallbackReceiveAddress",
|
|
USDTTRC20Addresses: map[string]string{
|
|
"aslan": "TNwqCkF8cnM4XiwjnbbKTfdZvFDqVoa1TN",
|
|
},
|
|
})
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "aslan",
|
|
ProviderCode: ledger.PaymentProviderUSDTTRC20,
|
|
ExternalOrderNo: "388611987194",
|
|
USDMinorAmount: 50000,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("VerifyCoinSellerRechargeReceipt binance offchain failed: %v", err)
|
|
}
|
|
if !receipt.Verified || receipt.Status != "confirmed" || receipt.ProviderOrderID != "Off-chain transfer 388611987194" || receipt.ProviderAmountMinor != 50000 || receipt.ReceiveAddress != "TNwqCkF8cnM4XiwjnbbKTfdZvFDqVoa1TN" {
|
|
t.Fatalf("binance offchain receipt snapshot mismatch: %+v", receipt)
|
|
}
|
|
if binance.calls != 1 || binance.req.AppCode != "aslan" || binance.req.ExternalID != "388611987194" || binance.req.Network != "TRX" || binance.req.ToAddress != "TNwqCkF8cnM4XiwjnbbKTfdZvFDqVoa1TN" || binance.req.AmountMinor != 50000 {
|
|
t.Fatalf("binance offchain request mismatch: calls=%d req=%+v", binance.calls, binance.req)
|
|
}
|
|
if tron.lastTxHash != "" {
|
|
t.Fatalf("off-chain Binance id must not call TronGrid: %+v", tron)
|
|
}
|
|
}
|
|
|
|
func TestVerifyCoinSellerRechargeReceiptUSDTTRC20OffchainMissingBinanceAccount(t *testing.T) {
|
|
binance := &fakeBinanceClient{err: xerr.New(xerr.Unavailable, "binance account is not configured")}
|
|
svc := walletservice.New(nil)
|
|
svc.SetBinanceClient(binance)
|
|
svc.SetExternalRechargeConfig(walletservice.ExternalRechargeConfig{
|
|
USDTTRC20Enabled: true,
|
|
USDTTRC20Address: "TNwqCkF8cnM4XiwjnbbKTfdZvFDqVoa1TN",
|
|
})
|
|
receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{
|
|
AppCode: "yumi",
|
|
ProviderCode: ledger.PaymentProviderUSDTTRC20,
|
|
ExternalOrderNo: "388611987194",
|
|
USDMinorAmount: 50000,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("missing binance account should return failed snapshot, got error %v", err)
|
|
}
|
|
if receipt.Verified || !strings.Contains(receipt.FailureReason, "binance account is not configured") {
|
|
t.Fatalf("missing binance account should be explicit in snapshot, got %+v", receipt)
|
|
}
|
|
}
|
|
|
|
func TestSubmitH5RechargeTxBinanceOffchainUsesAppScopedAccount(t *testing.T) {
|
|
for index, appCode := range []string{"lalu", "yumi", "aslan"} {
|
|
t.Run(appCode, func(t *testing.T) {
|
|
order := ledger.ExternalRechargeOrder{
|
|
OrderID: "order-" + appCode,
|
|
AppCode: appCode,
|
|
TargetUserID: int64(81000 + index),
|
|
USDMinorAmount: 30000,
|
|
ProviderCode: ledger.PaymentProviderUSDTTRC20,
|
|
ReceiveAddress: "TReceiveAddress-" + appCode,
|
|
Status: ledger.ExternalRechargeStatusPending,
|
|
}
|
|
repository := &fakeH5USDTRepository{order: order}
|
|
binance := &fakeBinanceClient{record: walletservice.BinanceDepositRecord{
|
|
TxID: "Off-chain transfer 390597460031",
|
|
AmountMinor: 30000,
|
|
Address: order.ReceiveAddress,
|
|
RawJSON: `{"txId":"Off-chain transfer 390597460031","status":1}`,
|
|
}}
|
|
tron := &fakeTronUSDTClient{err: xerr.New(xerr.Internal, "tron should not be called")}
|
|
svc := walletservice.New(repository)
|
|
svc.SetBinanceClient(binance)
|
|
svc.SetTronUSDTClient(tron)
|
|
|
|
credited, err := svc.SubmitH5RechargeTx(context.Background(), ledger.SubmitExternalRechargeTxCommand{
|
|
AppCode: appCode,
|
|
OrderID: order.OrderID,
|
|
TargetUserID: order.TargetUserID,
|
|
TxHash: "390597460031",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SubmitH5RechargeTx Binance off-chain failed: %v", err)
|
|
}
|
|
if credited.Status != ledger.ExternalRechargeStatusCredited {
|
|
t.Fatalf("Binance off-chain order was not credited: %+v", credited)
|
|
}
|
|
if binance.calls != 1 || binance.req.AppCode != appCode || binance.req.ExternalID != "390597460031" || binance.req.Coin != ledger.PaidCurrencyUSDT || binance.req.Network != "TRX" || binance.req.ToAddress != order.ReceiveAddress || binance.req.AmountMinor != 30000 {
|
|
t.Fatalf("Binance app-scoped lookup mismatch: calls=%d req=%+v", binance.calls, binance.req)
|
|
}
|
|
if tron.lastTxHash != "" {
|
|
t.Fatalf("Binance off-chain transfer must not query TronGrid: %+v", tron)
|
|
}
|
|
if repository.attachCommand.TxHash != "390597460031" || repository.attachPayload != binance.record.RawJSON {
|
|
t.Fatalf("submitted Binance evidence was not attached: command=%+v payload=%s", repository.attachCommand, repository.attachPayload)
|
|
}
|
|
if repository.creditProviderOrderID != binance.record.TxID || repository.creditTxHash != "390597460031" || repository.creditPayload != binance.record.RawJSON {
|
|
t.Fatalf("Binance credit evidence mismatch: provider_order_id=%s tx_hash=%s payload=%s", repository.creditProviderOrderID, repository.creditTxHash, repository.creditPayload)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type fakeReceiptRepository struct {
|
|
walletservice.Repository
|
|
channels []ledger.ThirdPartyPaymentChannel
|
|
query ledger.ListThirdPartyPaymentChannelsQuery
|
|
queryErr error
|
|
querySeen bool
|
|
}
|
|
|
|
type fakeH5USDTRepository struct {
|
|
walletservice.Repository
|
|
order ledger.ExternalRechargeOrder
|
|
attachCommand ledger.SubmitExternalRechargeTxCommand
|
|
attachPayload string
|
|
creditProviderOrderID string
|
|
creditTxHash string
|
|
creditPayload string
|
|
}
|
|
|
|
func (r *fakeH5USDTRepository) GetExternalRechargeOrder(_ context.Context, appCode string, orderID string) (ledger.ExternalRechargeOrder, error) {
|
|
if appCode != r.order.AppCode || orderID != r.order.OrderID {
|
|
return ledger.ExternalRechargeOrder{}, xerr.New(xerr.NotFound, "external recharge order not found")
|
|
}
|
|
return r.order, nil
|
|
}
|
|
|
|
func (r *fakeH5USDTRepository) AttachExternalRechargeTx(_ context.Context, command ledger.SubmitExternalRechargeTxCommand, payloadJSON string) (ledger.ExternalRechargeOrder, error) {
|
|
// 测试仓储保留入账前的凭证快照,用于断言上游查单结果没有在 service 层丢失。
|
|
r.attachCommand = command
|
|
r.attachPayload = payloadJSON
|
|
r.order.TxHash = command.TxHash
|
|
r.order.ProviderPayloadJSON = payloadJSON
|
|
return r.order, nil
|
|
}
|
|
|
|
func (r *fakeH5USDTRepository) CreditExternalRechargeOrder(_ context.Context, appCode string, orderID string, providerOrderID string, txHash string, payloadJSON string) (ledger.ExternalRechargeOrder, error) {
|
|
if appCode != r.order.AppCode || orderID != r.order.OrderID {
|
|
return ledger.ExternalRechargeOrder{}, xerr.New(xerr.NotFound, "external recharge order not found")
|
|
}
|
|
// 只有校验通过才会进入此方法;真实 MySQL 仓储仍负责唯一凭证和原子入账约束。
|
|
r.creditProviderOrderID = providerOrderID
|
|
r.creditTxHash = txHash
|
|
r.creditPayload = payloadJSON
|
|
r.order.ProviderOrderID = providerOrderID
|
|
r.order.TxHash = txHash
|
|
r.order.ProviderPayloadJSON = payloadJSON
|
|
r.order.Status = ledger.ExternalRechargeStatusCredited
|
|
return r.order, nil
|
|
}
|
|
|
|
func (r *fakeReceiptRepository) ListThirdPartyPaymentChannels(_ context.Context, query ledger.ListThirdPartyPaymentChannelsQuery) ([]ledger.ThirdPartyPaymentChannel, error) {
|
|
r.query = query
|
|
r.querySeen = true
|
|
if r.queryErr != nil {
|
|
return nil, r.queryErr
|
|
}
|
|
return r.channels, nil
|
|
}
|
|
|
|
func (r *fakeReceiptRepository) GetExternalRechargeOrder(context.Context, string, string) (ledger.ExternalRechargeOrder, error) {
|
|
return ledger.ExternalRechargeOrder{}, xerr.New(xerr.NotFound, "external recharge order not found")
|
|
}
|
|
|
|
func sameUTCDate(left time.Time, right time.Time) bool {
|
|
left = left.UTC()
|
|
right = right.UTC()
|
|
return left.Year() == right.Year() && left.YearDay() == right.YearDay()
|
|
}
|
|
|
|
type fakeBinanceClient struct {
|
|
req walletservice.BinanceDepositLookupRequest
|
|
record walletservice.BinanceDepositRecord
|
|
err error
|
|
calls int
|
|
}
|
|
|
|
func (f *fakeBinanceClient) FindUSDTDeposit(_ context.Context, req walletservice.BinanceDepositLookupRequest) (walletservice.BinanceDepositRecord, error) {
|
|
f.calls++
|
|
f.req = req
|
|
if f.err != nil {
|
|
return walletservice.BinanceDepositRecord{}, f.err
|
|
}
|
|
return f.record, nil
|
|
}
|