fix mifapay zero-decimal currency amount
This commit is contained in:
parent
1ac0cecf63
commit
f455df9192
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user