amount_test.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. //go:build unit
  2. package payment
  3. import (
  4. "math"
  5. "testing"
  6. )
  7. func TestYuanToFen(t *testing.T) {
  8. tests := []struct {
  9. name string
  10. input string
  11. want int64
  12. wantErr bool
  13. }{
  14. // Normal values
  15. {name: "one yuan", input: "1.00", want: 100},
  16. {name: "ten yuan fifty fen", input: "10.50", want: 1050},
  17. {name: "one fen", input: "0.01", want: 1},
  18. {name: "large amount", input: "99999.99", want: 9999999},
  19. // Edge: zero
  20. {name: "zero no decimal", input: "0", want: 0},
  21. {name: "zero with decimal", input: "0.00", want: 0},
  22. // IEEE 754 precision edge case: 1.15 * 100 = 114.99999... in float64
  23. {name: "ieee754 precision 1.15", input: "1.15", want: 115},
  24. // More precision edge cases
  25. {name: "ieee754 precision 0.1", input: "0.1", want: 10},
  26. {name: "ieee754 precision 0.2", input: "0.2", want: 20},
  27. {name: "ieee754 precision 33.33", input: "33.33", want: 3333},
  28. // Large value
  29. {name: "hundred thousand", input: "100000.00", want: 10000000},
  30. // Integer without decimal
  31. {name: "integer 5", input: "5", want: 500},
  32. {name: "integer 100", input: "100", want: 10000},
  33. // Single decimal place
  34. {name: "single decimal 1.5", input: "1.5", want: 150},
  35. // Negative values
  36. {name: "negative one yuan", input: "-1.00", want: -100},
  37. {name: "negative with fen", input: "-10.50", want: -1050},
  38. // Invalid inputs
  39. {name: "empty string", input: "", wantErr: true},
  40. {name: "alphabetic", input: "abc", wantErr: true},
  41. {name: "double dot", input: "1.2.3", wantErr: true},
  42. {name: "spaces", input: " ", wantErr: true},
  43. {name: "special chars", input: "$10.00", wantErr: true},
  44. }
  45. for _, tt := range tests {
  46. t.Run(tt.name, func(t *testing.T) {
  47. got, err := YuanToFen(tt.input)
  48. if tt.wantErr {
  49. if err == nil {
  50. t.Errorf("YuanToFen(%q) expected error, got %d", tt.input, got)
  51. }
  52. return
  53. }
  54. if err != nil {
  55. t.Fatalf("YuanToFen(%q) unexpected error: %v", tt.input, err)
  56. }
  57. if got != tt.want {
  58. t.Errorf("YuanToFen(%q) = %d, want %d", tt.input, got, tt.want)
  59. }
  60. })
  61. }
  62. }
  63. func TestFenToYuan(t *testing.T) {
  64. tests := []struct {
  65. name string
  66. fen int64
  67. want float64
  68. }{
  69. {name: "one yuan", fen: 100, want: 1.0},
  70. {name: "ten yuan fifty fen", fen: 1050, want: 10.5},
  71. {name: "one fen", fen: 1, want: 0.01},
  72. {name: "zero", fen: 0, want: 0.0},
  73. {name: "large amount", fen: 9999999, want: 99999.99},
  74. {name: "negative", fen: -100, want: -1.0},
  75. {name: "negative with fen", fen: -1050, want: -10.5},
  76. }
  77. for _, tt := range tests {
  78. t.Run(tt.name, func(t *testing.T) {
  79. got := FenToYuan(tt.fen)
  80. if math.Abs(got-tt.want) > 1e-9 {
  81. t.Errorf("FenToYuan(%d) = %f, want %f", tt.fen, got, tt.want)
  82. }
  83. })
  84. }
  85. }
  86. func TestYuanToFenRoundTrip(t *testing.T) {
  87. // Verify that converting yuan->fen->yuan preserves the value.
  88. cases := []struct {
  89. yuan string
  90. fen int64
  91. }{
  92. {"0.01", 1},
  93. {"1.00", 100},
  94. {"10.50", 1050},
  95. {"99999.99", 9999999},
  96. }
  97. for _, tc := range cases {
  98. fen, err := YuanToFen(tc.yuan)
  99. if err != nil {
  100. t.Fatalf("YuanToFen(%q) unexpected error: %v", tc.yuan, err)
  101. }
  102. if fen != tc.fen {
  103. t.Errorf("YuanToFen(%q) = %d, want %d", tc.yuan, fen, tc.fen)
  104. }
  105. yuan := FenToYuan(fen)
  106. // Parse expected yuan back for comparison
  107. expectedYuan := FenToYuan(tc.fen)
  108. if math.Abs(yuan-expectedYuan) > 1e-9 {
  109. t.Errorf("round-trip: FenToYuan(%d) = %f, want %f", fen, yuan, expectedYuan)
  110. }
  111. }
  112. }
  113. func TestPaymentCurrencyHelpers(t *testing.T) {
  114. tests := []struct {
  115. name string
  116. currency string
  117. amount string
  118. wantMinor int64
  119. wantBack float64
  120. }{
  121. {name: "hkd uses cents", currency: "hkd", amount: "12.34", wantMinor: 1234, wantBack: 12.34},
  122. {name: "jpy has no minor unit", currency: "JPY", amount: "12", wantMinor: 12, wantBack: 12},
  123. {name: "kwd uses three decimal minor units", currency: "KWD", amount: "12.345", wantMinor: 12345, wantBack: 12.345},
  124. {name: "isk uses Stripe legacy two-decimal API amount", currency: "ISK", amount: "12", wantMinor: 1200, wantBack: 12},
  125. {name: "ugx uses Stripe legacy two-decimal API amount", currency: "UGX", amount: "12.00", wantMinor: 1200, wantBack: 12},
  126. {name: "empty currency defaults to cny", currency: "", amount: "1.23", wantMinor: 123, wantBack: 1.23},
  127. }
  128. for _, tt := range tests {
  129. t.Run(tt.name, func(t *testing.T) {
  130. got, err := AmountToMinorUnit(tt.amount, tt.currency)
  131. if err != nil {
  132. t.Fatalf("AmountToMinorUnit(%q, %q) unexpected error: %v", tt.amount, tt.currency, err)
  133. }
  134. if got != tt.wantMinor {
  135. t.Fatalf("AmountToMinorUnit(%q, %q) = %d, want %d", tt.amount, tt.currency, got, tt.wantMinor)
  136. }
  137. back := MinorUnitToAmount(got, tt.currency)
  138. if math.Abs(back-tt.wantBack) > 1e-9 {
  139. t.Fatalf("MinorUnitToAmount(%d, %q) = %f, want %f", got, tt.currency, back, tt.wantBack)
  140. }
  141. })
  142. }
  143. }
  144. func TestFormatAmountForCurrency(t *testing.T) {
  145. tests := []struct {
  146. currency string
  147. amount float64
  148. want string
  149. }{
  150. {currency: "CNY", amount: 12.3, want: "12.30"},
  151. {currency: "JPY", amount: 12, want: "12"},
  152. {currency: "KWD", amount: 12.345, want: "12.345"},
  153. {currency: "ISK", amount: 12, want: "12"},
  154. }
  155. for _, tt := range tests {
  156. t.Run(tt.currency, func(t *testing.T) {
  157. if got := FormatAmountForCurrency(tt.amount, tt.currency); got != tt.want {
  158. t.Fatalf("FormatAmountForCurrency(%v, %q) = %q, want %q", tt.amount, tt.currency, got, tt.want)
  159. }
  160. })
  161. }
  162. }
  163. func TestAmountToMinorUnitRejectsUnsupportedPrecision(t *testing.T) {
  164. if _, err := AmountToMinorUnit("100.50", "JPY"); err == nil {
  165. t.Fatal("expected fractional JPY amount to fail")
  166. }
  167. if _, err := AmountToMinorUnit("100.50", "ISK"); err == nil {
  168. t.Fatal("expected fractional ISK amount to fail")
  169. }
  170. if _, err := AmountToMinorUnit("100.50", "UGX"); err == nil {
  171. t.Fatal("expected fractional UGX amount to fail")
  172. }
  173. if _, err := AmountToMinorUnit("12.345", "HKD"); err == nil {
  174. t.Fatal("expected amount with more than two decimal places to fail")
  175. }
  176. if _, err := AmountToMinorUnit("12.3456", "KWD"); err == nil {
  177. t.Fatal("expected amount with more than three decimal places to fail")
  178. }
  179. if got, err := AmountToMinorUnit("100.00", "JPY"); err != nil || got != 100 {
  180. t.Fatalf("AmountToMinorUnit integer-form JPY = (%d, %v), want (100, nil)", got, err)
  181. }
  182. }
  183. func TestThreeDecimalPaymentCurrencies(t *testing.T) {
  184. for _, currency := range []string{"BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND"} {
  185. t.Run(currency, func(t *testing.T) {
  186. got, err := AmountToMinorUnit("12.345", currency)
  187. if err != nil {
  188. t.Fatalf("AmountToMinorUnit(%q, %q) unexpected error: %v", "12.345", currency, err)
  189. }
  190. if got != 12345 {
  191. t.Fatalf("AmountToMinorUnit(%q, %q) = %d, want 12345", "12.345", currency, got)
  192. }
  193. if back := MinorUnitToAmount(got, currency); math.Abs(back-12.345) > 1e-9 {
  194. t.Fatalf("MinorUnitToAmount(%d, %q) = %f, want 12.345", got, currency, back)
  195. }
  196. })
  197. }
  198. }
  199. func TestNormalizePaymentCurrencyRejectsInvalidCodes(t *testing.T) {
  200. if _, err := NormalizePaymentCurrency("HK"); err == nil {
  201. t.Fatal("expected invalid two-letter currency to fail")
  202. }
  203. if _, err := NormalizePaymentCurrency("US1"); err == nil {
  204. t.Fatal("expected non-letter currency to fail")
  205. }
  206. }