payment-kit 1.24.4 → 1.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/api/src/index.ts +3 -0
  2. package/api/src/libs/credit-utils.ts +21 -0
  3. package/api/src/libs/discount/discount.ts +13 -0
  4. package/api/src/libs/env.ts +5 -0
  5. package/api/src/libs/error.ts +14 -0
  6. package/api/src/libs/exchange-rate/coingecko-provider.ts +193 -0
  7. package/api/src/libs/exchange-rate/coinmarketcap-provider.ts +180 -0
  8. package/api/src/libs/exchange-rate/index.ts +5 -0
  9. package/api/src/libs/exchange-rate/service.ts +583 -0
  10. package/api/src/libs/exchange-rate/token-address-mapping.ts +84 -0
  11. package/api/src/libs/exchange-rate/token-addresses.json +2147 -0
  12. package/api/src/libs/exchange-rate/token-data-provider.ts +142 -0
  13. package/api/src/libs/exchange-rate/types.ts +114 -0
  14. package/api/src/libs/exchange-rate/validator.ts +319 -0
  15. package/api/src/libs/invoice-quote.ts +158 -0
  16. package/api/src/libs/invoice.ts +143 -7
  17. package/api/src/libs/math-utils.ts +46 -0
  18. package/api/src/libs/notification/template/billing-discrepancy.ts +3 -4
  19. package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +174 -79
  20. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +2 -3
  21. package/api/src/libs/notification/template/customer-credit-insufficient.ts +3 -3
  22. package/api/src/libs/notification/template/customer-credit-low-balance.ts +3 -3
  23. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +2 -3
  24. package/api/src/libs/notification/template/customer-reward-succeeded.ts +9 -4
  25. package/api/src/libs/notification/template/exchange-rate-alert.ts +202 -0
  26. package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +203 -0
  27. package/api/src/libs/notification/template/subscription-slippage-warning.ts +212 -0
  28. package/api/src/libs/notification/template/subscription-will-canceled.ts +2 -2
  29. package/api/src/libs/notification/template/subscription-will-renew.ts +22 -8
  30. package/api/src/libs/payment.ts +3 -1
  31. package/api/src/libs/price.ts +4 -1
  32. package/api/src/libs/queue/index.ts +8 -0
  33. package/api/src/libs/quote-service.ts +1132 -0
  34. package/api/src/libs/quote-validation.ts +388 -0
  35. package/api/src/libs/session.ts +686 -39
  36. package/api/src/libs/slippage.ts +135 -0
  37. package/api/src/libs/subscription.ts +185 -15
  38. package/api/src/libs/util.ts +64 -3
  39. package/api/src/locales/en.ts +50 -0
  40. package/api/src/locales/zh.ts +48 -0
  41. package/api/src/queues/auto-recharge.ts +295 -21
  42. package/api/src/queues/exchange-rate-health.ts +242 -0
  43. package/api/src/queues/invoice.ts +48 -1
  44. package/api/src/queues/notification.ts +167 -1
  45. package/api/src/queues/payment.ts +177 -7
  46. package/api/src/queues/refund.ts +41 -9
  47. package/api/src/queues/subscription.ts +436 -6
  48. package/api/src/routes/auto-recharge-configs.ts +71 -6
  49. package/api/src/routes/checkout-sessions.ts +1730 -81
  50. package/api/src/routes/connect/auto-recharge-auth.ts +2 -0
  51. package/api/src/routes/connect/change-payer.ts +2 -0
  52. package/api/src/routes/connect/change-payment.ts +61 -8
  53. package/api/src/routes/connect/change-plan.ts +161 -17
  54. package/api/src/routes/connect/collect.ts +9 -6
  55. package/api/src/routes/connect/delegation.ts +1 -0
  56. package/api/src/routes/connect/pay.ts +157 -0
  57. package/api/src/routes/connect/setup.ts +32 -10
  58. package/api/src/routes/connect/shared.ts +159 -13
  59. package/api/src/routes/connect/subscribe.ts +32 -9
  60. package/api/src/routes/credit-grants.ts +99 -0
  61. package/api/src/routes/exchange-rate-providers.ts +248 -0
  62. package/api/src/routes/exchange-rates.ts +87 -0
  63. package/api/src/routes/index.ts +4 -0
  64. package/api/src/routes/invoices.ts +280 -2
  65. package/api/src/routes/payment-links.ts +13 -0
  66. package/api/src/routes/prices.ts +84 -2
  67. package/api/src/routes/subscriptions.ts +526 -15
  68. package/api/src/store/migrations/20251220-dynamic-pricing.ts +245 -0
  69. package/api/src/store/migrations/20251223-exchange-rate-provider-type.ts +28 -0
  70. package/api/src/store/migrations/20260110-add-quote-locked-at.ts +23 -0
  71. package/api/src/store/migrations/20260112-add-checkout-session-slippage-percent.ts +22 -0
  72. package/api/src/store/migrations/20260113-add-price-quote-slippage-fields.ts +45 -0
  73. package/api/src/store/migrations/20260116-subscription-slippage.ts +21 -0
  74. package/api/src/store/migrations/20260120-auto-recharge-slippage.ts +21 -0
  75. package/api/src/store/models/auto-recharge-config.ts +12 -0
  76. package/api/src/store/models/checkout-session.ts +7 -0
  77. package/api/src/store/models/exchange-rate-provider.ts +225 -0
  78. package/api/src/store/models/index.ts +6 -0
  79. package/api/src/store/models/payment-intent.ts +6 -0
  80. package/api/src/store/models/price-quote.ts +284 -0
  81. package/api/src/store/models/price.ts +53 -5
  82. package/api/src/store/models/subscription.ts +11 -0
  83. package/api/src/store/models/types.ts +61 -1
  84. package/api/tests/libs/change-payment-plan.spec.ts +282 -0
  85. package/api/tests/libs/exchange-rate-service.spec.ts +341 -0
  86. package/api/tests/libs/quote-service.spec.ts +199 -0
  87. package/api/tests/libs/session.spec.ts +464 -0
  88. package/api/tests/libs/slippage.spec.ts +109 -0
  89. package/api/tests/libs/token-data-provider.spec.ts +267 -0
  90. package/api/tests/models/exchange-rate-provider.spec.ts +121 -0
  91. package/api/tests/models/price-dynamic.spec.ts +100 -0
  92. package/api/tests/models/price-quote.spec.ts +112 -0
  93. package/api/tests/routes/exchange-rate-providers.spec.ts +215 -0
  94. package/api/tests/routes/subscription-slippage.spec.ts +254 -0
  95. package/blocklet.yml +1 -1
  96. package/package.json +7 -6
  97. package/src/components/customer/credit-overview.tsx +14 -0
  98. package/src/components/discount/discount-info.tsx +8 -2
  99. package/src/components/invoice/list.tsx +146 -16
  100. package/src/components/invoice/table.tsx +276 -71
  101. package/src/components/invoice-pdf/template.tsx +3 -7
  102. package/src/components/metadata/form.tsx +6 -8
  103. package/src/components/price/form.tsx +519 -149
  104. package/src/components/promotion/active-redemptions.tsx +5 -3
  105. package/src/components/quote/info.tsx +234 -0
  106. package/src/hooks/subscription.ts +132 -2
  107. package/src/locales/en.tsx +145 -0
  108. package/src/locales/zh.tsx +143 -1
  109. package/src/pages/admin/billing/invoices/detail.tsx +41 -4
  110. package/src/pages/admin/products/exchange-rate-providers/edit-dialog.tsx +354 -0
  111. package/src/pages/admin/products/exchange-rate-providers/index.tsx +363 -0
  112. package/src/pages/admin/products/index.tsx +12 -1
  113. package/src/pages/customer/invoice/detail.tsx +36 -12
  114. package/src/pages/customer/subscription/change-payment.tsx +65 -3
  115. package/src/pages/customer/subscription/change-plan.tsx +207 -38
  116. package/src/pages/customer/subscription/detail.tsx +599 -419
