payment-kit 1.20.10 → 1.20.12

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 (83) hide show
  1. package/README.md +25 -24
  2. package/api/src/index.ts +2 -0
  3. package/api/src/integrations/stripe/handlers/invoice.ts +63 -5
  4. package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -0
  5. package/api/src/integrations/stripe/resource.ts +253 -2
  6. package/api/src/libs/currency.ts +31 -0
  7. package/api/src/libs/discount/coupon.ts +1061 -0
  8. package/api/src/libs/discount/discount.ts +349 -0
  9. package/api/src/libs/discount/nft.ts +239 -0
  10. package/api/src/libs/discount/redemption.ts +636 -0
  11. package/api/src/libs/discount/vc.ts +73 -0
  12. package/api/src/libs/invoice.ts +50 -16
  13. package/api/src/libs/math-utils.ts +6 -0
  14. package/api/src/libs/price.ts +43 -0
  15. package/api/src/libs/session.ts +242 -57
  16. package/api/src/libs/subscription.ts +2 -6
  17. package/api/src/locales/en.ts +38 -38
  18. package/api/src/queues/auto-recharge.ts +1 -1
  19. package/api/src/queues/discount-status.ts +200 -0
  20. package/api/src/queues/subscription.ts +98 -5
  21. package/api/src/queues/usage-record.ts +1 -1
  22. package/api/src/routes/auto-recharge-configs.ts +5 -3
  23. package/api/src/routes/checkout-sessions.ts +755 -64
  24. package/api/src/routes/connect/change-payment.ts +6 -1
  25. package/api/src/routes/connect/change-plan.ts +6 -1
  26. package/api/src/routes/connect/setup.ts +6 -1
  27. package/api/src/routes/connect/shared.ts +80 -9
  28. package/api/src/routes/connect/subscribe.ts +12 -2
  29. package/api/src/routes/coupons.ts +518 -0
  30. package/api/src/routes/index.ts +4 -0
  31. package/api/src/routes/invoices.ts +44 -3
  32. package/api/src/routes/meter-events.ts +2 -1
  33. package/api/src/routes/payment-currencies.ts +1 -0
  34. package/api/src/routes/promotion-codes.ts +482 -0
  35. package/api/src/routes/subscriptions.ts +23 -2
  36. package/api/src/store/migrations/20250904-discount.ts +136 -0
  37. package/api/src/store/migrations/20250910-timestamp-fields.ts +116 -0
  38. package/api/src/store/migrations/20250916-add-description-fields.ts +30 -0
  39. package/api/src/store/models/checkout-session.ts +12 -0
  40. package/api/src/store/models/coupon.ts +144 -4
  41. package/api/src/store/models/discount.ts +23 -10
  42. package/api/src/store/models/index.ts +13 -2
  43. package/api/src/store/models/promotion-code.ts +295 -18
  44. package/api/src/store/models/types.ts +30 -1
  45. package/api/tests/libs/session.spec.ts +48 -27
  46. package/blocklet.yml +1 -1
  47. package/doc/vendor_fulfillment_system.md +38 -38
  48. package/package.json +20 -20
  49. package/src/app.tsx +2 -0
  50. package/src/components/customer/link.tsx +1 -1
  51. package/src/components/discount/discount-info.tsx +178 -0
  52. package/src/components/invoice/table.tsx +140 -48
  53. package/src/components/invoice-pdf/styles.ts +6 -0
  54. package/src/components/invoice-pdf/template.tsx +59 -33
  55. package/src/components/metadata/form.tsx +14 -5
  56. package/src/components/payment-link/actions.tsx +42 -0
  57. package/src/components/price/form.tsx +91 -65
  58. package/src/components/product/vendor-config.tsx +5 -3
  59. package/src/components/promotion/active-redemptions.tsx +534 -0
  60. package/src/components/promotion/currency-multi-select.tsx +350 -0
  61. package/src/components/promotion/currency-restrictions.tsx +117 -0
  62. package/src/components/promotion/product-select.tsx +292 -0
  63. package/src/components/promotion/promotion-code-form.tsx +534 -0
  64. package/src/components/subscription/portal/list.tsx +6 -1
  65. package/src/components/subscription/vendor-service-list.tsx +13 -2
  66. package/src/locales/en.tsx +253 -26
  67. package/src/locales/zh.tsx +222 -1
  68. package/src/pages/admin/billing/subscriptions/detail.tsx +5 -0
  69. package/src/pages/admin/products/coupons/applicable-products.tsx +166 -0
  70. package/src/pages/admin/products/coupons/create.tsx +612 -0
  71. package/src/pages/admin/products/coupons/detail.tsx +538 -0
  72. package/src/pages/admin/products/coupons/edit.tsx +127 -0
  73. package/src/pages/admin/products/coupons/index.tsx +210 -3
  74. package/src/pages/admin/products/index.tsx +22 -3
  75. package/src/pages/admin/products/products/detail.tsx +12 -2
  76. package/src/pages/admin/products/promotion-codes/actions.tsx +103 -0
  77. package/src/pages/admin/products/promotion-codes/create.tsx +235 -0
  78. package/src/pages/admin/products/promotion-codes/detail.tsx +416 -0
  79. package/src/pages/admin/products/promotion-codes/list.tsx +247 -0
  80. package/src/pages/admin/products/promotion-codes/verification-config.tsx +327 -0
  81. package/src/pages/admin/products/vendors/index.tsx +17 -5
  82. package/src/pages/customer/subscription/detail.tsx +5 -0
  83. package/vite.config.ts +4 -3
