Files
sub2api-ht/backend/internal/payment/currency.go

119 lines
3.8 KiB
Go

package payment
import (
"fmt"
"strings"
"github.com/shopspring/decimal"
)
const DefaultPaymentCurrency = "CNY"
type paymentCurrencyAmountUnit struct {
apiMinorUnit int
maxFractionDigits int
}
var (
zeroDecimalAmountUnit = paymentCurrencyAmountUnit{apiMinorUnit: 0, maxFractionDigits: 0}
twoDecimalAmountUnit = paymentCurrencyAmountUnit{apiMinorUnit: 2, maxFractionDigits: 2}
threeDecimalAmountUnit = paymentCurrencyAmountUnit{apiMinorUnit: 3, maxFractionDigits: 3}
stripeLegacyZeroAmount = paymentCurrencyAmountUnit{apiMinorUnit: 2, maxFractionDigits: 0}
)
var paymentCurrencyAmountUnits = map[string]paymentCurrencyAmountUnit{
"BIF": zeroDecimalAmountUnit,
"CLP": zeroDecimalAmountUnit,
"DJF": zeroDecimalAmountUnit,
"GNF": zeroDecimalAmountUnit,
"JPY": zeroDecimalAmountUnit,
"KMF": zeroDecimalAmountUnit,
"KRW": zeroDecimalAmountUnit,
"MGA": zeroDecimalAmountUnit,
"PYG": zeroDecimalAmountUnit,
"RWF": zeroDecimalAmountUnit,
"VND": zeroDecimalAmountUnit,
"VUV": zeroDecimalAmountUnit,
"XAF": zeroDecimalAmountUnit,
"XOF": zeroDecimalAmountUnit,
"XPF": zeroDecimalAmountUnit,
"ISK": stripeLegacyZeroAmount,
"UGX": stripeLegacyZeroAmount,
"BHD": threeDecimalAmountUnit,
"IQD": threeDecimalAmountUnit,
"JOD": threeDecimalAmountUnit,
"KWD": threeDecimalAmountUnit,
"LYD": threeDecimalAmountUnit,
"OMR": threeDecimalAmountUnit,
"TND": threeDecimalAmountUnit,
}
func NormalizePaymentCurrency(raw string) (string, error) {
currency := strings.ToUpper(strings.TrimSpace(raw))
if currency == "" {
return DefaultPaymentCurrency, nil
}
if len(currency) != 3 {
return "", fmt.Errorf("payment currency must be a 3-letter ISO currency code")
}
for _, ch := range currency {
if ch < 'A' || ch > 'Z' {
return "", fmt.Errorf("payment currency must be a 3-letter ISO currency code")
}
}
return currency, nil
}
func CurrencyMinorUnit(currency string) int {
return paymentCurrencyAmountUnitFor(currency).apiMinorUnit
}
// CurrencyMaxFractionDigits 返回支付金额允许展示和输入的小数位数。
func CurrencyMaxFractionDigits(currency string) int {
return paymentCurrencyAmountUnitFor(currency).maxFractionDigits
}
// FormatAmountForCurrency 按币种允许的小数位格式化支付金额。
func FormatAmountForCurrency(amount float64, currency string) string {
return decimal.NewFromFloat(amount).StringFixed(int32(CurrencyMaxFractionDigits(currency)))
}
func paymentCurrencyAmountUnitFor(currency string) paymentCurrencyAmountUnit {
normalized, err := NormalizePaymentCurrency(currency)
if err != nil {
return twoDecimalAmountUnit
}
if amountUnit, ok := paymentCurrencyAmountUnits[normalized]; ok {
return amountUnit
}
return twoDecimalAmountUnit
}
func AmountToMinorUnit(amountStr, currency string) (int64, error) {
d, err := decimal.NewFromString(strings.TrimSpace(amountStr))
if err != nil {
return 0, fmt.Errorf("invalid amount: %s", amountStr)
}
normalizedCurrency, err := NormalizePaymentCurrency(currency)
if err != nil {
return 0, err
}
amountUnit := paymentCurrencyAmountUnitFor(normalizedCurrency)
precisionFactor := decimal.New(1, int32(amountUnit.maxFractionDigits))
scaledForPrecision := d.Mul(precisionFactor)
if !scaledForPrecision.Equal(scaledForPrecision.Truncate(0)) {
if amountUnit.maxFractionDigits == 0 {
return 0, fmt.Errorf("payment amount for %s must be a whole number", normalizedCurrency)
}
return 0, fmt.Errorf("payment amount for %s must not have more than %d decimal places", normalizedCurrency, amountUnit.maxFractionDigits)
}
factor := decimal.New(1, int32(amountUnit.apiMinorUnit))
minorAmount := d.Mul(factor)
return minorAmount.IntPart(), nil
}
func MinorUnitToAmount(value int64, currency string) float64 {
factor := decimal.New(1, int32(CurrencyMinorUnit(currency)))
return decimal.NewFromInt(value).Div(factor).InexactFloat64()
}