@@ -0,0 +1,215 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+
3
+ /**
4
+ * Unit tests for exchange-rate-providers routes
5
+ * Focus: DELETE endpoint and test-connection endpoint
6
+ *
7
+ * Note: These are logic tests, not full integration tests with database
8
+ */
9
+
10
+ describe('Exchange Rate Providers Routes', () => {
11
+ describe('DELETE endpoint logic', () => {
12
+ /**
13
+ * TC-D03: Cannot delete in-use provider
14
+ */
15
+ it('should prevent deletion of in-use provider', () => {
16
+ // Mock scenario
17
+ const targetProviderId = 'provider-1';
18
+ const activeProviderId = 'provider-1'; // Same as target
19
+
20
+ // Logic from DELETE endpoint
21
+ const isInUse = targetProviderId === activeProviderId;
22
+
23
+ expect(isInUse).toBe(true);
24
+ // In actual endpoint: return 400 with error message
25
+ });
26
+
27
+ /**
28
+ * TC-D02: Allow deletion of non-active provider
29
+ */
30
+ it('should allow deletion of non-active provider', () => {
31
+ const targetProviderId: string = 'provider-2';
32
+ const activeProviderId: string = 'provider-1'; // Different
33
+
34
+ const isInUse = targetProviderId === activeProviderId;
35
+
36
+ expect(isInUse).toBe(false);
37
+ // In actual endpoint: proceed with deletion
38
+ });
39
+
40
+ /**
41
+ * TC-D04: Cannot delete last enabled provider
42
+ */
43
+ it('should prevent deletion of last enabled provider', () => {
44
+ // Mock scenario: target is enabled, no other enabled providers
45
+ const otherEnabledCount = 0;
46
+ const providerEnabled = true;
47
+ const providerStatus: string = 'active';
48
+
49
+ const isLastEnabled = otherEnabledCount === 0 && providerEnabled && providerStatus !== 'paused';
50
+
51
+ expect(isLastEnabled).toBe(true);
52
+ // In actual endpoint: return 400 with error message
53
+ });
54
+
55
+ /**
56
+ * TC-D05: Allow deletion when multiple enabled providers exist
57
+ */
58
+ it('should allow deletion when other enabled providers exist', () => {
59
+ const otherEnabledCount: number = 2; // Other enabled providers available
60
+ const providerEnabled = true;
61
+ const providerStatus: string = 'active';
62
+
63
+ const isLastEnabled = otherEnabledCount === 0 && providerEnabled && providerStatus !== 'paused';
64
+
65
+ expect(isLastEnabled).toBe(false);
66
+ // In actual endpoint: proceed with deletion
67
+ });
68
+
69
+ /**
70
+ * TC-D09: Allow deletion of paused provider
71
+ */
72
+ it('should allow deletion of paused provider even if no other enabled', () => {
73
+ const otherEnabledCount = 0;
74
+ const providerEnabled = true;
75
+ const providerStatus = 'paused';
76
+
77
+ const isLastEnabled = otherEnabledCount === 0 && providerEnabled && providerStatus !== 'paused';
78
+
79
+ expect(isLastEnabled).toBe(false);
80
+ // Paused providers don't count as "active", safe to delete
81
+ });
82
+ });
83
+
84
+ describe('Active provider selection logic', () => {
85
+ /**
86
+ * Verify the logic for determining active provider matches frontend
87
+ */
88
+ it('should select lowest priority enabled provider', () => {
89
+ const allProviders = [
90
+ { id: 'p1', priority: 2, enabled: true, status: 'active' },
91
+ { id: 'p2', priority: 1, enabled: true, status: 'active' },
92
+ { id: 'p3', priority: 3, enabled: false, status: 'active' },
93
+ ];
94
+
95
+ // Logic from DELETE endpoint
96
+ const activeProviders = allProviders
97
+ .filter((p) => p.enabled && p.status !== 'paused')
98
+ .sort((a, b) => a.priority - b.priority);
99
+
100
+ const activeProviderId = activeProviders[0]?.id || null;
101
+
102
+ expect(activeProviderId).toBe('p2'); // Lowest priority among enabled
103
+ });
104
+
105
+ it('should exclude paused providers from active selection', () => {
106
+ const allProviders = [
107
+ { id: 'p1', priority: 1, enabled: true, status: 'paused' },
108
+ { id: 'p2', priority: 2, enabled: true, status: 'active' },
109
+ ];
110
+
111
+ const activeProviders = allProviders
112
+ .filter((p) => p.enabled && p.status !== 'paused')
113
+ .sort((a, b) => a.priority - b.priority);
114
+
115
+ const activeProviderId = activeProviders[0]?.id || null;
116
+
117
+ expect(activeProviderId).toBe('p2'); // p1 is paused, so p2 is active
118
+ });
119
+
120
+ it('should return null when no active providers', () => {
121
+ const allProviders = [
122
+ { id: 'p1', priority: 1, enabled: false, status: 'active' },
123
+ { id: 'p2', priority: 2, enabled: true, status: 'paused' },
124
+ ];
125
+
126
+ const activeProviders = allProviders
127
+ .filter((p) => p.enabled && p.status !== 'paused')
128
+ .sort((a, b) => a.priority - b.priority);
129
+
130
+ const activeProviderId = activeProviders[0]?.id || null;
131
+
132
+ expect(activeProviderId).toBeNull();
133
+ });
134
+ });
135
+
136
+ describe('Test connection endpoint validation', () => {
137
+ /**
138
+ * TC-T07: Invalid provider type
139
+ */
140
+ it('should reject invalid provider types', () => {
141
+ const allowedTypes = ['token-data', 'coingecko', 'coinmarketcap'];
142
+ const testType = 'invalid-type';
143
+
144
+ const isValid = allowedTypes.includes(testType);
145
+
146
+ expect(isValid).toBe(false);
147
+ // In actual endpoint: return 400
148
+ });
149
+
150
+ /**
151
+ * TC-T01: Valid provider types
152
+ */
153
+ it('should accept valid provider types', () => {
154
+ const allowedTypes = ['token-data', 'coingecko', 'coinmarketcap'];
155
+
156
+ expect(allowedTypes.includes('token-data')).toBe(true);
157
+ expect(allowedTypes.includes('coingecko')).toBe(true);
158
+ expect(allowedTypes.includes('coinmarketcap')).toBe(true);
159
+ });
160
+
161
+ /**
162
+ * Test symbol selection
163
+ */
164
+ it('should use ETH as test symbol', () => {
165
+ const testSymbol = 'ETH';
166
+
167
+ expect(testSymbol).toBe('ETH');
168
+ // ETH is most common and stable token for testing
169
+ });
170
+ });
171
+
172
+ describe('Safety constraint scenarios', () => {
173
+ /**
174
+ * INT-02: Full delete protection workflow
175
+ */
176
+ it('should handle delete protection workflow correctly', () => {
177
+ // Scenario: 2 providers, A (priority 1), B (priority 2)
178
+ const providers = [
179
+ { id: 'A', priority: 1, enabled: true, status: 'active' },
180
+ { id: 'B', priority: 2, enabled: true, status: 'active' },
181
+ ];
182
+
183
+ // Provider A is in-use
184
+ const activeProviders = providers
185
+ .filter((p) => p.enabled && p.status !== 'paused')
186
+ .sort((a, b) => a.priority - b.priority);
187
+ let activeProviderId = activeProviders[0]?.id;
188
+
189
+ expect(activeProviderId).toBe('A');
190
+
191
+ // Try to delete A - should be blocked
192
+ const canDeleteA = activeProviderId !== 'A';
193
+ expect(canDeleteA).toBe(false);
194
+
195
+ // Disable A
196
+ providers[0]!.enabled = false;
197
+
198
+ // Now B should become active
199
+ const updatedActiveProviders = providers
200
+ .filter((p) => p.enabled && p.status !== 'paused')
201
+ .sort((a, b) => a.priority - b.priority);
202
+ activeProviderId = updatedActiveProviders[0]?.id;
203
+
204
+ expect(activeProviderId).toBe('B');
205
+
206
+ // Now A can be deleted (not in-use, not last enabled since B exists)
207
+ const canDeleteANow = activeProviderId !== 'A';
208
+ const otherEnabledCount: number = 1; // B is enabled
209
+ const aIsLastEnabled = otherEnabledCount === 0 && !providers[0]!.enabled;
210
+
211
+ expect(canDeleteANow).toBe(true);
212
+ expect(aIsLastEnabled).toBe(false);
213
+ });
214
+ });
215
+ });
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Unit tests for subscription slippage routes
3
+ * Focus: PUT /:id/slippage endpoint validation logic
4
+ *
5
+ * Note: These are logic tests, not full integration tests with database
6
+ */
7
+
8
+ describe('Subscription Slippage Routes', () => {
9
+ describe('normalizePercent validation', () => {
10
+ const normalizePercent = (value: any) => {
11
+ const normalized = typeof value === 'string' ? Number(value) : value;
12
+ if (!Number.isFinite(normalized) || normalized < 0) {
13
+ return null;
14
+ }
15
+ return normalized;
16
+ };
17
+
18
+ it('should accept valid positive number', () => {
19
+ expect(normalizePercent(0.5)).toBe(0.5);
20
+ expect(normalizePercent(1)).toBe(1);
21
+ expect(normalizePercent(5)).toBe(5);
22
+ expect(normalizePercent(100)).toBe(100);
23
+ });
24
+
25
+ it('should accept zero', () => {
26
+ expect(normalizePercent(0)).toBe(0);
27
+ });
28
+
29
+ it('should accept string numbers', () => {
30
+ expect(normalizePercent('0.5')).toBe(0.5);
31
+ expect(normalizePercent('1')).toBe(1);
32
+ expect(normalizePercent('0')).toBe(0);
33
+ });
34
+
35
+ it('should reject negative numbers', () => {
36
+ expect(normalizePercent(-1)).toBeNull();
37
+ expect(normalizePercent(-0.5)).toBeNull();
38
+ expect(normalizePercent('-1')).toBeNull();
39
+ });
40
+
41
+ it('should reject invalid values', () => {
42
+ expect(normalizePercent(NaN)).toBeNull();
43
+ expect(normalizePercent(Infinity)).toBeNull();
44
+ expect(normalizePercent(-Infinity)).toBeNull();
45
+ expect(normalizePercent('abc')).toBeNull();
46
+ expect(normalizePercent(null)).toBeNull();
47
+ expect(normalizePercent(undefined)).toBeNull();
48
+ });
49
+ });
50
+
51
+ describe('calcMinRateFromPercent', () => {
52
+ const calcMinRateFromPercent = (percent: number, currentRate: string): string => {
53
+ const rateNum = Number(currentRate);
54
+ if (!Number.isFinite(rateNum) || rateNum <= 0) {
55
+ return '0';
56
+ }
57
+ // min_acceptable_rate = current_rate / (1 + percent/100)
58
+ const minRate = rateNum / (1 + percent / 100);
59
+ return minRate.toFixed(8);
60
+ };
61
+
62
+ it('should calculate min rate correctly with 0.5% slippage', () => {
63
+ // If current rate is 100 and slippage is 0.5%, min rate = 100 / 1.005 = 99.502...
64
+ const result = calcMinRateFromPercent(0.5, '100');
65
+ expect(Number(result)).toBeCloseTo(99.5024876, 5);
66
+ });
67
+
68
+ it('should calculate min rate correctly with 1% slippage', () => {
69
+ // If current rate is 100 and slippage is 1%, min rate = 100 / 1.01 = 99.0099...
70
+ const result = calcMinRateFromPercent(1, '100');
71
+ expect(Number(result)).toBeCloseTo(99.00990099, 5);
72
+ });
73
+
74
+ it('should calculate min rate correctly with 5% slippage', () => {
75
+ // If current rate is 100 and slippage is 5%, min rate = 100 / 1.05 = 95.238...
76
+ const result = calcMinRateFromPercent(5, '100');
77
+ expect(Number(result)).toBeCloseTo(95.23809524, 5);
78
+ });
79
+
80
+ it('should handle zero percent (no slippage)', () => {
81
+ // If slippage is 0%, min rate equals current rate
82
+ const result = calcMinRateFromPercent(0, '100');
83
+ expect(Number(result)).toBe(100);
84
+ });
85
+
86
+ it('should return 0 for invalid rate', () => {
87
+ expect(calcMinRateFromPercent(0.5, '0')).toBe('0');
88
+ expect(calcMinRateFromPercent(0.5, '-100')).toBe('0');
89
+ expect(calcMinRateFromPercent(0.5, 'abc')).toBe('0');
90
+ });
91
+ });
92
+
93
+ describe('slippage config validation', () => {
94
+ describe('percent mode', () => {
95
+ it('should accept valid percent config', () => {
96
+ const rawConfig = {
97
+ mode: 'percent',
98
+ percent: 0.5,
99
+ base_currency: 'USD',
100
+ };
101
+
102
+ expect(rawConfig.mode).toBe('percent');
103
+ expect(rawConfig.percent).toBe(0.5);
104
+ expect(rawConfig.base_currency).toBe('USD');
105
+ });
106
+
107
+ it('should reject config with negative percent', () => {
108
+ const rawConfig = {
109
+ mode: 'percent',
110
+ percent: -1,
111
+ };
112
+
113
+ const normalizePercent = (value: any) => {
114
+ const normalized = typeof value === 'string' ? Number(value) : value;
115
+ if (!Number.isFinite(normalized) || normalized < 0) {
116
+ return null;
117
+ }
118
+ return normalized;
119
+ };
120
+
121
+ expect(normalizePercent(rawConfig.percent)).toBeNull();
122
+ });
123
+
124
+ it('should accept config with pre-calculated min_acceptable_rate', () => {
125
+ const rawConfig = {
126
+ mode: 'percent',
127
+ percent: 0.5,
128
+ min_acceptable_rate: '99.5024876',
129
+ base_currency: 'USD',
130
+ };
131
+
132
+ expect(rawConfig.min_acceptable_rate).toBe('99.5024876');
133
+ });
134
+ });
135
+
136
+ describe('rate mode', () => {
137
+ it('should require min_acceptable_rate for rate mode', () => {
138
+ const rawConfig: { mode: string; percent: number; min_acceptable_rate?: string } = {
139
+ mode: 'rate',
140
+ percent: 0,
141
+ // missing min_acceptable_rate
142
+ };
143
+
144
+ const minRate = rawConfig.min_acceptable_rate ?? (rawConfig as any).minAcceptableRate;
145
+ expect(minRate === undefined || minRate === null || minRate === '').toBe(true);
146
+ });
147
+
148
+ it('should accept valid rate config', () => {
149
+ const rawConfig = {
150
+ mode: 'rate',
151
+ percent: 0.5,
152
+ min_acceptable_rate: '95.5',
153
+ base_currency: 'USD',
154
+ };
155
+
156
+ expect(rawConfig.mode).toBe('rate');
157
+ expect(rawConfig.min_acceptable_rate).toBe('95.5');
158
+ });
159
+ });
160
+
161
+ describe('legacy slippage_percent support', () => {
162
+ it('should accept simple slippage_percent', () => {
163
+ const reqBody = {
164
+ slippage_percent: 0.5,
165
+ };
166
+
167
+ const normalizePercent = (value: any) => {
168
+ const normalized = typeof value === 'string' ? Number(value) : value;
169
+ if (!Number.isFinite(normalized) || normalized < 0) {
170
+ return null;
171
+ }
172
+ return normalized;
173
+ };
174
+
175
+ expect(normalizePercent(reqBody.slippage_percent)).toBe(0.5);
176
+ });
177
+
178
+ it('should reject invalid slippage_percent', () => {
179
+ const reqBody = {
180
+ slippage_percent: 'invalid',
181
+ };
182
+
183
+ const normalizePercent = (value: any) => {
184
+ const normalized = typeof value === 'string' ? Number(value) : value;
185
+ if (!Number.isFinite(normalized) || normalized < 0) {
186
+ return null;
187
+ }
188
+ return normalized;
189
+ };
190
+
191
+ expect(normalizePercent(reqBody.slippage_percent)).toBeNull();
192
+ });
193
+ });
194
+ });
195
+
196
+ describe('config building', () => {
197
+ it('should build percent mode config with timestamp', () => {
198
+ const now = Date.now();
199
+ const config = {
200
+ mode: 'percent',
201
+ percent: 0.5,
202
+ base_currency: 'USD',
203
+ min_acceptable_rate: '99.5',
204
+ updated_at_ms: now,
205
+ };
206
+
207
+ expect(config.mode).toBe('percent');
208
+ expect(config.percent).toBe(0.5);
209
+ expect(config.updated_at_ms).toBe(now);
210
+ });
211
+
212
+ it('should build rate mode config with all fields', () => {
213
+ const now = Date.now();
214
+ const config = {
215
+ mode: 'rate',
216
+ percent: 0,
217
+ min_acceptable_rate: '95.0',
218
+ base_currency: 'USD',
219
+ rate_at_config_time: '100.0',
220
+ updated_at_ms: now,
221
+ };
222
+
223
+ expect(config.mode).toBe('rate');
224
+ expect(config.min_acceptable_rate).toBe('95.0');
225
+ expect(config.rate_at_config_time).toBe('100.0');
226
+ });
227
+ });
228
+
229
+ describe('error scenarios', () => {
230
+ it('should return error when no config provided', () => {
231
+ const rawConfig = null;
232
+ const slippagePercent = undefined;
233
+
234
+ const hasConfig = rawConfig && typeof rawConfig === 'object';
235
+ const hasPercent = slippagePercent !== undefined && slippagePercent !== null;
236
+
237
+ expect(hasConfig || hasPercent).toBe(false);
238
+ // In actual endpoint: return 400 with error 'slippage config is required'
239
+ });
240
+
241
+ it('should return error for rate mode without min_acceptable_rate', () => {
242
+ const rawConfig: { mode: string; percent: number; min_acceptable_rate?: string } = {
243
+ mode: 'rate',
244
+ percent: 0,
245
+ };
246
+
247
+ const minRate = rawConfig.min_acceptable_rate ?? (rawConfig as any).minAcceptableRate;
248
+ const isInvalid = minRate === undefined || minRate === null || minRate === '';
249
+
250
+ expect(isInvalid).toBe(true);
251
+ // In actual endpoint: return 400 with error 'min_acceptable_rate is required for rate mode'
252
+ });
253
+ });
254
+ });
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.24.4
17
+ version: 1.25.1
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.24.4",
3
+ "version": "1.25.1",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "prelint": "npm run types",
@@ -59,9 +59,9 @@
59
59
  "@blocklet/error": "^0.3.5",
