payment-kit 1.20.11 → 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 (80) hide show
  1. package/api/src/index.ts +2 -0
  2. package/api/src/integrations/stripe/handlers/invoice.ts +63 -5
  3. package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -0
  4. package/api/src/integrations/stripe/resource.ts +253 -2
  5. package/api/src/libs/currency.ts +31 -0
  6. package/api/src/libs/discount/coupon.ts +1061 -0
  7. package/api/src/libs/discount/discount.ts +349 -0
  8. package/api/src/libs/discount/nft.ts +239 -0
  9. package/api/src/libs/discount/redemption.ts +636 -0
  10. package/api/src/libs/discount/vc.ts +73 -0
  11. package/api/src/libs/invoice.ts +44 -10
  12. package/api/src/libs/math-utils.ts +6 -0
  13. package/api/src/libs/price.ts +43 -0
  14. package/api/src/libs/session.ts +242 -57
  15. package/api/src/libs/subscription.ts +2 -6
  16. package/api/src/queues/auto-recharge.ts +1 -1
  17. package/api/src/queues/discount-status.ts +200 -0
  18. package/api/src/queues/subscription.ts +98 -5
  19. package/api/src/queues/usage-record.ts +1 -1
  20. package/api/src/routes/auto-recharge-configs.ts +5 -3
  21. package/api/src/routes/checkout-sessions.ts +755 -64
  22. package/api/src/routes/connect/change-payment.ts +6 -1
  23. package/api/src/routes/connect/change-plan.ts +6 -1
  24. package/api/src/routes/connect/setup.ts +6 -1
  25. package/api/src/routes/connect/shared.ts +80 -9
  26. package/api/src/routes/connect/subscribe.ts +12 -2
  27. package/api/src/routes/coupons.ts +518 -0
  28. package/api/src/routes/index.ts +4 -0
  29. package/api/src/routes/invoices.ts +44 -3
  30. package/api/src/routes/meter-events.ts +2 -1
  31. package/api/src/routes/payment-currencies.ts +1 -0
  32. package/api/src/routes/promotion-codes.ts +482 -0
  33. package/api/src/routes/subscriptions.ts +23 -2
  34. package/api/src/store/migrations/20250904-discount.ts +136 -0
  35. package/api/src/store/migrations/20250910-timestamp-fields.ts +116 -0
  36. package/api/src/store/migrations/20250916-add-description-fields.ts +30 -0
  37. package/api/src/store/models/checkout-session.ts +12 -0
  38. package/api/src/store/models/coupon.ts +144 -4
  39. package/api/src/store/models/discount.ts +23 -10
  40. package/api/src/store/models/index.ts +13 -2
  41. package/api/src/store/models/promotion-code.ts +295 -18
  42. package/api/src/store/models/types.ts +30 -1
  43. package/api/tests/libs/session.spec.ts +48 -27
  44. package/blocklet.yml +1 -1
  45. package/package.json +20 -20
  46. package/src/app.tsx +2 -0
  47. package/src/components/customer/link.tsx +1 -1
  48. package/src/components/discount/discount-info.tsx +178 -0
  49. package/src/components/invoice/table.tsx +140 -48
  50. package/src/components/invoice-pdf/styles.ts +6 -0
  51. package/src/components/invoice-pdf/template.tsx +59 -33
  52. package/src/components/metadata/form.tsx +14 -5
  53. package/src/components/payment-link/actions.tsx +42 -0
  54. package/src/components/price/form.tsx +91 -65
  55. package/src/components/product/vendor-config.tsx +5 -3
  56. package/src/components/promotion/active-redemptions.tsx +534 -0
  57. package/src/components/promotion/currency-multi-select.tsx +350 -0
  58. package/src/components/promotion/currency-restrictions.tsx +117 -0
  59. package/src/components/promotion/product-select.tsx +292 -0
  60. package/src/components/promotion/promotion-code-form.tsx +534 -0
  61. package/src/components/subscription/portal/list.tsx +6 -1
  62. package/src/components/subscription/vendor-service-list.tsx +13 -2
  63. package/src/locales/en.tsx +227 -0
  64. package/src/locales/zh.tsx +222 -1
  65. package/src/pages/admin/billing/subscriptions/detail.tsx +5 -0
  66. package/src/pages/admin/products/coupons/applicable-products.tsx +166 -0
  67. package/src/pages/admin/products/coupons/create.tsx +612 -0
  68. package/src/pages/admin/products/coupons/detail.tsx +538 -0
  69. package/src/pages/admin/products/coupons/edit.tsx +127 -0
  70. package/src/pages/admin/products/coupons/index.tsx +210 -3
  71. package/src/pages/admin/products/index.tsx +22 -3
  72. package/src/pages/admin/products/products/detail.tsx +12 -2
  73. package/src/pages/admin/products/promotion-codes/actions.tsx +103 -0
  74. package/src/pages/admin/products/promotion-codes/create.tsx +235 -0
  75. package/src/pages/admin/products/promotion-codes/detail.tsx +416 -0
  76. package/src/pages/admin/products/promotion-codes/list.tsx +247 -0
  77. package/src/pages/admin/products/promotion-codes/verification-config.tsx +327 -0
  78. package/src/pages/admin/products/vendors/index.tsx +17 -5
  79. package/src/pages/customer/subscription/detail.tsx +5 -0
  80. package/vite.config.ts +4 -3
