payment-kit 1.24.3 → 1.25.0
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.
- package/api/src/crons/overdue-detection.ts +10 -1
- package/api/src/index.ts +3 -0
- package/api/src/libs/credit-utils.ts +21 -0
- package/api/src/libs/discount/discount.ts +13 -0
- package/api/src/libs/env.ts +5 -0
- package/api/src/libs/error.ts +14 -0
- package/api/src/libs/exchange-rate/coingecko-provider.ts +193 -0
- package/api/src/libs/exchange-rate/coinmarketcap-provider.ts +180 -0
- package/api/src/libs/exchange-rate/index.ts +5 -0
- package/api/src/libs/exchange-rate/service.ts +583 -0
- package/api/src/libs/exchange-rate/token-address-mapping.ts +84 -0
- package/api/src/libs/exchange-rate/token-addresses.json +2147 -0
- package/api/src/libs/exchange-rate/token-data-provider.ts +142 -0
- package/api/src/libs/exchange-rate/types.ts +114 -0
- package/api/src/libs/exchange-rate/validator.ts +319 -0
- package/api/src/libs/invoice-quote.ts +158 -0
- package/api/src/libs/invoice.ts +143 -7
- package/api/src/libs/math-utils.ts +46 -0
- package/api/src/libs/notification/template/billing-discrepancy.ts +3 -4
- package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +174 -79
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +2 -3
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +3 -3
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +3 -3
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +2 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +9 -4
- package/api/src/libs/notification/template/exchange-rate-alert.ts +202 -0
- package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +203 -0
- package/api/src/libs/notification/template/subscription-slippage-warning.ts +212 -0
- package/api/src/libs/notification/template/subscription-will-canceled.ts +2 -2
- package/api/src/libs/notification/template/subscription-will-renew.ts +22 -8
- package/api/src/libs/payment.ts +1 -1
- package/api/src/libs/price.ts +4 -1
- package/api/src/libs/queue/index.ts +8 -0
- package/api/src/libs/quote-service.ts +1132 -0
- package/api/src/libs/quote-validation.ts +388 -0
- package/api/src/libs/session.ts +686 -39
- package/api/src/libs/slippage.ts +135 -0
- package/api/src/libs/subscription.ts +185 -15
- package/api/src/libs/util.ts +64 -3
- package/api/src/locales/en.ts +50 -0
- package/api/src/locales/zh.ts +48 -0
- package/api/src/queues/auto-recharge.ts +295 -21
- package/api/src/queues/exchange-rate-health.ts +242 -0
- package/api/src/queues/invoice.ts +48 -1
- package/api/src/queues/notification.ts +190 -3
- package/api/src/queues/payment.ts +177 -7
- package/api/src/queues/subscription.ts +436 -6
- package/api/src/routes/auto-recharge-configs.ts +71 -6
- package/api/src/routes/checkout-sessions.ts +1730 -81
- package/api/src/routes/connect/auto-recharge-auth.ts +2 -0
- package/api/src/routes/connect/change-payer.ts +2 -0
- package/api/src/routes/connect/change-payment.ts +61 -8
- package/api/src/routes/connect/change-plan.ts +161 -17
- package/api/src/routes/connect/collect.ts +9 -6
- package/api/src/routes/connect/delegation.ts +1 -0
- package/api/src/routes/connect/pay.ts +157 -0
- package/api/src/routes/connect/setup.ts +32 -10
- package/api/src/routes/connect/shared.ts +159 -13
- package/api/src/routes/connect/subscribe.ts +32 -9
- package/api/src/routes/credit-grants.ts +99 -0
- package/api/src/routes/exchange-rate-providers.ts +248 -0
- package/api/src/routes/exchange-rates.ts +87 -0
- package/api/src/routes/index.ts +4 -0
- package/api/src/routes/invoices.ts +280 -2
- package/api/src/routes/meter-events.ts +3 -0
- package/api/src/routes/payment-links.ts +13 -0
- package/api/src/routes/prices.ts +84 -2
- package/api/src/routes/subscriptions.ts +526 -15
- package/api/src/store/migrations/20251220-dynamic-pricing.ts +245 -0
- package/api/src/store/migrations/20251223-exchange-rate-provider-type.ts +28 -0
- package/api/src/store/migrations/20260110-add-quote-locked-at.ts +23 -0
- package/api/src/store/migrations/20260112-add-checkout-session-slippage-percent.ts +22 -0
- package/api/src/store/migrations/20260113-add-price-quote-slippage-fields.ts +45 -0
- package/api/src/store/migrations/20260116-subscription-slippage.ts +21 -0
- package/api/src/store/migrations/20260120-auto-recharge-slippage.ts +21 -0
- package/api/src/store/models/auto-recharge-config.ts +12 -0
- package/api/src/store/models/checkout-session.ts +7 -0
- package/api/src/store/models/exchange-rate-provider.ts +225 -0
- package/api/src/store/models/index.ts +6 -0
- package/api/src/store/models/payment-intent.ts +6 -0
- package/api/src/store/models/price-quote.ts +284 -0
- package/api/src/store/models/price.ts +53 -5
- package/api/src/store/models/subscription.ts +11 -0
- package/api/src/store/models/types.ts +61 -1
- package/api/tests/libs/change-payment-plan.spec.ts +282 -0
- package/api/tests/libs/exchange-rate-service.spec.ts +341 -0
- package/api/tests/libs/quote-service.spec.ts +199 -0
- package/api/tests/libs/session.spec.ts +464 -0
- package/api/tests/libs/slippage.spec.ts +109 -0
- package/api/tests/libs/token-data-provider.spec.ts +267 -0
- package/api/tests/models/exchange-rate-provider.spec.ts +121 -0
- package/api/tests/models/price-dynamic.spec.ts +100 -0
- package/api/tests/models/price-quote.spec.ts +112 -0
- package/api/tests/routes/exchange-rate-providers.spec.ts +215 -0
- package/api/tests/routes/subscription-slippage.spec.ts +254 -0
- package/blocklet.yml +1 -1
- package/package.json +7 -6
- package/src/components/customer/credit-overview.tsx +14 -0
- package/src/components/discount/discount-info.tsx +8 -2
- package/src/components/invoice/list.tsx +146 -16
- package/src/components/invoice/table.tsx +276 -71
- package/src/components/invoice-pdf/template.tsx +3 -7
- package/src/components/metadata/form.tsx +6 -8
- package/src/components/price/form.tsx +519 -149
- package/src/components/promotion/active-redemptions.tsx +5 -3
- package/src/components/quote/info.tsx +234 -0
- package/src/hooks/subscription.ts +132 -2
- package/src/locales/en.tsx +145 -0
- package/src/locales/zh.tsx +143 -1
- package/src/pages/admin/billing/invoices/detail.tsx +41 -4
- package/src/pages/admin/products/exchange-rate-providers/edit-dialog.tsx +354 -0
- package/src/pages/admin/products/exchange-rate-providers/index.tsx +363 -0
- package/src/pages/admin/products/index.tsx +12 -1
- package/src/pages/customer/invoice/detail.tsx +36 -12
- package/src/pages/customer/subscription/change-payment.tsx +65 -3
- package/src/pages/customer/subscription/change-plan.tsx +207 -38
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
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.
|
|
63
|
-
"@blocklet/payment-react": "1.
|
|
64
|
-
"@blocklet/payment-vendor": "1.
|
|
62
|
+
"@blocklet/payment-broker-client": "1.25.0",
|
|
63
|
+
"@blocklet/payment-react": "1.25.0",
|
|
64
|
+
"@blocklet/payment-vendor": "1.25.0",
|
|
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.
|
|
135
|
+
"@blocklet/payment-types": "1.25.0",
|
|
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": "
|
|
182
|
+
"gitHead": "00a94943fea21487c042a7b484f4649add502bc9"
|
|
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
|
|
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
|