@@ -0,0 +1,482 @@
1
+ /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/require-await */
2
+ import { Router } from 'express';
3
+ import Joi from 'joi';
4
+ import pick from 'lodash/pick';
5
+
6
+ import { createIdGenerator, formatMetadata } from '../libs/util';
7
+
8
+ import { authenticate } from '../libs/security';
9
+ import { PromotionCode, Coupon, PaymentCurrency } from '../store/models';
10
+ import { getRedemptionData } from '../libs/discount/redemption';
11
+ import { createListParamSchema } from '../libs/api';
12
+ import logger from '../libs/logger';
13
+
14
+ const router = Router();
15
+ const authAdmin = authenticate({ component: true, roles: ['owner', 'admin'] });
16
+
17
+ // Get expanded promotion code with all related information
18
+ export async function getExpandedPromotionCode(id: string) {
19
+ const promotionCode = await PromotionCode.findByPk(id, {
20
+ include: [{ model: Coupon, as: 'coupon' }],
21
+ });
22
+
23
+ if (promotionCode) {
24
+ return promotionCode.toJSON();
25
+ }
26
+
27
+ return null;
28
+ }
29
+
30
+ // Create promotion code with full validation
31
+ export async function createPromotionCode(payload: any) {
32
+ // 1. Prepare promotion code data
33
+ const promotionCodeData = pick(payload, [
34
+ 'id',
35
+ 'coupon_id',
36
+ 'code',
37
+ 'description',
38
+ 'active',
39
+ 'max_redemptions',
40
+ 'expires_at',
41
+ 'verification_type',
42
+ 'nft_config',
43
+ 'vc_config',
44
+ 'customer_dids',
45
+ 'restrictions',
46
+ 'metadata',
47
+ ]);
48
+
49
+ (promotionCodeData as any).livemode = !!payload.livemode;
50
+ (promotionCodeData as any).created_via = payload.created_via || 'api';
51
+ promotionCodeData.metadata = formatMetadata(promotionCodeData.metadata);
52
+
53
+ // 2. Validate that coupon exists
54
+ const coupon = await Coupon.findOne({
55
+ where: { id: promotionCodeData.coupon_id, livemode: !!(promotionCodeData as any).livemode },
56
+ });
57
+ if (!coupon) {
58
+ throw new Error(`Coupon ${promotionCodeData.coupon_id} not found`);
59
+ }
60
+
61
+ // 3. Generate code if not provided
62
+ if (!promotionCodeData.code) {
63
+ promotionCodeData.code = createIdGenerator('', 8)();
64
+ }
65
+
66
+ // 4. Ensure code is unique
67
+ const existingCode = await PromotionCode.findOne({
68
+ where: { code: promotionCodeData.code, livemode: !!(promotionCodeData as any).livemode },
69
+ });
70
+ if (existingCode) {
71
+ throw new Error('Promotion code already exists');
72
+ }
73
+
74
+ // 4.5. Format restrictions currency_options if provided
75
+ if ((promotionCodeData as any).restrictions) {
76
+ const allCurrencies = await PaymentCurrency.findAll();
77
+ (promotionCodeData as any).restrictions = PromotionCode.formatRestrictionsCurrencyOptions(
78
+ (promotionCodeData as any).restrictions,
79
+ allCurrencies
80
+ );
81
+ }
82
+
83
+ // 5. Create promotion code
84
+ const promotionCode = await PromotionCode.insert(promotionCodeData);
85
+
86
+ return promotionCode;
87
+ }
88
+
89
+ /**
90
+ * POST /api/promotion-codes
91
+ * Create a new promotion code
92
+ */
93
+ router.post('/', authAdmin, async (req, res) => {
94
+ try {
95
+ const schema = Joi.object({
96
+ coupon_id: Joi.string().required(),
97
+ code: Joi.string().optional().max(16),
98
+ description: Joi.string().empty('').max(250).optional(),
99
+ active: Joi.boolean().default(true),
100
+ max_redemptions: Joi.number().integer().positive().optional(),
101
+ expires_at: Joi.number().integer().min(0).optional(),
102
+ verification_type: Joi.string().valid('code', 'nft', 'vc', 'user_restricted').default('code'),
103
+ nft_config: Joi.object({
104
+ addresses: Joi.array().items(Joi.string()).optional(),
105
+ tags: Joi.array().items(Joi.string()).optional(),
106
+ trusted_issuers: Joi.array().items(Joi.string()).optional(),
107
+ trusted_parents: Joi.array().items(Joi.string()).optional(),
108
+ min_balance: Joi.number().integer().positive().default(1),
109
+ }).optional(),
110
+ vc_config: Joi.object({
111
+ roles: Joi.array().items(Joi.string()).optional(),
112
+ trusted_issuers: Joi.array().items(Joi.string()).optional(),
113
+ }).optional(),
114
+ customer_dids: Joi.array().items(Joi.string()).optional(),
115
+ restrictions: Joi.object({
116
+ currency_options: Joi.object()
117
+ .pattern(
118
+ Joi.string(),
119
+ Joi.object({
120
+ minimum_amount: Joi.number().min(0).required(),
121
+ })
122
+ )
123
+ .optional(),
124
+ first_time_transaction: Joi.boolean().optional(),
125
+ minimum_amount: Joi.number().positive().optional(),
126
+ minimum_amount_currency: Joi.string().optional(),
127
+ require_minimum_amount: Joi.boolean().optional(),
128
+ })
129
+ .optional()
130
+ .unknown(true),
131
+ metadata: Joi.object().optional(),
132
+ });
133
+
134
+ const { error, value } = schema.validate(req.body);
135
+ if (error) {
136
+ return res.status(400).json({
137
+ error: 'Invalid request',
138
+ details: error.details?.[0]?.message || 'Validation error',
139
+ });
140
+ }
141
+
142
+ const promotionCode = await createPromotionCode({
143
+ ...value,
144
+ livemode: req.livemode,
145
+ created_via: req.user?.via || 'api',
146
+ });
147
+
148
+ logger.info('Promotion code created', {
149
+ promotionCodeId: promotionCode.id,
150
+ code: promotionCode.code,
151
+ requestedBy: req.user?.did,
152
+ });
153
+
154
+ const doc = await getExpandedPromotionCode(promotionCode.id as string);
155
+ return res.json(doc);
156
+ } catch (error) {
157
+ logger.error('Error creating promotion code', {
158
+ error: error.message,
159
+ body: req.body,
160
+ });
161
+ return res.status(400).json({ error: error.message });
162
+ }
163
+ });
164
+
165
+ /**
166
+ * GET /api/promotion-codes
167
+ * List all promotion codes with pagination
168
+ */
169
+ router.get('/', authAdmin, async (req, res) => {
170
+ const schema = Joi.object({
171
+ page: Joi.number().integer().positive().default(1),
172
+ pageSize: Joi.number().integer().positive().max(100).default(20),
173
+ coupon_id: Joi.string().optional(),
174
+ active: Joi.boolean().optional(),
175
+ });
176
+
177
+ const { error, value } = schema.validate(req.query, {
178
+ stripUnknown: false,
179
+ allowUnknown: true,
180
+ });
181
+ if (error) {
182
+ return res.status(400).json({
183
+ error: 'Invalid request',
184
+ details: error.details?.[0]?.message || 'Validation error',
185
+ });
186
+ }
187
+
188
+ const { page, pageSize, coupon_id, active } = value;
189
+ const where: any = { livemode: req.livemode };
190
+
191
+ if (coupon_id) {
192
+ where.coupon_id = coupon_id;
193
+ }
194
+
195
+ if (active !== undefined) {
196
+ where.active = active;
197
+ }
198
+
199
+ const { count, rows } = await PromotionCode.findAndCountAll({
200
+ where,
201
+ include: [{ model: Coupon, as: 'coupon' }],
202
+ limit: pageSize,
203
+ offset: (page - 1) * pageSize,
204
+ order: [['created_at', 'DESC']],
205
+ });
206
+
207
+ return res.json({
208
+ count,
209
+ list: rows.map((promotionCode) => promotionCode.toJSON()),
210
+ paging: { page, pageSize },
211
+ });
212
+ });
213
+
214
+ /**
215
+ * GET /api/promotion-codes/:id
216
+ * Get promotion code details by ID (admin only)
217
+ */
218
+ router.get('/:id', authAdmin, async (req, res) => {
219
+ const promotionCode = await getExpandedPromotionCode(req.params.id as string);
220
+
221
+ if (!promotionCode) {
222
+ return res.status(404).json({ error: 'Promotion code not found' });
223
+ }
224
+
225
+ return res.json(promotionCode);
226
+ });
227
+
228
+ /**
229
+ * PUT /api/promotion-codes/:id
230
+ * Update a promotion code (limited fields can be updated)
231
+ */
232
+ router.put('/:id', authAdmin, async (req, res) => {
233
+ try {
234
+ const schema = Joi.object({
235
+ description: Joi.string().empty('').max(250).optional(),
236
+ active: Joi.boolean().optional(),
237
+ max_redemptions: Joi.number().integer().positive().optional(),
238
+ expires_at: Joi.number().integer().min(0).optional(),
239
+ restrictions: Joi.object({
240
+ currency_options: Joi.object()
241
+ .pattern(
242
+ Joi.string(),
243
+ Joi.object({
244
+ minimum_amount: Joi.number().min(0).required(),
245
+ })
246
+ )
247
+ .optional(),
248
+ first_time_transaction: Joi.boolean().optional(),
249
+ minimum_amount: Joi.number().positive().optional(),
250
+ minimum_amount_currency: Joi.string().optional(),
251
+ }).optional(),
252
+ metadata: Joi.object().optional(),
253
+ });
254
+
255
+ const { error, value } = schema.validate(req.body, {
256
+ stripUnknown: true,
257
+ });
258
+ if (error) {
259
+ return res.status(400).json({
260
+ error: 'Invalid request',
261
+ details: error.details?.[0]?.message || 'Validation error',
262
+ });
263
+ }
264
+
265
+ const promotionCode = await PromotionCode.findOne({
266
+ where: { id: req.params.id },
267
+ });
268
+
269
+ if (!promotionCode) {
270
+ return res.status(404).json({ error: 'Promotion code not found' });
271
+ }
272
+
273
+ if (promotionCode.locked) {
274
+ return res.status(403).json({ error: 'Promotion code is locked and cannot be modified' });
275
+ }
276
+
277
+ if (!promotionCode.active) {
278
+ return res.status(403).json({ error: 'Promotion code is invalid and cannot be modified' });
279
+ }
280
+
281
+ // Format restrictions currency_options if provided
282
+ if (value.restrictions) {
283
+ const allCurrencies = await PaymentCurrency.findAll();
284
+ value.restrictions = PromotionCode.formatRestrictionsCurrencyOptions(value.restrictions, allCurrencies);
285
+ }
286
+
287
+ // Check if promotion code is being used
288
+ if (await promotionCode.isUsed()) {
289
+ // Lock the promotion code and only allow limited updates
290
+ await promotionCode.update({ locked: true });
291
+ const allowedUpdates = pick(value, ['metadata']);
292
+ if (Object.keys(allowedUpdates).length === 0) {
293
+ return res.status(403).json({
294
+ error: 'Promotion code is being used. Only metadata can be updated.',
295
+ });
296
+ }
297
+ await promotionCode.update(PromotionCode.formatBeforeSave(allowedUpdates));
298
+ } else {
299
+ await promotionCode.update(PromotionCode.formatBeforeSave(value));
300
+ }
301
+
302
+ logger.info('Promotion code updated', {
303
+ promotionCodeId: req.params.id,
304
+ updatedFields: Object.keys(value),
305
+ requestedBy: req.user?.did,
306
+ });
307
+
308
+ const doc = await getExpandedPromotionCode(req.params.id as string);
309
+ return res.json(doc);
310
+ } catch (error) {
311
+ logger.error('Error updating promotion code', {
312
+ error: error.message,
313
+ id: req.params.id,
314
+ body: req.body,
315
+ });
316
+ return res.status(400).json({ error: error.message });
317
+ }
318
+ });
319
+
320
+ /**
321
+ * PUT /api/promotion-codes/:id/archive
322
+ * Archive a promotion code (set active to false, making it unusable)
323
+ */
324
+ router.put('/:id/archive', authAdmin, async (req, res) => {
325
+ try {
326
+ const promotionCode = await PromotionCode.findOne({
327
+ where: { id: req.params.id, livemode: req.livemode },
328
+ });
329
+
330
+ if (!promotionCode) {
331
+ return res.status(404).json({ error: 'Promotion code not found' });
332
+ }
333
+
334
+ if (!promotionCode.active) {
335
+ return res.status(400).json({ error: 'Promotion code is already archived' });
336
+ }
337
+
338
+ // Archive the promotion code by setting active to false
339
+ await promotionCode.update({ active: false });
340
+
341
+ logger.info('Promotion code archived', {
342
+ promotionCodeId: req.params.id,
343
+ requestedBy: req.user?.did,
344
+ });
345
+ return res.json(promotionCode);
346
+ } catch (error) {
347
+ logger.error('Error archiving promotion code', {
348
+ error: error.message,
349
+ id: req.params.id,
350
+ });
351
+ return res.status(400).json({ error: error.message });
352
+ }
353
+ });
354
+
355
+ /**
356
+ * DELETE /api/promotion-codes/:id
357
+ * Delete a promotion code (mark as inactive if used, otherwise hard delete)
358
+ */
359
+ router.delete('/:id', authAdmin, async (req, res) => {
360
+ try {
361
+ const promotionCode = await PromotionCode.findOne({
362
+ where: { id: req.params.id, livemode: req.livemode },
363
+ });
364
+
365
+ if (!promotionCode) {
366
+ return res.status(404).json({ error: 'Promotion code not found' });
367
+ }
368
+
369
+ if (promotionCode.locked) {
370
+ return res.status(403).json({ error: 'Promotion code is locked and cannot be deleted' });
371
+ }
372
+
373
+ // Check if promotion code is being used
374
+ if (await promotionCode.isUsed()) {
375
+ // Mark as inactive instead of deleting
376
+ await promotionCode.update({ locked: true });
377
+ return res.status(403).json({ error: 'Promotion code is locked and cannot be deleted' });
378
+ }
379
+
380
+ // Hard delete if not used
381
+ await promotionCode.destroy();
382
+
383
+ logger.info('Promotion code deleted', {
384
+ promotionCodeId: req.params.id,
385
+ requestedBy: req.user?.did,
386
+ });
387
+
388
+ return res.json(promotionCode);
389
+ } catch (error) {
390
+ logger.error('Error deleting promotion code', {
391
+ error: error.message,
392
+ id: req.params.id,
393
+ });
394
+ return res.status(400).json({ error: error.message });
395
+ }
396
+ });
397
+
398
+ /**
399
+ * GET /api/promotion-codes/:id/used
400
+ * Check if a promotion code is being used
401
+ */
402
+ router.get('/:id/used', authAdmin, async (req, res) => {
403
+ const promotionCode = await PromotionCode.findOne({
404
+ where: { id: req.params.id, livemode: req.livemode },
405
+ });
406
+
407
+ if (!promotionCode) {
408
+ return res.status(404).json({ error: 'Promotion code not found' });
409
+ }
410
+
411
+ const used = await promotionCode.isUsed();
412
+ if (!used) {
413
+ await promotionCode.update({ locked: false });
414
+ }
415
+
416
+ return res.json({ used });
417
+ });
418
+
419
+ /**
420
+ * GET /api/promotion-codes/by-code/:code
421
+ * Get promotion code details by code (admin only)
422
+ */
423
+ router.get('/by-code/:code', authAdmin, async (req, res) => {
424
+ const { code } = req.params;
425
+ if (!code) {
426
+ return res.status(400).json({ error: 'Code parameter is required' });
427
+ }
428
+
429
+ const promotionCode = await PromotionCode.findByCode(code.toLowerCase());
430
+
431
+ if (!promotionCode) {
432
+ return res.status(404).json({ error: 'Promotion code not found' });
433
+ }
434
+ const doc = await getExpandedPromotionCode(promotionCode.id as string);
435
+
436
+ return res.json(doc);
437
+ });
438
+
439
+ // Create redemptions pagination schema for promotion codes
440
+ const promotionCodeRedemptionsSchema = createListParamSchema<{
441
+ type?: string;
442
+ }>({
443
+ type: Joi.string().valid('customer', 'subscription').optional(),
444
+ });
445
+
446
+ /**
447
+ * GET /api/promotion-codes/:id/redemptions
448
+ * Get active redemptions for a promotion code with detailed admin analytics
449
+ */
450
+ router.get('/:id/redemptions', authAdmin, async (req, res) => {
451
+ try {
452
+ const promotionCodeId = req.params.id as string;
453
+
454
+ const { page, pageSize, type } = await promotionCodeRedemptionsSchema.validateAsync(req.query, {
455
+ stripUnknown: false,
456
+ allowUnknown: true,
457
+ });
458
+
459
+ const promotionCode = await PromotionCode.findByPk(promotionCodeId);
460
+ if (!promotionCode) {
461
+ return res.status(404).json({ error: 'Promotion code not found' });
462
+ }
463
+
464
+ const result = await getRedemptionData(
465
+ { promotion_code_id: promotionCodeId },
466
+ { page, pageSize, type: type as 'customer' | 'subscription' | undefined },
467
+ promotionCode,
468
+ 'promotion_code'
469
+ );
470
+
471
+ return res.json(result);
472
+ } catch (error: any) {
473
+ logger.error('Error getting promotion code redemptions', {
474
+ error: error.message,
475
+ stack: error.stack,
476
+ promotionCodeId: req.params.id,
477
+ });
478
+ return res.status(500).json({ error: error.message });
479
+ }
480
+ });
481
+
482
+ export default router;
@@ -65,6 +65,7 @@ import { SubscriptionWillCanceledSchedule } from '../crons/subscription-will-can
65
65
  import { getTokenByAddress } from '../integrations/arcblock/stake';