package/api/src/index.ts CHANGED
@@ -26,6 +26,7 @@ import { initEventBroadcast } from './libs/ws';
26
26
  import { startCheckoutSessionQueue } from './queues/checkout-session';
27
27
  import { startCreditConsumeQueue } from './queues/credit-consume';
28
28
  import { startCreditGrantQueue } from './queues/credit-grant';
29
+ import { startDiscountStatusQueue } from './queues/discount-status';
29
30
  import { startEventQueue } from './queues/event';
30
31
  import { startInvoiceQueue } from './queues/invoice';
31
32
  import { startNotificationQueue } from './queues/notification';
@@ -135,6 +136,7 @@ export const server = app.listen(port, (err?: any) => {
135
136
  startRefundQueue().then(() => logger.info('refund queue started'));
136
137
  startCreditConsumeQueue().then(() => logger.info('credit queue started'));
137
138
  startCreditGrantQueue().then(() => logger.info('credit grant queue started'));
139
+ startDiscountStatusQueue().then(() => logger.info('discount status queue started'));
138
140
  startUploadBillingInfoListener();
139
141
 
140
142
  if (process.env.BLOCKLET_MODE === 'production') {
@@ -4,6 +4,7 @@ import pick from 'lodash/pick';
4
4
  import pWaitFor from 'p-wait-for';
5
5
  import type Stripe from 'stripe';
6
6
 
7
+ import type { WhereOptions } from 'sequelize';
7
8
  import { checkUsageReportEmpty } from '../../../libs/subscription';
8
9
  import { createEvent } from '../../../libs/audit';
9
10
  import { getLock } from '../../../libs/lock';
@@ -12,6 +13,7 @@ import {
12
13
  AutoRechargeConfig,
13
14
  CheckoutSession,
14
15
  Customer,
16
+ Discount,
15
17
  Invoice,
16
18
  InvoiceItem,
17
19
  PaymentMethod,
@@ -24,6 +26,11 @@ import { handleSubscriptionOnPaymentFailure } from './subscription';
24
26
 
25
27
  export async function handleStripeInvoicePaid(invoice: Invoice, event: TEventExpanded) {
26
28
  logger.info('invoice paid on stripe event', { locale: invoice.id });
29
+ const processDiscounts = await processInvoiceDiscounts(
30
+ event.data.object,
31
+ invoice.subscription_id || '',
32
+ invoice.checkout_session_id || ''
33
+ );
27
34
  await invoice.update({
28
35
  status: 'paid',
29
36
  ...pick(event.data.object, [
@@ -37,12 +44,38 @@ export async function handleStripeInvoicePaid(invoice: Invoice, event: TEventExp
37
44
  'subtotal_excluding_tax',
38
45
  'subtotal',
39
46
  'tax',
40
- 'total_discount_amounts',
41
47
  'total',
42
48
  ]),
49
+ total_discount_amounts: processDiscounts,
43
50
  });
44
51
  }
45
52
 
53
+ const processInvoiceDiscounts = async (stripeInvoice: any, subscriptionId: string, checkoutSessionId: string) => {
54
+ const discountFilter: WhereOptions = {};
55
+ if (checkoutSessionId) {
56
+ discountFilter.checkout_session_id = checkoutSessionId;
57
+ }
58
+ if (subscriptionId) {
59
+ discountFilter.subscription_id = subscriptionId;
60
+ }
61
+ const discount =
62
+ stripeInvoice.total_discount_amounts && Object.keys(discountFilter).length > 0
63
+ ? await Discount.findOne({
64
+ where: discountFilter,
65
+ })
66
+ : null;
67
+ const processDiscounts = stripeInvoice.total_discount_amounts
68
+ ? stripeInvoice.total_discount_amounts.map((d: any) => {
69
+ return {
70
+ stripeDiscount: d.discount,
71
+ discount: discount?.id || d.discount,
72
+ amount: d.amount,
73
+ };
74
+ })
75
+ : null;
76
+ return processDiscounts;
77
+ };
78
+
46
79
  export function getStripeInvoicePeriod(invoice: any) {
47
80
  const lineItem: TInvoiceItem = (invoice.lines.data || []).find((x: any) => !x.proration && x.type === 'subscription');
48
81
  if (lineItem && lineItem.period) {
@@ -71,6 +104,11 @@ export async function syncStripeInvoice(invoice: Invoice) {
71
104
  const client = await method.getStripeClient();
72
105
  const stripeInvoice = await client.invoices.retrieve(invoice.metadata.stripe_id);
73
106
  if (stripeInvoice) {
107
+ const processDiscounts = await processInvoiceDiscounts(
108
+ stripeInvoice,
109
+ invoice.subscription_id || '',
110
+ invoice.checkout_session_id || ''
111
+ );
74
112
  await invoice.update(
75
113
  // @ts-ignore
76
114
  merge(
@@ -89,7 +127,10 @@ export async function syncStripeInvoice(invoice: Invoice) {
89
127
  'total_discount_amounts',
90
128
  'total',
91
129
  ]),
92
- getStripeInvoicePeriod(stripeInvoice)
130
+ getStripeInvoicePeriod(stripeInvoice),
131
+ {
132
+ total_discount_amounts: processDiscounts,
133
+ }
93
134
  )
94
135
  );
95
136
  logger.info('stripe invoice synced', { locale: invoice.id, remote: stripeInvoice.id });
@@ -122,6 +163,19 @@ export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subs
122
163
  return invoice;
123
164
  }
124
165
 
166
+ const processTotalDiscounts = await processInvoiceDiscounts(
167
+ stripeInvoice,
168
+ subscription.id,
169
+ checkoutSession?.id || ''
170
+ );
171
+
172
+ let processDiscounts = stripeInvoice.discounts;
173
+ if (stripeInvoice.discounts && stripeInvoice.discounts.length > 0) {
174
+ if (processTotalDiscounts) {
175
+ processDiscounts = processTotalDiscounts.map((d: any) => d?.discount);
176
+ }
177
+ }
178
+
125
179
  const invoiceNumber = await customer.getInvoiceNumber();
126
180
  // @ts-ignore
127
181
  invoice = await Invoice.create({
@@ -157,11 +211,11 @@ export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subs
157
211
  'subtotal_excluding_tax',
158
212
  'subtotal',
159
213
  'tax',
160
- 'total_discount_amounts',
161
214
  'total',
162
215
  'last_finalization_error',
163
216
  ]),
164
-
217
+ discounts: processDiscounts,
218
+ total_discount_amounts: processTotalDiscounts,
165
219
  currency_id: subscription.currency_id,
166
220
  customer_id: subscription.customer_id,
167
221
  default_payment_method_id: subscription.default_payment_method_id as string,
@@ -173,6 +227,7 @@ export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subs
173
227
  payment_settings: subscription.payment_settings,
174
228
  metadata: {
175
229
  stripe_id: stripeInvoice.id,
230
+ stripe_discounts: stripeInvoice.discounts,
176
231
  },
177
232
  });
178
233
  if (checkoutSession) {
@@ -216,7 +271,10 @@ export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subs
216
271
  },
217
272
  });
218
273
 
219
- logger.info('stripe invoice items mirrored', { local: item.id, remote: line.id });
274
+ logger.info('stripe invoice items mirrored', {
275
+ local: item.id,
276
+ remote: line.id,
277
+ });
220
278
  return item;
221
279
  })
222
280
  );
