fix mifapay zero-decimal currency amount

This commit is contained in:
local 2026-06-23 23:08:18 +08:00
parent 1ac0cecf63
commit f455df9192
4 changed files with 64 additions and 5 deletions

View File

@ -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)
providerAmountMinor, err := calculateProviderAmountMinor(amountMicroToUSDMinor(product.AmountMicro), method.USDToCurrencyRate, method.CurrencyCode)
if err != nil {
return ledger.ExternalRechargeOrder{}, err
}
@ -111,7 +111,7 @@ func (s *Service) createV5PayRechargeOrder(ctx context.Context, command ledger.C
if err := validateV5PayMethodForOrder(method, command); err != nil {
return ledger.ExternalRechargeOrder{}, err
}
providerAmountMinor, err := calculateProviderAmountMinor(amountMicroToUSDMinor(product.AmountMicro), method.USDToCurrencyRate)
providerAmountMinor, err := calculateProviderAmountMinor(amountMicroToUSDMinor(product.AmountMicro), method.USDToCurrencyRate, method.CurrencyCode)
if err != nil {
return ledger.ExternalRechargeOrder{}, err
}

View File

@ -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); err != nil {
if _, err := calculateProviderAmountMinor(100, method.USDToCurrencyRate, method.CurrencyCode); err != nil {
return err
}
return nil
@ -132,13 +132,13 @@ func validateV5PayMethodForOrder(method ledger.ThirdPartyPaymentMethod, command
if strings.TrimSpace(method.PayType) == "" {
return xerr.New(xerr.InvalidArgument, "v5pay product type is required")
}
if _, err := calculateProviderAmountMinor(100, method.USDToCurrencyRate); err != nil {
if _, err := calculateProviderAmountMinor(100, method.USDToCurrencyRate, method.CurrencyCode); err != nil {
return err
}
return nil
}
func calculateProviderAmountMinor(usdMinor int64, rate string) (int64, error) {
func calculateProviderAmountMinor(usdMinor int64, rate string, currencyCode string) (int64, error) {
if usdMinor <= 0 {
return 0, xerr.New(xerr.InvalidArgument, "usd amount is invalid")
}
@ -146,9 +146,23 @@ func calculateProviderAmountMinor(usdMinor int64, rate string) (int64, error) {
if err != nil || value <= 0 {
return 0, xerr.New(xerr.InvalidArgument, "payment exchange rate is invalid")
}
if providerCurrencyScale(currencyCode) == 0 {
// MiFaPay still receives a fixed two-decimal integer amount, but zero-decimal currencies
// such as IDR must represent a whole major-unit amount after the provider divides by 100.
return int64(math.Round(float64(usdMinor)/100*value)) * 100, nil
}
return int64(math.Round(float64(usdMinor) * value)), nil
}
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":
return 0
default:
return 2
}
}
func amountMicroToUSDMinor(amountMicro int64) int64 {
if amountMicro <= 0 {
return 0

View File

@ -2452,6 +2452,37 @@ func TestH5MifaPayOrderRejectsPaymentMethodFromAnotherCountry(t *testing.T) {
}
}
// TestH5MifaPayOrderRoundsZeroDecimalCurrency 锁定 IDR 等零小数币种的三方下单金额:
// MiFaPay 按 fixed minor 接收 amount但会校验 IDR 主单位不能带小数,因此本地要把主单位先归整。
func TestH5MifaPayOrderRoundsZeroDecimalCurrency(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, 7201, 300_000_000, 28_200_000)
idBankMethodID := repository.ThirdPartyPaymentMethodID("ID", "BankTransfer", "BCA")
repository.SetThirdPartyPaymentRate(idBankMethodID, "17706.65883333")
order, err := svc.CreateH5RechargeOrder(context.Background(), ledger.CreateExternalRechargeOrderCommand{
AppCode: appcode.Default,
CommandID: "cmd-h5-mifapay-idr-integer",
TargetUserID: 42002,
TargetRegionID: 7201,
TargetCountryCode: "ID",
AudienceType: ledger.RechargeAudienceNormal,
ProductID: product.ProductID,
ProviderCode: ledger.PaymentProviderMifaPay,
PaymentMethodID: idBankMethodID,
})
if err != nil {
t.Fatalf("CreateH5RechargeOrder IDR failed: %v", err)
}
if order.ProviderAmountMinor != 531_199_800 || mifaPay.createReq.ProviderAmountMinor != 531_199_800 {
t.Fatalf("IDR amount must be rounded to whole major units before fixed-minor submit: order=%+v req=%+v", order, mifaPay.createReq)
}
}
// TestH5MifaPayOrderRejectsUpstreamFailureAsUnavailable 锁定 MiFaPay 网关拒单的边界:本地订单要落失败便于排障,
// 对外错误必须保持上游依赖失败语义,不能伪装成商品、幂等或状态冲突。
func TestH5MifaPayOrderRejectsUpstreamFailureAsUnavailable(t *testing.T) {

View File

@ -410,6 +410,20 @@ func (r *Repository) ThirdPartyProviderPaymentMethodID(providerCode string, coun
return methodID
}
// SetThirdPartyPaymentRate updates the production payment method row so service tests can lock provider amount semantics.
func (r *Repository) SetThirdPartyPaymentRate(methodID int64, rate string) {
r.t.Helper()
if _, err := r.schema.DB.ExecContext(context.Background(), `
UPDATE third_party_payment_methods
SET usd_to_currency_rate = ?, updated_at_ms = ?
WHERE method_id = ?`,
strings.TrimSpace(rate), time.Now().UnixMilli(), methodID,
); err != nil {
r.t.Fatalf("set third party payment rate failed: %v", err)
}
}
// SetGiftPrice overrides or inserts a gift settlement price for server-side billing tests.
func (r *Repository) SetGiftPrice(giftID string, priceVersion string, coinPrice int64, giftPointAmount int64, heatValue int64) {
r.t.Helper()