66
66
  import { ensureOverdraftProtectionPrice } from '../libs/overdraft-protection';
67
67
  import { CHARGE_SUPPORTED_CHAIN_TYPES } from '../libs/constants';
68
+ import { getSubscriptionDiscountStats } from '../libs/discount/redemption';
68
69
 
69
70
  const router = Router();
70
71
  const auth = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
@@ -257,7 +258,22 @@ router.get('/:id', authPortal, async (req, res) => {
257
258
  expandLineItems(json.items, products, prices);
258
259
  // @ts-ignore
259
260
  json.serviceType = serviceType;
260
- res.json(json);
261
+
262
+ // Get discount statistics if subscription has active discounts
263
+ let discountStats = null;
264
+ try {
265
+ const stats = await getSubscriptionDiscountStats(json.id);
266
+ if (stats.total_discount_records > 0) {
267
+ discountStats = stats;
268
+ }
269
+ } catch (error) {
270
+ logger.error('Failed to fetch subscription discount stats', { error, subscriptionId: json.id });
271
+ }
272
+
273
+ res.json({
274
+ ...json,
275
+ discountStats,
276
+ });
261
277
  } else {
262
278
  res.status(404).json(null);
263
279
  }
@@ -1663,7 +1679,12 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
1663
1679
  paymentMethod,
1664
1680
  paymentCurrency,
1665
1681
  userDid: payer,
1666
- amount: getFastCheckoutAmount(lineItems, 'subscription', paymentCurrency.id, false),
1682
+ amount: await getFastCheckoutAmount({
1683
+ items: lineItems,
1684
+ mode: 'subscription',
1685
+ currencyId: paymentCurrency.id,
1686
+ trialing: false,
1687
+ }),
1667
1688
  });