@@ -132,6 +132,7 @@ const waitForStripePaymentMirrored = (stripeInvoiceId: string) => {
132
132
 
133
133
  export async function handlePaymentIntentEvent(event: TEventExpanded, client: Stripe) {
134
134
  const localIntentId = event.data.object.metadata?.id;
135
+
135
136
  if (!localIntentId) {
136
137
  // We only handle payment_intents created from subscriptions
137
138
  if (event.data.object.invoice) {
@@ -7,13 +7,14 @@ import pick from 'lodash/pick';
7
7
  import { Op } from 'sequelize';
8
8
 
9
9
  import logger from '../../libs/logger';
10
- import { getPriceUintAmountByCurrency } from '../../libs/session';
11
10
  import { getSubscriptionItemPrice } from '../../libs/subscription';
12
11
  import { sleep } from '../../libs/util';
13
12
  import {
14
13
  AutoRechargeConfig,
15
14
  CheckoutSession,
16
15
  Customer,
16
+ Discount,
17
+ Coupon,
17
18
  Invoice,
18
19
  InvoiceItem,
19
20
  PaymentCurrency,
@@ -28,6 +29,7 @@ import {
28
29
  import { syncStripeInvoice } from './handlers/invoice';
29
30
  import { syncStripePayment } from './handlers/payment-intent';
30
31
  import { getLock } from '../../libs/lock';
32
+ import { getPriceUintAmountByCurrency } from '../../libs/price';
31
33
 
32
34
  export async function ensureStripeProduct(internal: Product, method: PaymentMethod) {
33
35
  const client = method.getStripeClient();
@@ -281,7 +283,8 @@ export async function ensureStripeSubscription(
281
283
  stripeSubscription = await client.subscriptions.retrieve(internal.payment_details.stripe.subscription_id, {
282
284
  expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],
283
285
  });
284
- // FIXME: update?
286
+ // Handle subscription discount updates
287
+ await updateSubscriptionDiscounts(client, stripeSubscription, internal, method, currency);
285
288
  } else {
286
289
  const customer = await ensureStripePaymentCustomer(internal, method);
287
290
  const prices = await Promise.all(
@@ -327,6 +330,17 @@ export async function ensureStripeSubscription(
327
330
  props.trial_end = trialEnd;
328
331
  }
329
332
 
333
+ // Handle discounts for subscription
334
+ const discounts = await getSubscriptionDiscounts(internal, method, currency);
335
+ if (discounts.length > 0) {
336
+ props.discounts = discounts;
337
+ logger.info('Applied discounts to Stripe subscription', {
338
+ subscription_id: internal.id,
339
+ discounts: discounts.map((d) => d.coupon),
340
+ discount_count: discounts.length,
341
+ });
342
+ }
343
+
330
344
  stripeSubscription = await client.subscriptions.create(props);
331
345
  logger.info('stripe subscription created', { local: internal.id, remote: stripeSubscription.id });
332
346
 
@@ -400,6 +414,243 @@ export async function forwardUsageRecordToStripe(
400
414
  return result;
401
415
  }
402
416
 
417
+ /**
418
+ * Get all discounts associated with a subscription and create Stripe coupons
419
+ */
420
+ async function getSubscriptionDiscounts(
421
+ subscription: Subscription,
422
+ method: PaymentMethod,
423
+ currency: PaymentCurrency
424
+ ): Promise<{ coupon: string }[]> {
425
+ const discounts = (await Discount.findAll({
426
+ where: {
427
+ subscription_id: subscription.id,
428
+ },
429
+ include: [
430
+ {
431
+ model: Coupon,
432
+ as: 'coupon',
433
+ required: true,
434
+ where: {
435
+ valid: true,
436
+ },
437
+ },
438
+ ],
439
+ })) as (Discount & { coupon: Coupon })[];
440
+
441
+ const stripeDiscounts = [];
442
+
443
+ for (const discount of discounts) {
444
+ try {
445
+ const stripeCouponId = await createStripeSubscriptionCoupon(discount.coupon, subscription.id, method, currency);
446
+ stripeDiscounts.push({ coupon: stripeCouponId });
447
+
448
+ logger.info('Created Stripe coupon for subscription', {
449
+ local_coupon: discount.coupon_id,
450
+ stripe_coupon: stripeCouponId,
451
+ subscription_id: subscription.id,
452
+ });
453
+ } catch (error) {
454
+ logger.error('Failed to create Stripe coupon for subscription', {
455
+ discount_id: discount.id,
456
+ coupon_id: discount.coupon_id,
457
+ subscription_id: subscription.id,
458
+ error: error.message,
459
+ });
460
+ }
461
+ }
462
+
463
+ return stripeDiscounts;
464
+ }
465
+
466
+ /**
467
+ * Update subscription discounts by managing existing and new discounts
468
+ * Since Stripe doesn't allow direct discount updates on subscriptions,
469
+ * we need to use discount deletion and application approach
470
+ */
471
+ async function updateSubscriptionDiscounts(
472
+ client: any,
473
+ stripeSubscription: any,
474
+ subscription: Subscription,
475
+ method: PaymentMethod,
476
+ currency: PaymentCurrency
477
+ ): Promise<void> {
478
+ try {
479
+ const newDiscounts = await getSubscriptionDiscounts(subscription, method, currency);
480
+ const existingDiscounts = stripeSubscription.discounts || [];
481
+
482
+ const existingCouponIds = existingDiscounts.map((d: any) => d.coupon?.id).filter(Boolean);
483
+ const newCouponIds = newDiscounts.map((d: any) => d.coupon);
484
+
485
+ const hasChanges =
486
+ existingCouponIds.length !== newCouponIds.length ||
487
+ !existingCouponIds.every((id: string) => newCouponIds.includes(id));
488
+
489
+ if (!hasChanges) {
490
+ logger.info('No discount changes needed for subscription', {
491
+ subscription_id: subscription.id,
492
+ stripe_subscription_id: stripeSubscription.id,
493
+ });
494
+ return;
495
+ }
496
+
497
+ // Remove existing discounts (Stripe subscriptions typically have only one discount)
498
+ if (existingDiscounts.length > 0) {
499
+ try {
500
+ await client.subscriptions.deleteDiscount(stripeSubscription.id);
501
+ logger.info('Removed existing discounts from subscription', {
502
+ subscription_id: subscription.id,
503
+ stripe_subscription_id: stripeSubscription.id,
504
+ removed_count: existingDiscounts.length,
505
+ });
506
+ } catch (error) {
507
+ logger.error('Failed to remove discounts from subscription', {
508
+ subscription_id: subscription.id,
509
+ stripe_subscription_id: stripeSubscription.id,
510
+ error: error.message,
511
+ });
512
+ }
513
+ }
514
+
515
+ // Apply new discounts using the discounts field
516
+ if (newDiscounts.length > 0) {
517
+ try {
518
+ await client.subscriptions.update(stripeSubscription.id, {
519
+ discounts: newDiscounts,
520
+ });
521
+
522
+ logger.info('Applied new discounts to subscription', {
523
+ subscription_id: subscription.id,
524
+ stripe_subscription_id: stripeSubscription.id,
525
+ discounts: newDiscounts.map((d) => d.coupon),
526
+ discount_count: newDiscounts.length,
527
+ });
528
+ } catch (error) {
529
+ logger.error('Failed to apply discounts to subscription', {
530
+ subscription_id: subscription.id,
531
+ stripe_subscription_id: stripeSubscription.id,
532
+ discounts: newDiscounts,
533
+ error: error.message,
534
+ });
535
+ }
536
+ }
537
+ } catch (error) {
538
+ logger.error('Failed to update subscription discounts', {
539
+ subscription_id: subscription.id,
540
+ stripe_subscription_id: stripeSubscription.id,
541
+ error: error.message,
542
+ });
543
+ throw error;
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Create a Stripe coupon specifically for a subscription with max_redemptions = 1
549
+ */
550
+ async function createStripeSubscriptionCoupon(
551
+ localCoupon: Coupon,
552
+ subscriptionId: string,
553
+ method: PaymentMethod,
554
+ currency?: PaymentCurrency
555
+ ): Promise<string> {
556
+ const client = method.getStripeClient();
557
+
558
+ // Check if coupon already exists for this subscription
559
+ const existingCouponId = `${localCoupon.id}_sub_${subscriptionId.slice(-8)}`;
560
+ try {
561
+ const existingCoupon = await client.coupons.retrieve(existingCouponId);
562
+ if (existingCoupon) {
563
+ logger.info('Stripe coupon already exists for subscription', {
564
+ local_coupon_id: localCoupon.id,
565
+ stripe_coupon_id: existingCoupon.id,
566
+ subscription_id: subscriptionId,
567
+ });
568
+ return existingCoupon.id;
569
+ }
570
+ } catch (error) {
571
+ logger.error('Failed to retrieve Stripe coupon for subscription', {
572
+ local_coupon_id: localCoupon.id,
573
+ subscription_id: subscriptionId,
574
+ error,
575
+ });
576
+ }
577
+ // Prepare coupon data
578
+ const couponData: any = {
579
+ id: existingCouponId, // Use predictable ID
580
+ name: `sub_coupon_${localCoupon.id}`,
581
+ duration: localCoupon.duration as 'once' | 'forever' | 'repeating',
582
+ max_redemptions: 1, // Limit to this subscription only
583
+ metadata: {
584
+ appPid: env.appPid,
585
+ local_coupon_id: localCoupon.id,
586
+ subscription_id: subscriptionId,
587
+ created_for: 'subscription',
588
+ },
589
+ };
590
+
591
+ // Add duration in months for repeating coupons
592
+ if (localCoupon.duration === 'repeating' && localCoupon.duration_in_months) {
593
+ couponData.duration_in_months = localCoupon.duration_in_months;
594
+ }
595
+
596
+ // Add discount amount - either percent_off or amount_off
597
+ if (localCoupon.percent_off && localCoupon.percent_off > 0) {
598
+ couponData.percent_off = localCoupon.percent_off;
599
+ } else if (localCoupon.amount_off && localCoupon.amount_off !== '0') {
600
+ couponData.amount_off = Number(localCoupon.amount_off || '0');
601
+
602
+ // Get currency for fixed amount discounts
603
+ let targetCurrency = currency;
604
+ if (!targetCurrency && localCoupon.currency_id) {
605
+ targetCurrency = (await PaymentCurrency.findByPk(localCoupon.currency_id)) || undefined;
606
+ }
607
+
608
+ if (targetCurrency) {
609
+ couponData.currency = targetCurrency.symbol.toLowerCase();
610
+ } else {
611
+ // Fallback to USD if no currency specified
612
+ couponData.currency = 'usd';
613
+ logger.warn('No currency specified for fixed amount coupon, defaulting to USD', {
614
+ local_coupon_id: localCoupon.id,
615
+ subscription_id: subscriptionId,
616
+ });
617
+ }
618
+ } else {
619
+ // Default to 0% discount if neither percent_off nor amount_off is set
620
+ couponData.percent_off = 0;
621
+ logger.warn('Coupon has no discount amount set, defaulting to 0%', {
622
+ local_coupon_id: localCoupon.id,
623
+ subscription_id: subscriptionId,
624
+ });
625
+ }
626
+
627
+ try {
628
+ const stripeCoupon = await client.coupons.create(couponData);
629
+
630
+ logger.info('Created Stripe coupon for subscription', {
631
+ local_coupon_id: localCoupon.id,
632
+ stripe_coupon_id: stripeCoupon.id,
633
+ subscription_id: subscriptionId,
634
+ coupon_name: stripeCoupon.name,
635
+ duration: stripeCoupon.duration,
636
+ percent_off: stripeCoupon.percent_off,
637
+ amount_off: stripeCoupon.amount_off,
638
+ currency: stripeCoupon.currency,
639
+ max_redemptions: stripeCoupon.max_redemptions,
640
+ });
641
+
642
+ return stripeCoupon.id;
643
+ } catch (error) {
644
+ logger.error('Failed to create Stripe coupon for subscription', {
645
+ local_coupon_id: localCoupon.id,
646
+ subscription_id: subscriptionId,
647
+ coupon_data: couponData,
648
+ error,
649
+ });
650
+ throw error;
651
+ }
652
+ }
653
+
403
654
  export async function batchHandleStripeInvoices() {
404
655
  const stripeMethods = await PaymentMethod.findAll({ where: { type: 'stripe' } });
405
656
  if (stripeMethods.length === 0) {
@@ -0,0 +1,31 @@
1
+ import { fromTokenToUnit, fromUnitToToken } from '@ocap/util';
2
+ import { PaymentCurrency } from '../store/models';
3
+ import { trimDecimals } from './math-utils';
4
+
5
+ export async function formatCurrencyToken(amount: string, currencyId: string) {
6
+ if (!amount) {
7
+ return '0';
8
+ }
9
+ if (!currencyId) {
10
+ return amount;
11
+ }
12
+ const currency = await PaymentCurrency.findByPk(currencyId);
13
+ if (!currency) {
14
+ throw new Error(`Currency ${currencyId} not found`);
15
+ }
16
+ return fromUnitToToken(trimDecimals(amount || '0', currency.decimal), currency.decimal);
17
+ }
18
+
19
+ export async function formatCurrencyUnit(amount: string, currencyId: string) {
20
+ if (!amount) {
21
+ return '0';
22
+ }
23
+ if (!currencyId) {
24
+ return amount;
25
+ }
26
+ const currency = await PaymentCurrency.findByPk(currencyId);
27
+ if (!currency) {
28
+ throw new Error(`Currency ${currencyId} not found`);
29
+ }
30
+ return fromTokenToUnit(amount || '0', currency.decimal).toString();
31
+ }