60
60
  "@blocklet/js-sdk": "^1.17.8-beta-20260104-120132-cb5b1914",
61
61
  "@blocklet/logger": "^1.17.8-beta-20260104-120132-cb5b1914",
62
- "@blocklet/payment-broker-client": "1.24.4",
63
- "@blocklet/payment-react": "1.24.4",
64
- "@blocklet/payment-vendor": "1.24.4",
62
+ "@blocklet/payment-broker-client": "1.25.1",
63
+ "@blocklet/payment-react": "1.25.1",
64
+ "@blocklet/payment-vendor": "1.25.1",
65
65
  "@blocklet/sdk": "^1.17.8-beta-20260104-120132-cb5b1914",
66
66
  "@blocklet/ui-react": "^3.4.7",
67
67
  "@blocklet/uploader": "^0.3.19",
@@ -79,6 +79,7 @@
79
79
  "@stripe/stripe-js": "^2.4.0",
80
80
  "ahooks": "^3.8.5",
81
81
  "axios": "^1.10.0",
82
+ "bignumber.js": "^9.3.1",
82
83
  "body-parser": "^1.20.3",
83
84
  "cls-hooked": "^4.2.2",
84
85
  "cookie-parser": "^1.4.7",
@@ -131,7 +132,7 @@
131
132
  "devDependencies": {
132
133
  "@abtnode/types": "^1.17.8-beta-20260104-120132-cb5b1914",
133
134
  "@arcblock/eslint-config-ts": "^0.3.3",
134
- "@blocklet/payment-types": "1.24.4",
135
+ "@blocklet/payment-types": "1.25.1",
135
136
  "@types/cookie-parser": "^1.4.9",
136
137
  "@types/cors": "^2.8.19",
137
138
  "@types/debug": "^4.1.12",
@@ -178,5 +179,5 @@
178
179
  "parser": "typescript"
179
180
  }