1668
1689
  const noStake = subscription.billing_thresholds?.no_stake;
1669
1690
  if (paymentMethod.type === 'arcblock' && delegation.sufficient && !noStake) {
@@ -0,0 +1,136 @@
1
+ import { DataTypes } from 'sequelize';
2
+ import { createIndexIfNotExists, Migration, safeApplyColumnChanges } from '../migrate';
3
+
4
+ export const up: Migration = async ({ context }) => {
5
+ await safeApplyColumnChanges(context, {
6
+ checkout_sessions: [
7
+ {
8
+ name: 'discounts',
9
+ field: {
10
+ type: DataTypes.JSON,
11
+ allowNull: true,
12
+ },
13
+ },
14
+ ],
15
+ discounts: [
16
+ {
17
+ name: 'verification_method',
18
+ field: {
19
+ type: DataTypes.STRING(50),
20
+ allowNull: true,
21
+ },
22
+ },
23
+ {
24
+ name: 'verification_data',
25
+ field: {
26
+ type: DataTypes.JSON,
27
+ allowNull: true,
28
+ },
29
+ },
30
+ {
31
+ name: 'metadata',
32
+ field: {
33
+ type: DataTypes.JSON,
34
+ allowNull: true,
35
+ },
36
+ },
37
+ ],
38
+ promotion_codes: [
39
+ {
40
+ name: 'verification_type',
41
+ field: {
42
+ type: DataTypes.ENUM('code', 'nft', 'vc', 'user_restricted'),
43
+ defaultValue: 'code',
44
+ allowNull: false,
45
+ },
46
+ },
47
+ {
48
+ name: 'nft_config',
49
+ field: {
50
+ type: DataTypes.JSON,
51
+ allowNull: true,
52
+ },
53
+ },
54
+ {
55
+ name: 'vc_config',
56
+ field: {
57
+ type: DataTypes.JSON,
58
+ allowNull: true,
59
+ },
60
+ },
61
+ {
62
+ name: 'customer_dids',
63
+ field: {
64
+ type: DataTypes.JSON,
65
+ allowNull: true,
66
+ },
67
+ },
68
+ {
69
+ name: 'metadata',
70
+ field: {
71
+ type: DataTypes.JSON,
72
+ allowNull: true,
73
+ },
74
+ },
75
+ {
76
+ name: 'created_via',
77
+ field: {
78
+ type: DataTypes.ENUM('api', 'dashboard', 'portal'),
79
+ allowNull: true,
80
+ },
81
+ },
82
+ {
83
+ name: 'locked',
84
+ field: {
85
+ type: DataTypes.BOOLEAN,
86
+ allowNull: false,
87
+ defaultValue: false,
88
+ },
89
+ },
90
+ ],
91
+ coupons: [
92
+ {
93
+ name: 'created_via',
94
+ field: {
95
+ type: DataTypes.ENUM('api', 'dashboard', 'portal'),
96
+ allowNull: true,
97
+ },
98
+ },
99
+ {
100
+ name: 'locked',
101
+ field: {
102
+ type: DataTypes.BOOLEAN,
103
+ allowNull: false,
104
+ defaultValue: false,
105
+ },
106
+ },
107
+ ],
108
+ });
109
+
110
+ await createIndexIfNotExists(context, 'discounts', ['customer_id'], 'idx_discounts_customer_id');
111
+ await createIndexIfNotExists(
112
+ context,
113
+ 'promotion_codes',
114
+ ['verification_type', 'coupon_id'],
115
+ 'idx_promotion_codes_verification_type_coupon_id'
116
+ );
117
+ };
118
+
119
+ export const down: Migration = async ({ context }) => {
120
+ await context.removeIndex('discounts', 'idx_discounts_customer_id');
121
+ await context.removeIndex('promotion_codes', 'idx_promotion_codes_verification_type_coupon_id');
122
+
123
+ await context.removeColumn('checkout_sessions', 'discounts');
124
+ await context.removeColumn('discounts', 'verification_method');
125
+ await context.removeColumn('discounts', 'verification_data');
126
+ await context.removeColumn('discounts', 'metadata');
127
+ await context.removeColumn('promotion_codes', 'verification_type');
128
+ await context.removeColumn('promotion_codes', 'nft_config');
129
+ await context.removeColumn('promotion_codes', 'vc_config');
130
+ await context.removeColumn('promotion_codes', 'customer_dids');
131
+ await context.removeColumn('promotion_codes', 'metadata');
132
+ await context.removeColumn('promotion_codes', 'created_via');
133
+ await context.removeColumn('promotion_codes', 'locked');
134
+ await context.removeColumn('coupons', 'created_via');
135
+ await context.removeColumn('coupons', 'locked');
136
+ };