fix: adjust mifapay inr amount formatting
This commit is contained in:
parent
0d4ce59c10
commit
8e5f213d9b
@ -64,7 +64,7 @@ func (c *Client) CreateOrder(ctx context.Context, req walletservice.MifaPayCreat
|
||||
}
|
||||
payer := rechargePayer(req)
|
||||
data := map[string]any{
|
||||
"amount": strconv.FormatInt(req.ProviderAmountMinor, 10),
|
||||
"amount": mifaPayProviderAmountValue(req.ProviderAmountMinor, req.CurrencyCode),
|
||||
"orderId": req.OrderID,
|
||||
"payWay": req.PayWay,
|
||||
"language": firstNonEmpty(req.Language, "en"),
|
||||
@ -364,6 +364,15 @@ func productInfo(req walletservice.MifaPayCreateOrderRequest) string {
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func mifaPayProviderAmountValue(providerAmountMinor int64, currencyCode string) string {
|
||||
if strings.EqualFold(strings.TrimSpace(currencyCode), "INR") {
|
||||
// MiFaPay India treats amount as whole INR units while wallet snapshots keep local minor units;
|
||||
// converting at the client boundary keeps V5Pay and non-INR MiFaPay contracts unchanged.
|
||||
return strconv.FormatInt(providerAmountMinor/100, 10)
|
||||
}
|
||||
return strconv.FormatInt(providerAmountMinor, 10)
|
||||
}
|
||||
|
||||
type payerIdentity struct {
|
||||
email string
|
||||
firstName string
|
||||
|
||||
@ -83,6 +83,63 @@ func TestCreateOrderMapsGatewayRejectionToUnavailable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOrderFormatsINRAmountAsWholeMajorUnits(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
if request.URL.Path != "/paygateway/mbpay/order/en_v2" {
|
||||
t.Fatalf("unexpected mifapay path: %s", request.URL.Path)
|
||||
}
|
||||
raw, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read mifapay request body: %v", err)
|
||||
}
|
||||
var outer envelope
|
||||
if err := json.Unmarshal(raw, &outer); err != nil {
|
||||
t.Fatalf("decode mifapay request envelope: %v", err)
|
||||
}
|
||||
var data struct {
|
||||
Amount string `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(outer.Data), &data); err != nil {
|
||||
t.Fatalf("decode mifapay request data: %v", err)
|
||||
}
|
||||
if data.Amount != "100" || data.Currency != "INR" {
|
||||
t.Fatalf("mifapay INR amount should be whole major units: %+v", data)
|
||||
}
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
_, _ = writer.Write([]byte(`{"code":"100002","msg":"stop after request inspection"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := testMifaPayConfig(t, server.URL)
|
||||
client, err := New(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("new mifapay client failed: %v", err)
|
||||
}
|
||||
|
||||
_, err = client.CreateOrder(context.Background(), walletservice.MifaPayCreateOrderRequest{
|
||||
OrderID: "h5r_inr_test",
|
||||
TargetUserID: 10001,
|
||||
ProductName: "coin",
|
||||
AmountMinor: 100,
|
||||
CurrencyCode: "INR",
|
||||
CountryCode: "IN",
|
||||
PayWay: "Ewallet",
|
||||
PayType: "Paytm",
|
||||
NotifyURL: "https://api.example.com/api/v1/payment/mifapay/notify",
|
||||
ReturnURL: "https://h5.example.com/recharge/index.html",
|
||||
ClientIP: "127.0.0.1",
|
||||
Language: "en",
|
||||
ProviderAmountMinor: 10_000,
|
||||
PayerName: "normal55",
|
||||
PayerAccount: "163055",
|
||||
PayerEmail: "163055@lalu.com",
|
||||
})
|
||||
if !xerr.IsCode(err, xerr.Unavailable) {
|
||||
t.Fatalf("expected inspected MiFaPay rejection to map to UNAVAILABLE, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryOrderUsesProviderOrderIDAndParsesSignedStatus(t *testing.T) {
|
||||
var signer *Client
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
@ -60,7 +60,7 @@ func (s *Service) createMifaPayRechargeOrder(ctx context.Context, command ledger
|
||||
if err := validateMifaPayMethodForOrder(method, command); err != nil {
|
||||
return ledger.ExternalRechargeOrder{}, err
|
||||
}
|
||||
providerAmountMinor, err := calculateProviderAmountMinor(amountMicroToUSDMinor(product.AmountMicro), method.USDToCurrencyRate, method.CurrencyCode)
|
||||
providerAmountMinor, err := calculateMifaPayProviderAmountMinor(amountMicroToUSDMinor(product.AmountMicro), method.USDToCurrencyRate, method.CurrencyCode)
|
||||
if err != nil {
|
||||
return ledger.ExternalRechargeOrder{}, err
|
||||
}
|
||||
|
||||
@ -43,7 +43,7 @@ func (s *Service) createTemporaryMifaPayRechargeOrder(ctx context.Context, comma
|
||||
return ledger.ExternalRechargeOrder{}, err
|
||||
}
|
||||
// 临时链接没有商品档位,三方实付金额只能由运营输入的 USD 金额和支付方式汇率即时换算;订单快照会保存换算结果。
|
||||
providerAmountMinor, err := calculateProviderAmountMinor(command.USDMinorAmount, method.USDToCurrencyRate, method.CurrencyCode)
|
||||
providerAmountMinor, err := calculateMifaPayProviderAmountMinor(command.USDMinorAmount, method.USDToCurrencyRate, method.CurrencyCode)
|
||||
if err != nil {
|
||||
return ledger.ExternalRechargeOrder{}, err
|
||||
}
|
||||
@ -205,7 +205,7 @@ func validateMifaPayMethodForTemporaryOrder(method ledger.ThirdPartyPaymentMetho
|
||||
if method.ProviderCode != ledger.PaymentProviderMifaPay || method.Status != ledger.ThirdPartyPaymentStatusActive || method.MethodID != command.PaymentMethodID {
|
||||
return xerr.New(xerr.Conflict, "mifapay method is not active")
|
||||
}
|
||||
if _, err := calculateProviderAmountMinor(command.USDMinorAmount, method.USDToCurrencyRate, method.CurrencyCode); err != nil {
|
||||
if _, err := calculateMifaPayProviderAmountMinor(command.USDMinorAmount, method.USDToCurrencyRate, method.CurrencyCode); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -116,7 +116,7 @@ func validateMifaPayMethodForOrder(method ledger.ThirdPartyPaymentMethod, comman
|
||||
if method.CountryCode != "GLOBAL" && !strings.EqualFold(method.CountryCode, command.TargetCountryCode) {
|
||||
return xerr.New(xerr.Conflict, "mifapay method country does not match")
|
||||
}
|
||||
if _, err := calculateProviderAmountMinor(100, method.USDToCurrencyRate, method.CurrencyCode); err != nil {
|
||||
if _, err := calculateMifaPayProviderAmountMinor(100, method.USDToCurrencyRate, method.CurrencyCode); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@ -154,6 +154,39 @@ func calculateProviderAmountMinor(usdMinor int64, rate string, currencyCode stri
|
||||
return int64(math.Round(float64(usdMinor) * value)), nil
|
||||
}
|
||||
|
||||
const mifaPayINRMinimumMajorAmount = int64(100)
|
||||
|
||||
func calculateMifaPayProviderAmountMinor(usdMinor int64, rate string, currencyCode string) (int64, error) {
|
||||
if !mifaPayUsesWholeMajorAmount(currencyCode) {
|
||||
return calculateProviderAmountMinor(usdMinor, rate, currencyCode)
|
||||
}
|
||||
if usdMinor <= 0 {
|
||||
return 0, xerr.New(xerr.InvalidArgument, "usd amount is invalid")
|
||||
}
|
||||
value, err := strconv.ParseFloat(strings.TrimSpace(rate), 64)
|
||||
if err != nil || value <= 0 {
|
||||
return 0, xerr.New(xerr.InvalidArgument, "payment exchange rate is invalid")
|
||||
}
|
||||
majorAmount := int64(math.Round(float64(usdMinor) / 100 * value))
|
||||
if strings.EqualFold(currencyCode, "INR") && majorAmount < mifaPayINRMinimumMajorAmount {
|
||||
// MiFaPay 印度通道要求 amount 是整数卢比,且当前网关最小金额为 100 INR;
|
||||
// 本地仍按 minor 保存 10000,回调和账单都能表达实际向三方提交的收款金额。
|
||||
majorAmount = mifaPayINRMinimumMajorAmount
|
||||
}
|
||||
return majorAmount * 100, nil
|
||||
}
|
||||
|
||||
func mifaPayProviderAmountValue(providerAmountMinor int64, currencyCode string) string {
|
||||
if mifaPayUsesWholeMajorAmount(currencyCode) {
|
||||
return strconv.FormatInt(providerAmountMinor/100, 10)
|
||||
}
|
||||
return strconv.FormatInt(providerAmountMinor, 10)
|
||||
}
|
||||
|
||||
func mifaPayUsesWholeMajorAmount(currencyCode string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(currencyCode), "INR")
|
||||
}
|
||||
|
||||
func providerCurrencyScale(currencyCode string) int {
|
||||
switch strings.ToUpper(strings.TrimSpace(currencyCode)) {
|
||||
case "BIF", "CLP", "DJF", "GNF", "IDR", "JPY", "KMF", "KRW", "MGA", "PYG", "RWF", "UGX", "VND", "VUV", "XAF", "XOF", "XPF":
|
||||
@ -182,8 +215,7 @@ func mifaPayNotifyMatchesOrder(data MifaPayNotifyData, order ledger.ExternalRech
|
||||
if data.PayType != "" && !strings.EqualFold(data.PayType, order.PayType) {
|
||||
return false
|
||||
}
|
||||
amount, err := strconv.ParseInt(strings.TrimSpace(data.Amount), 10, 64)
|
||||
return err == nil && amount == order.ProviderAmountMinor
|
||||
return strings.TrimSpace(data.Amount) == mifaPayProviderAmountValue(order.ProviderAmountMinor, order.CurrencyCode)
|
||||
}
|
||||
|
||||
func mifaPayQueryMatchesOrder(data MifaPayNotifyData, order ledger.ExternalRechargeOrder) bool {
|
||||
@ -196,8 +228,7 @@ func mifaPayQueryMatchesOrder(data MifaPayNotifyData, order ledger.ExternalRecha
|
||||
if data.PayType != "" && !strings.EqualFold(data.PayType, order.PayType) {
|
||||
return false
|
||||
}
|
||||
amount, err := strconv.ParseInt(strings.TrimSpace(data.Amount), 10, 64)
|
||||
return err == nil && amount == order.ProviderAmountMinor
|
||||
return strings.TrimSpace(data.Amount) == mifaPayProviderAmountValue(order.ProviderAmountMinor, order.CurrencyCode)
|
||||
}
|
||||
|
||||
func v5PayOrderDataMatchesOrder(data V5PayNotifyData, order ledger.ExternalRechargeOrder) bool {
|
||||
|
||||
@ -2700,6 +2700,57 @@ func TestH5MifaPayOrderRoundsZeroDecimalCurrency(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestH5MifaPayOrderUsesWholeINRProviderAmount 锁定印度 MiFaPay 的金额边界:上游 amount 传整数卢比,
|
||||
// 且 1 USD 档位按 99 INR 汇率会低于网关 100 INR 下限,本地订单必须保存实际提交的 100.00 INR。
|
||||
func TestH5MifaPayOrderUsesWholeINRProviderAmount(t *testing.T) {
|
||||
repository := mysqltest.NewRepository(t)
|
||||
svc := walletservice.New(repository)
|
||||
mifaPay := &fakeMifaPayClient{}
|
||||
svc.SetMifaPayClient(mifaPay)
|
||||
svc.SetExternalRechargeConfig(walletservice.ExternalRechargeConfig{MifaPayNotifyURL: "https://api.example.com/api/v1/payment/mifapay/notify"})
|
||||
product := repository.CreateH5RechargeProduct(ledger.RechargeAudienceNormal, 7210, 1_000_000, 80300)
|
||||
inPaytmMethodID := repository.ThirdPartyPaymentMethodID("IN", "Ewallet", "Paytm")
|
||||
repository.SetThirdPartyPaymentRate(inPaytmMethodID, "99.00000000")
|
||||
|
||||
order, err := svc.CreateH5RechargeOrder(context.Background(), ledger.CreateExternalRechargeOrderCommand{
|
||||
AppCode: appcode.Default,
|
||||
CommandID: "cmd-h5-mifapay-inr-minimum",
|
||||
TargetUserID: 42003,
|
||||
TargetRegionID: 7210,
|
||||
TargetCountryCode: "IN",
|
||||
AudienceType: ledger.RechargeAudienceNormal,
|
||||
ProductID: product.ProductID,
|
||||
ProviderCode: ledger.PaymentProviderMifaPay,
|
||||
PaymentMethodID: inPaytmMethodID,
|
||||
PayerName: "normal55",
|
||||
PayerAccount: "163055",
|
||||
PayerEmail: "normal55@lalu.com",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateH5RechargeOrder INR failed: %v", err)
|
||||
}
|
||||
if order.ProviderAmountMinor != 10_000 || mifaPay.createReq.ProviderAmountMinor != 10_000 {
|
||||
t.Fatalf("INR amount must respect MiFaPay 100 INR minimum: order=%+v req=%+v", order, mifaPay.createReq)
|
||||
}
|
||||
|
||||
mifaPay.queryResp = walletservice.MifaPayQueryOrderResponse{
|
||||
OrderID: order.OrderID,
|
||||
ProviderOrderID: order.ProviderOrderID,
|
||||
Status: "1",
|
||||
Amount: "100",
|
||||
Currency: order.CurrencyCode,
|
||||
PayType: order.PayType,
|
||||
RawJSON: `{"status":"1","amount":"100","currency":"INR"}`,
|
||||
}
|
||||
credited, err := svc.GetH5RechargeOrder(context.Background(), appcode.Default, order.OrderID, order.TargetUserID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetH5RechargeOrder INR query failed: %v", err)
|
||||
}
|
||||
if credited.Status != ledger.ExternalRechargeStatusCredited || credited.ProviderAmountMinor != 10_000 {
|
||||
t.Fatalf("INR query should match whole-major MiFaPay amount and credit: %+v", credited)
|
||||
}
|
||||
}
|
||||
|
||||
// TestH5MifaPayOrderRejectsUpstreamFailureAsUnavailable 锁定 MiFaPay 网关拒单的边界:本地订单要落失败便于排障,
|
||||
// 对外错误必须保持上游依赖失败语义,不能伪装成商品、幂等或状态冲突。
|
||||
func TestH5MifaPayOrderRejectsUpstreamFailureAsUnavailable(t *testing.T) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user