180
181
  },
181
- "gitHead": "d4a5f67e657cafa8862912bb8de38a3d56a7919d"
182
+ "gitHead": "87555132c62024e5c677fd0a39df0d90be06a543"
182
183
  }
@@ -79,6 +79,20 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
79
79
  }
80
80
  }, [searchParams]);
81
81
 
82
+ // Handle auto-recharge action from notification deep link
83
+ useEffect(() => {
84
+ const action = searchParams.get('action');
85
+ const currencyId = searchParams.get('currencyId');
86
+ if (action === 'auto-recharge' && currencyId) {
87
+ setAutoRecharge({ currencyId, open: true });
88
+ // Clean up query params after opening modal
89
+ const newParams = new URLSearchParams(searchParams);
90
+ newParams.delete('action');
91
+ newParams.delete('currencyId');
92
+ setSearchParams(newParams, { replace: true });
93
+ }
94
+ }, [searchParams, setSearchParams]);
95
+
82
96
  const creditCurrencies = useMemo(() => {
83
97
  return (
84
98
  settings?.paymentMethods
@@ -66,11 +66,17 @@ export default function DiscountInfo({ discountStats = null }: DiscountInfoProps
66
66
  return t(`admin.coupon.couponTermsDuration.${coupon.duration}`);
67
67
  };
68
68
 
69
- // Format total savings
69
+ // Format total savings with proper decimal precision
70
70
  const totalSavingsEntries = Object.entries(discountStats.total_savings);
71
71
  const savingsText =
72
72
  totalSavingsEntries.length > 0
73
- ? totalSavingsEntries.map(([, savings]) => savings.formattedAmount).join(', ')
73
+ ? totalSavingsEntries
74
+ .map(([, savings]) => {
75
+ // Use formatAmount to properly format with decimal precision
76
+ const formatted = formatAmount(savings.amount, savings.currency?.decimal || 18);
77
+ return `${formatted} ${savings.currency?.symbol || ''}`;
78
+ })
79
+ .join(', ')
74
80
  : '--';
75
81
 
76
82
  // Get validity period