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.
- 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 +3 -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 +167 -1
- package/api/src/queues/payment.ts +177 -7
- package/api/src/queues/refund.ts +41 -9
- 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/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,199 @@
|
|
|
1
|
+
import { BN } from '@ocap/util';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
|
|
4
|
+
describe('QuoteService', () => {
|
|
5
|
+
describe('Idempotency key generation', () => {
|
|
6
|
+
it('should generate consistent idempotency key for same params', () => {
|
|
7
|
+
const params = {
|
|
8
|
+
contextId: 'session_123',
|
|
9
|
+
priceId: 'price_456',
|
|
10
|
+
currencyId: 'currency_789',
|
|
11
|
+
quantity: 1,
|
|
12
|
+
baseAmount: '10.00',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const key1 = crypto
|
|
16
|
+
.createHash('sha256')
|
|
17
|
+
.update(`${params.contextId}:${params.priceId}:${params.currencyId}:${params.quantity}:${params.baseAmount}`)
|
|
18
|
+
.digest('hex')
|
|
19
|
+
.substring(0, 64);
|
|
20
|
+
|
|
21
|
+
const key2 = crypto
|
|
22
|
+
.createHash('sha256')
|
|
23
|
+
.update(`${params.contextId}:${params.priceId}:${params.currencyId}:${params.quantity}:${params.baseAmount}`)
|
|
24
|
+
.digest('hex')
|
|
25
|
+
.substring(0, 64);
|
|
26
|
+
|
|
27
|
+
expect(key1).toBe(key2);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should generate different keys for different quantities', () => {
|
|
31
|
+
const params1 = {
|
|
32
|
+
contextId: 'session_123',
|
|
33
|
+
priceId: 'price_456',
|
|
34
|
+
currencyId: 'currency_789',
|
|
35
|
+
quantity: 1,
|
|
36
|
+
baseAmount: '10.00',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const params2 = {
|
|
40
|
+
...params1,
|
|
41
|
+
quantity: 2,
|
|
42
|
+
baseAmount: '20.00',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const key1 = crypto
|
|
46
|
+
.createHash('sha256')
|
|
47
|
+
.update(
|
|
48
|
+
`${params1.contextId}:${params1.priceId}:${params1.currencyId}:${params1.quantity}:${params1.baseAmount}`
|
|
49
|
+
)
|
|
50
|
+
.digest('hex')
|
|
51
|
+
.substring(0, 64);
|
|
52
|
+
|
|
53
|
+
const key2 = crypto
|
|
54
|
+
.createHash('sha256')
|
|
55
|
+
.update(
|
|
56
|
+
`${params2.contextId}:${params2.priceId}:${params2.currencyId}:${params2.quantity}:${params2.baseAmount}`
|
|
57
|
+
)
|
|
58
|
+
.digest('hex')
|
|
59
|
+
.substring(0, 64);
|
|
60
|
+
|
|
61
|
+
expect(key1).not.toBe(key2);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('Quote amount calculation', () => {
|
|
66
|
+
it('should correctly calculate quoted amount with ceiling', () => {
|
|
67
|
+
const baseAmount = 10; // $10 USD
|
|
68
|
+
const rate = 0.1; // 1 token = $0.1 USD
|
|
69
|
+
const decimals = 18;
|
|
70
|
+
|
|
71
|
+
// token_amount = base_usd / rate = 10 / 0.1 = 100 tokens
|
|
72
|
+
const tokenAmount = baseAmount / rate;
|
|
73
|
+
expect(tokenAmount).toBe(100);
|
|
74
|
+
|
|
75
|
+
// Using the same logic as QuoteService
|
|
76
|
+
const PRECISION_SCALE = 1e8;
|
|
77
|
+
const scaledNumerator = Math.floor(baseAmount * PRECISION_SCALE);
|
|
78
|
+
const scaledDenominator = Math.floor(rate * PRECISION_SCALE);
|
|
79
|
+
const numerator = new BN(scaledNumerator).mul(new BN(10).pow(new BN(decimals)));
|
|
80
|
+
const denominator = new BN(scaledDenominator);
|
|
81
|
+
const quotedAmount = numerator.add(denominator).sub(new BN(1)).div(denominator);
|
|
82
|
+
|
|
83
|
+
expect(quotedAmount.toString()).toBe('100000000000000000000');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should round up fractional token amounts', () => {
|
|
87
|
+
const baseAmount = 10.5; // $10.5 USD
|
|
88
|
+
const rate = 0.15; // 1 token = $0.15 USD
|
|
89
|
+
const decimals = 18;
|
|
90
|
+
|
|
91
|
+
// token_amount = 10.5 / 0.15 = 70
|
|
92
|
+
const tokenAmount = baseAmount / rate;
|
|
93
|
+
expect(tokenAmount).toBe(70);
|
|
94
|
+
|
|
95
|
+
const PRECISION_SCALE = 1e8;
|
|
96
|
+
const scaledNumerator = Math.floor(baseAmount * PRECISION_SCALE);
|
|
97
|
+
const scaledDenominator = Math.floor(rate * PRECISION_SCALE);
|
|
98
|
+
const numerator = new BN(scaledNumerator).mul(new BN(10).pow(new BN(decimals)));
|
|
99
|
+
const denominator = new BN(scaledDenominator);
|
|
100
|
+
const quotedAmount = numerator.add(denominator).sub(new BN(1)).div(denominator);
|
|
101
|
+
|
|
102
|
+
expect(quotedAmount.toString()).toBe('70000000000000000000');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should handle precision correctly with many decimals', () => {
|
|
106
|
+
const baseAmount = 100;
|
|
107
|
+
const rate = 0.123456789;
|
|
108
|
+
const decimals = 18;
|
|
109
|
+
|
|
110
|
+
const PRECISION_SCALE = 1e8;
|
|
111
|
+
const scaledNumerator = Math.floor(baseAmount * PRECISION_SCALE);
|
|
112
|
+
const scaledDenominator = Math.floor(rate * PRECISION_SCALE);
|
|
113
|
+
const numerator = new BN(scaledNumerator).mul(new BN(10).pow(new BN(decimals)));
|
|
114
|
+
const denominator = new BN(scaledDenominator);
|
|
115
|
+
const quotedAmount = numerator.add(denominator).sub(new BN(1)).div(denominator);
|
|
116
|
+
|
|
117
|
+
// Should ceil to ensure user pays enough
|
|
118
|
+
expect(quotedAmount.gt(new BN(0))).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('Quote expiration', () => {
|
|
123
|
+
it('should calculate correct expiration time', () => {
|
|
124
|
+
const now = Math.floor(Date.now() / 1000);
|
|
125
|
+
const lockDuration = 300; // 5 minutes
|
|
126
|
+
const expiresAt = now + lockDuration;
|
|
127
|
+
|
|
128
|
+
expect(expiresAt).toBeGreaterThan(now);
|
|
129
|
+
expect(expiresAt - now).toBe(lockDuration);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should use default lock duration of 300 seconds', () => {
|
|
133
|
+
const defaultLockDuration = 300;
|
|
134
|
+
const now = Math.floor(Date.now() / 1000);
|
|
135
|
+
const expiresAt = now + defaultLockDuration;
|
|
136
|
+
|
|
137
|
+
expect(expiresAt - now).toBe(300);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('Quote metadata structure', () => {
|
|
142
|
+
it('should include calculation details in metadata', () => {
|
|
143
|
+
const tokenAmount = '100.123456789';
|
|
144
|
+
const unitAmount = '100123456789000000000';
|
|
145
|
+
|
|
146
|
+
const metadata = {
|
|
147
|
+
calculation: {
|
|
148
|
+
token_amount_raw: tokenAmount,
|
|
149
|
+
unit_amount_raw: unitAmount,
|
|
150
|
+
},
|
|
151
|
+
rounding: {
|
|
152
|
+
mode: 'ceil',
|
|
153
|
+
token_decimals: 18,
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
expect(metadata.calculation.token_amount_raw).toBe(tokenAmount);
|
|
158
|
+
expect(metadata.calculation.unit_amount_raw).toBe(unitAmount);
|
|
159
|
+
expect(metadata.rounding.mode).toBe('ceil');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should include risk information in metadata', () => {
|
|
163
|
+
const metadata = {
|
|
164
|
+
risk: {
|
|
165
|
+
anomaly_detected: false,
|
|
166
|
+
degraded: false,
|
|
167
|
+
degraded_reason: null,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
expect(metadata.risk.anomaly_detected).toBe(false);
|
|
172
|
+
expect(metadata.risk.degraded).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('Validation logic', () => {
|
|
177
|
+
it('should require either session_id or invoice_id', () => {
|
|
178
|
+
const params = {
|
|
179
|
+
price_id: 'price_123',
|
|
180
|
+
target_currency_id: 'currency_456',
|
|
181
|
+
quantity: 1,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Neither session_id nor invoice_id provided
|
|
185
|
+
expect(params).not.toHaveProperty('session_id');
|
|
186
|
+
expect(params).not.toHaveProperty('invoice_id');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should validate dynamic pricing type', () => {
|
|
190
|
+
const price = {
|
|
191
|
+
pricing_type: 'fixed',
|
|
192
|
+
base_currency: 'USD',
|
|
193
|
+
base_amount: '10',
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
expect(price.pricing_type).not.toBe('dynamic');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -383,6 +383,470 @@ describe('getSubscriptionCreateSetup', () => {
|
|
|
383
383
|
.unix()
|
|
384
384
|
);
|
|
385
385
|
});
|
|
386
|
+
|
|
387
|
+
describe('subscription authorization slippage protection', () => {
|
|
388
|
+
it('should NOT apply slippage for static pricing (backward compatibility)', () => {
|
|
389
|
+
const items = [
|
|
390
|
+
{
|
|
391
|
+
price: {
|
|
392
|
+
type: 'recurring',
|
|
393
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
394
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
395
|
+
pricing_type: 'fixed', // Static pricing
|
|
396
|
+
},
|
|
397
|
+
quantity: 1,
|
|
398
|
+
},
|
|
399
|
+
];
|
|
400
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd');
|
|
401
|
+
// Base: 1000, No slippage for static pricing
|
|
402
|
+
expect(result.amount.setup).toBe('1000');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should apply 0.5% slippage protection for dynamic pricing', () => {
|
|
406
|
+
const items = [
|
|
407
|
+
{
|
|
408
|
+
price: {
|
|
409
|
+
type: 'recurring',
|
|
410
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
411
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
412
|
+
pricing_type: 'dynamic', // Dynamic pricing
|
|
413
|
+
},
|
|
414
|
+
quantity: 1,
|
|
415
|
+
},
|
|
416
|
+
];
|
|
417
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd');
|
|
418
|
+
// Base: 1000, With 0.5% slippage: 1000 * 1.005 = 1005
|
|
419
|
+
expect(result.amount.setup).toBe('1005');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('should apply slippage correctly with multiple quantities', () => {
|
|
423
|
+
const items = [
|
|
424
|
+
{
|
|
425
|
+
price: {
|
|
426
|
+
type: 'recurring',
|
|
427
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
428
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
429
|
+
pricing_type: 'dynamic',
|
|
430
|
+
},
|
|
431
|
+
quantity: 3,
|
|
432
|
+
},
|
|
433
|
+
];
|
|
434
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd');
|
|
435
|
+
// Base: 1000 * 3 = 3000, With 0.5% slippage: 3000 * 1.005 = 3015
|
|
436
|
+
expect(result.amount.setup).toBe('3015');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should apply slippage with mixed dynamic and one-time items', () => {
|
|
440
|
+
const items = [
|
|
441
|
+
{
|
|
442
|
+
price: {
|
|
443
|
+
type: 'recurring',
|
|
444
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
445
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
446
|
+
pricing_type: 'dynamic',
|
|
447
|
+
},
|
|
448
|
+
quantity: 1,
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
price: {
|
|
452
|
+
type: 'one_time',
|
|
453
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '500' }],
|
|
454
|
+
},
|
|
455
|
+
quantity: 1,
|
|
456
|
+
},
|
|
457
|
+
];
|
|
458
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd');
|
|
459
|
+
// Base: 1000 (recurring) + 500 (one-time) = 1500
|
|
460
|
+
// With 0.5% slippage: 1500 * 1.005 = 1507
|
|
461
|
+
expect(result.amount.setup).toBe('1507');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should apply slippage with mixed dynamic and static recurring items', () => {
|
|
465
|
+
const items = [
|
|
466
|
+
{
|
|
467
|
+
price: {
|
|
468
|
+
type: 'recurring',
|
|
469
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
470
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
471
|
+
pricing_type: 'dynamic',
|
|
472
|
+
},
|
|
473
|
+
quantity: 1,
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
price: {
|
|
477
|
+
type: 'recurring',
|
|
478
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '500' }],
|
|
479
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
480
|
+
pricing_type: 'fixed', // Static pricing
|
|
481
|
+
},
|
|
482
|
+
quantity: 1,
|
|
483
|
+
},
|
|
484
|
+
];
|
|
485
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd');
|
|
486
|
+
// Base: 1000 (dynamic) + 500 (static) = 1500
|
|
487
|
+
// With 0.5% slippage (because at least one item is dynamic): 1500 * 1.005 = 1507
|
|
488
|
+
expect(result.amount.setup).toBe('1507');
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('should NOT apply slippage when setup amount is zero', () => {
|
|
492
|
+
const items = [
|
|
493
|
+
{
|
|
494
|
+
price: {
|
|
495
|
+
type: 'recurring',
|
|
496
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '0' }],
|
|
497
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'metered' }, // Metered not included in setup
|
|
498
|
+
pricing_type: 'dynamic',
|
|
499
|
+
},
|
|
500
|
+
quantity: 1,
|
|
501
|
+
},
|
|
502
|
+
];
|
|
503
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd');
|
|
504
|
+
// Base: 0 (metered), No slippage applied
|
|
505
|
+
expect(result.amount.setup).toBe('0');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('should handle large amounts with slippage correctly', () => {
|
|
509
|
+
const items = [
|
|
510
|
+
{
|
|
511
|
+
price: {
|
|
512
|
+
type: 'recurring',
|
|
513
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '10000000' }], // 10M
|
|
514
|
+
recurring: { interval: 'year', interval_count: '1', usage_type: 'licensed' },
|
|
515
|
+
pricing_type: 'dynamic',
|
|
516
|
+
},
|
|
517
|
+
quantity: 1,
|
|
518
|
+
},
|
|
519
|
+
];
|
|
520
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd');
|
|
521
|
+
// Base: 10,000,000, With 0.5% slippage: 10,000,000 * 1.005 = 10,050,000
|
|
522
|
+
expect(result.amount.setup).toBe('10050000');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// Tests for custom slippage percentage parameter
|
|
526
|
+
it('should apply custom 5% slippage for dynamic pricing', () => {
|
|
527
|
+
const items = [
|
|
528
|
+
{
|
|
529
|
+
price: {
|
|
530
|
+
type: 'recurring',
|
|
531
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
532
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
533
|
+
pricing_type: 'dynamic',
|
|
534
|
+
},
|
|
535
|
+
quantity: 1,
|
|
536
|
+
},
|
|
537
|
+
];
|
|
538
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd', 0, 0, 5);
|
|
539
|
+
// Base: 1000, With 5% slippage: 1000 * 1.05 = 1050
|
|
540
|
+
expect(result.amount.setup).toBe('1050');
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('should apply custom 10% slippage for dynamic pricing', () => {
|
|
544
|
+
const items = [
|
|
545
|
+
{
|
|
546
|
+
price: {
|
|
547
|
+
type: 'recurring',
|
|
548
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
549
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
550
|
+
pricing_type: 'dynamic',
|
|
551
|
+
},
|
|
552
|
+
quantity: 1,
|
|
553
|
+
},
|
|
554
|
+
];
|
|
555
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd', 0, 0, 10);
|
|
556
|
+
// Base: 1000, With 10% slippage: 1000 * 1.10 = 1100
|
|
557
|
+
expect(result.amount.setup).toBe('1100');
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('should apply zero slippage when slippagePercent is 0', () => {
|
|
561
|
+
const items = [
|
|
562
|
+
{
|
|
563
|
+
price: {
|
|
564
|
+
type: 'recurring',
|
|
565
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
566
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
567
|
+
pricing_type: 'dynamic',
|
|
568
|
+
},
|
|
569
|
+
quantity: 1,
|
|
570
|
+
},
|
|
571
|
+
];
|
|
572
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd', 0, 0, 0);
|
|
573
|
+
// Base: 1000, With 0% slippage: 1000 * 1.00 = 1000
|
|
574
|
+
expect(result.amount.setup).toBe('1000');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('should fallback to 0.5% slippage when slippagePercent is negative', () => {
|
|
578
|
+
const items = [
|
|
579
|
+
{
|
|
580
|
+
price: {
|
|
581
|
+
type: 'recurring',
|
|
582
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
583
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
584
|
+
pricing_type: 'dynamic',
|
|
585
|
+
},
|
|
586
|
+
quantity: 1,
|
|
587
|
+
},
|
|
588
|
+
];
|
|
589
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd', 0, 0, -1);
|
|
590
|
+
// Fallback to 0.5%: 1000 * 1.005 = 1005
|
|
591
|
+
expect(result.amount.setup).toBe('1005');
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('should fallback to 0.5% slippage when slippagePercent is NaN', () => {
|
|
595
|
+
const items = [
|
|
596
|
+
{
|
|
597
|
+
price: {
|
|
598
|
+
type: 'recurring',
|
|
599
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
600
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
601
|
+
pricing_type: 'dynamic',
|
|
602
|
+
},
|
|
603
|
+
quantity: 1,
|
|
604
|
+
},
|
|
605
|
+
];
|
|
606
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd', 0, 0, NaN);
|
|
607
|
+
// Fallback to 0.5%: 1000 * 1.005 = 1005
|
|
608
|
+
expect(result.amount.setup).toBe('1005');
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('should handle fractional slippage percentages (1.25%)', () => {
|
|
612
|
+
const items = [
|
|
613
|
+
{
|
|
614
|
+
price: {
|
|
615
|
+
type: 'recurring',
|
|
616
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
617
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
618
|
+
pricing_type: 'dynamic',
|
|
619
|
+
},
|
|
620
|
+
quantity: 1,
|
|
621
|
+
},
|
|
622
|
+
];
|
|
623
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd', 0, 0, 1.25);
|
|
624
|
+
// Base: 1000, With 1.25% slippage: multiplier = Math.round(1000 + 12.5) = 1013
|
|
625
|
+
// 1000 * 1013 / 1000 = 1013
|
|
626
|
+
expect(result.amount.setup).toBe('1013');
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('should NOT apply custom slippage for static pricing', () => {
|
|
630
|
+
const items = [
|
|
631
|
+
{
|
|
632
|
+
price: {
|
|
633
|
+
type: 'recurring',
|
|
634
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
635
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
636
|
+
pricing_type: 'fixed',
|
|
637
|
+
},
|
|
638
|
+
quantity: 1,
|
|
639
|
+
},
|
|
640
|
+
];
|
|
641
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd', 0, 0, 10);
|
|
642
|
+
// Static pricing: No slippage applied regardless of parameter
|
|
643
|
+
expect(result.amount.setup).toBe('1000');
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
// Tests for SlippageOptions object with minAcceptableRate
|
|
647
|
+
it('should accept SlippageOptions object with percent', () => {
|
|
648
|
+
const items = [
|
|
649
|
+
{
|
|
650
|
+
price: {
|
|
651
|
+
type: 'recurring',
|
|
652
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
653
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
654
|
+
pricing_type: 'dynamic',
|
|
655
|
+
},
|
|
656
|
+
quantity: 1,
|
|
657
|
+
},
|
|
658
|
+
];
|
|
659
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd', 0, 0, { percent: 5 });
|
|
660
|
+
// Base: 1000, With 5% slippage: 1000 * 1.05 = 1050
|
|
661
|
+
expect(result.amount.setup).toBe('1050');
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('should calculate authorization using minAcceptableRate when provided', () => {
|
|
665
|
+
const items = [
|
|
666
|
+
{
|
|
667
|
+
price: {
|
|
668
|
+
type: 'recurring',
|
|
669
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
670
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
671
|
+
pricing_type: 'dynamic',
|
|
672
|
+
base_amount: '100', // $100 USD
|
|
673
|
+
},
|
|
674
|
+
quantity: 1,
|
|
675
|
+
},
|
|
676
|
+
];
|
|
677
|
+
// min_acceptable_rate = 0.5 means 1 token = $0.5 USD
|
|
678
|
+
// For $100 USD, we need: 100 / 0.5 = 200 tokens
|
|
679
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd', 0, 0, {
|
|
680
|
+
percent: 5, // Should be ignored when minAcceptableRate is provided
|
|
681
|
+
minAcceptableRate: '0.5',
|
|
682
|
+
currencyDecimal: 0, // Simplified for testing
|
|
683
|
+
});
|
|
684
|
+
expect(result.amount.setup).toBe('200');
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('should use ceiling division for minAcceptableRate calculation', () => {
|
|
688
|
+
const items = [
|
|
689
|
+
{
|
|
690
|
+
price: {
|
|
691
|
+
type: 'recurring',
|
|
692
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
693
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
694
|
+
pricing_type: 'dynamic',
|
|
695
|
+
base_amount: '100', // $100 USD
|
|
696
|
+
},
|
|
697
|
+
quantity: 1,
|
|
698
|
+
},
|
|
699
|
+
];
|
|
700
|
+
// min_acceptable_rate = 0.3 means 1 token = $0.3 USD
|
|
701
|
+
// For $100 USD, we need: ceil(100 / 0.3) = ceil(333.33) = 334 tokens
|
|
702
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd', 0, 0, {
|
|
703
|
+
minAcceptableRate: '0.3',
|
|
704
|
+
currencyDecimal: 0,
|
|
705
|
+
});
|
|
706
|
+
expect(result.amount.setup).toBe('334');
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('should handle multiple dynamic pricing items with minAcceptableRate', () => {
|
|
710
|
+
const items = [
|
|
711
|
+
{
|
|
712
|
+
price: {
|
|
713
|
+
type: 'recurring',
|
|
714
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '500' }],
|
|
715
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
716
|
+
pricing_type: 'dynamic',
|
|
717
|
+
base_amount: '50', // $50 USD
|
|
718
|
+
},
|
|
719
|
+
quantity: 2, // Total: $100 USD
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
price: {
|
|
723
|
+
type: 'recurring',
|
|
724
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '250' }],
|
|
725
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
726
|
+
pricing_type: 'dynamic',
|
|
727
|
+
base_amount: '25', // $25 USD
|
|
728
|
+
},
|
|
729
|
+
quantity: 2, // Total: $50 USD
|
|
730
|
+
},
|
|
731
|
+
];
|
|
732
|
+
// Total base_amount: $100 + $50 = $150 USD
|
|
733
|
+
// min_acceptable_rate = 0.5 means 1 token = $0.5 USD
|
|
734
|
+
// For $150 USD, we need: 150 / 0.5 = 300 tokens
|
|
735
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd', 0, 0, {
|
|
736
|
+
minAcceptableRate: '0.5',
|
|
737
|
+
currencyDecimal: 0,
|
|
738
|
+
});
|
|
739
|
+
expect(result.amount.setup).toBe('300');
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it('should fallback to percent when minAcceptableRate is missing', () => {
|
|
743
|
+
const items = [
|
|
744
|
+
{
|
|
745
|
+
price: {
|
|
746
|
+
type: 'recurring',
|
|
747
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
748
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
749
|
+
pricing_type: 'dynamic',
|
|
750
|
+
base_amount: '100',
|
|
751
|
+
},
|
|
752
|
+
quantity: 1,
|
|
753
|
+
},
|
|
754
|
+
];
|
|
755
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd', 0, 0, {
|
|
756
|
+
percent: 10,
|
|
757
|
+
// minAcceptableRate is undefined
|
|
758
|
+
currencyDecimal: 0,
|
|
759
|
+
});
|
|
760
|
+
// Fallback to percent: 1000 * 1.10 = 1100
|
|
761
|
+
expect(result.amount.setup).toBe('1100');
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it('should fallback to percent when currencyDecimal is missing', () => {
|
|
765
|
+
const items = [
|
|
766
|
+
{
|
|
767
|
+
price: {
|
|
768
|
+
type: 'recurring',
|
|
769
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
770
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
771
|
+
pricing_type: 'dynamic',
|
|
772
|
+
base_amount: '100',
|
|
773
|
+
},
|
|
774
|
+
quantity: 1,
|
|
775
|
+
},
|
|
776
|
+
];
|
|
777
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd', 0, 0, {
|
|
778
|
+
percent: 5,
|
|
779
|
+
minAcceptableRate: '0.5',
|
|
780
|
+
// currencyDecimal is undefined
|
|
781
|
+
});
|
|
782
|
+
// Fallback to percent: 1000 * 1.05 = 1050
|
|
783
|
+
expect(result.amount.setup).toBe('1050');
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
it('should fallback to percent when minAcceptableRate is zero', () => {
|
|
787
|
+
const items = [
|
|
788
|
+
{
|
|
789
|
+
price: {
|
|
790
|
+
type: 'recurring',
|
|
791
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
792
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
793
|
+
pricing_type: 'dynamic',
|
|
794
|
+
base_amount: '100',
|
|
795
|
+
},
|
|
796
|
+
quantity: 1,
|
|
797
|
+
},
|
|
798
|
+
];
|
|
799
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd', 0, 0, {
|
|
800
|
+
percent: 5,
|
|
801
|
+
minAcceptableRate: '0',
|
|
802
|
+
currencyDecimal: 0,
|
|
803
|
+
});
|
|
804
|
+
// Fallback to percent: 1000 * 1.05 = 1050
|
|
805
|
+
expect(result.amount.setup).toBe('1050');
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it('should fallback to default 0.5% when SlippageOptions is empty object', () => {
|
|
809
|
+
const items = [
|
|
810
|
+
{
|
|
811
|
+
price: {
|
|
812
|
+
type: 'recurring',
|
|
813
|
+
currency_options: [{ currency_id: 'usd', unit_amount: '1000' }],
|
|
814
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
815
|
+
pricing_type: 'dynamic',
|
|
816
|
+
},
|
|
817
|
+
quantity: 1,
|
|
818
|
+
},
|
|
819
|
+
];
|
|
820
|
+
const result = getSubscriptionCreateSetup(items as any, 'usd', 0, 0, {});
|
|
821
|
+
// Fallback to default 0.5%: 1000 * 1.005 = 1005
|
|
822
|
+
expect(result.amount.setup).toBe('1005');
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it('should handle minAcceptableRate with higher precision currencyDecimal', () => {
|
|
826
|
+
const items = [
|
|
827
|
+
{
|
|
828
|
+
price: {
|
|
829
|
+
type: 'recurring',
|
|
830
|
+
currency_options: [{ currency_id: 'eth', unit_amount: '100000000000000000' }], // 0.1 ETH in wei
|
|
831
|
+
recurring: { interval: 'month', interval_count: '1', usage_type: 'licensed' },
|
|
832
|
+
pricing_type: 'dynamic',
|
|
833
|
+
base_amount: '100', // $100 USD
|
|
834
|
+
},
|
|
835
|
+
quantity: 1,
|
|
836
|
+
},
|
|
837
|
+
];
|
|
838
|
+
// min_acceptable_rate = 1000 means 1 token (wei) = $1000 USD
|
|
839
|
+
// But we have 18 decimals, so 1 ETH = $1000 * 10^18 / 10^18 = $1000
|
|
840
|
+
// For $100 USD with currencyDecimal=18:
|
|
841
|
+
// authorization = ceil(100 * 10^8 * 10^18 / (1000 * 10^8))
|
|
842
|
+
// = ceil(10^28 / 10^11) = ceil(10^17) = 100000000000000000 (0.1 ETH)
|
|
843
|
+
const result = getSubscriptionCreateSetup(items as any, 'eth', 0, 0, {
|
|
844
|
+
minAcceptableRate: '1000',
|
|
845
|
+
currencyDecimal: 18,
|
|
846
|
+
});
|
|
847
|
+
expect(result.amount.setup).toBe('100000000000000000');
|
|
848
|
+
});
|
|
849
|
+
});
|
|
386
850
|
});
|
|
387
851
|
|
|
388
852
|
describe('getCheckoutSessionSubscriptionIds', () => {
|