currency.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. package payment
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/shopspring/decimal"
  6. )
  7. const DefaultPaymentCurrency = "CNY"
  8. type paymentCurrencyAmountUnit struct {
  9. apiMinorUnit int
  10. maxFractionDigits int
  11. }
  12. var (
  13. zeroDecimalAmountUnit = paymentCurrencyAmountUnit{apiMinorUnit: 0, maxFractionDigits: 0}
  14. twoDecimalAmountUnit = paymentCurrencyAmountUnit{apiMinorUnit: 2, maxFractionDigits: 2}
  15. threeDecimalAmountUnit = paymentCurrencyAmountUnit{apiMinorUnit: 3, maxFractionDigits: 3}
  16. stripeLegacyZeroAmount = paymentCurrencyAmountUnit{apiMinorUnit: 2, maxFractionDigits: 0}
  17. )
  18. var paymentCurrencyAmountUnits = map[string]paymentCurrencyAmountUnit{
  19. "BIF": zeroDecimalAmountUnit,
  20. "CLP": zeroDecimalAmountUnit,
  21. "DJF": zeroDecimalAmountUnit,
  22. "GNF": zeroDecimalAmountUnit,
  23. "JPY": zeroDecimalAmountUnit,
  24. "KMF": zeroDecimalAmountUnit,
  25. "KRW": zeroDecimalAmountUnit,
  26. "MGA": zeroDecimalAmountUnit,
  27. "PYG": zeroDecimalAmountUnit,
  28. "RWF": zeroDecimalAmountUnit,
  29. "VND": zeroDecimalAmountUnit,
  30. "VUV": zeroDecimalAmountUnit,
  31. "XAF": zeroDecimalAmountUnit,
  32. "XOF": zeroDecimalAmountUnit,
  33. "XPF": zeroDecimalAmountUnit,
  34. "ISK": stripeLegacyZeroAmount,
  35. "UGX": stripeLegacyZeroAmount,
  36. "BHD": threeDecimalAmountUnit,
  37. "IQD": threeDecimalAmountUnit,
  38. "JOD": threeDecimalAmountUnit,
  39. "KWD": threeDecimalAmountUnit,
  40. "LYD": threeDecimalAmountUnit,
  41. "OMR": threeDecimalAmountUnit,
  42. "TND": threeDecimalAmountUnit,
  43. }
  44. func NormalizePaymentCurrency(raw string) (string, error) {
  45. currency := strings.ToUpper(strings.TrimSpace(raw))
  46. if currency == "" {
  47. return DefaultPaymentCurrency, nil
  48. }
  49. if len(currency) != 3 {
  50. return "", fmt.Errorf("payment currency must be a 3-letter ISO currency code")
  51. }
  52. for _, ch := range currency {
  53. if ch < 'A' || ch > 'Z' {
  54. return "", fmt.Errorf("payment currency must be a 3-letter ISO currency code")
  55. }
  56. }
  57. return currency, nil
  58. }
  59. func CurrencyMinorUnit(currency string) int {
  60. return paymentCurrencyAmountUnitFor(currency).apiMinorUnit
  61. }
  62. // CurrencyMaxFractionDigits 返回支付金额允许展示和输入的小数位数。
  63. func CurrencyMaxFractionDigits(currency string) int {
  64. return paymentCurrencyAmountUnitFor(currency).maxFractionDigits
  65. }
  66. // FormatAmountForCurrency 按币种允许的小数位格式化支付金额。
  67. func FormatAmountForCurrency(amount float64, currency string) string {
  68. return decimal.NewFromFloat(amount).StringFixed(int32(CurrencyMaxFractionDigits(currency)))
  69. }
  70. func paymentCurrencyAmountUnitFor(currency string) paymentCurrencyAmountUnit {
  71. normalized, err := NormalizePaymentCurrency(currency)
  72. if err != nil {
  73. return twoDecimalAmountUnit
  74. }
  75. if amountUnit, ok := paymentCurrencyAmountUnits[normalized]; ok {
  76. return amountUnit
  77. }
  78. return twoDecimalAmountUnit
  79. }
  80. func AmountToMinorUnit(amountStr, currency string) (int64, error) {
  81. d, err := decimal.NewFromString(strings.TrimSpace(amountStr))
  82. if err != nil {
  83. return 0, fmt.Errorf("invalid amount: %s", amountStr)
  84. }
  85. normalizedCurrency, err := NormalizePaymentCurrency(currency)
  86. if err != nil {
  87. return 0, err
  88. }
  89. amountUnit := paymentCurrencyAmountUnitFor(normalizedCurrency)
  90. precisionFactor := decimal.New(1, int32(amountUnit.maxFractionDigits))
  91. scaledForPrecision := d.Mul(precisionFactor)
  92. if !scaledForPrecision.Equal(scaledForPrecision.Truncate(0)) {
  93. if amountUnit.maxFractionDigits == 0 {
  94. return 0, fmt.Errorf("payment amount for %s must be a whole number", normalizedCurrency)
  95. }
  96. return 0, fmt.Errorf("payment amount for %s must not have more than %d decimal places", normalizedCurrency, amountUnit.maxFractionDigits)
  97. }
  98. factor := decimal.New(1, int32(amountUnit.apiMinorUnit))
  99. minorAmount := d.Mul(factor)
  100. return minorAmount.IntPart(), nil
  101. }
  102. func MinorUnitToAmount(value int64, currency string) float64 {
  103. factor := decimal.New(1, int32(CurrencyMinorUnit(currency)))
  104. return decimal.NewFromInt(value).Div(factor).InexactFloat64()
  105. }