payment-kit 1.20.11 → 1.20.13

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 (92) hide show
  1. package/api/src/crons/index.ts +8 -0
  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/env.ts +1 -0
  13. package/api/src/libs/invoice.ts +44 -10
  14. package/api/src/libs/math-utils.ts +6 -0
  15. package/api/src/libs/price.ts +43 -0
  16. package/api/src/libs/session.ts +242 -57
  17. package/api/src/libs/subscription.ts +2 -6
  18. package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +1 -1
  19. package/api/src/libs/vendor-util/adapters/types.ts +1 -0
  20. package/api/src/libs/vendor-util/fulfillment.ts +1 -1
  21. package/api/src/queues/auto-recharge.ts +1 -1
  22. package/api/src/queues/discount-status.ts +200 -0
  23. package/api/src/queues/subscription.ts +98 -5
  24. package/api/src/queues/usage-record.ts +1 -1
  25. package/api/src/queues/vendors/fulfillment-coordinator.ts +1 -29
  26. package/api/src/queues/vendors/return-processor.ts +184 -0
  27. package/api/src/queues/vendors/return-scanner.ts +119 -0
  28. package/api/src/queues/vendors/status-check.ts +1 -1
  29. package/api/src/routes/auto-recharge-configs.ts +5 -3
  30. package/api/src/routes/checkout-sessions.ts +755 -64
  31. package/api/src/routes/connect/change-payment.ts +6 -1
  32. package/api/src/routes/connect/change-plan.ts +6 -1
  33. package/api/src/routes/connect/setup.ts +6 -1
  34. package/api/src/routes/connect/shared.ts +80 -9
  35. package/api/src/routes/connect/subscribe.ts +12 -2
  36. package/api/src/routes/coupons.ts +518 -0
  37. package/api/src/routes/index.ts +4 -0
  38. package/api/src/routes/invoices.ts +44 -3
  39. package/api/src/routes/meter-events.ts +2 -1
  40. package/api/src/routes/payment-currencies.ts +1 -0
  41. package/api/src/routes/promotion-codes.ts +482 -0
  42. package/api/src/routes/subscriptions.ts +23 -2
  43. package/api/src/routes/vendor.ts +89 -2
  44. package/api/src/store/migrations/20250904-discount.ts +136 -0
  45. package/api/src/store/migrations/20250910-timestamp-fields.ts +116 -0
  46. package/api/src/store/migrations/20250916-add-description-fields.ts +30 -0
  47. package/api/src/store/migrations/20250918-add-vendor-extends.ts +20 -0
  48. package/api/src/store/models/checkout-session.ts +17 -2
  49. package/api/src/store/models/coupon.ts +144 -4
  50. package/api/src/store/models/discount.ts +23 -10
  51. package/api/src/store/models/index.ts +13 -2
  52. package/api/src/store/models/product-vendor.ts +6 -0
  53. package/api/src/store/models/promotion-code.ts +295 -18
  54. package/api/src/store/models/types.ts +30 -1
  55. package/api/tests/libs/session.spec.ts +48 -27
  56. package/blocklet.yml +1 -1
  57. package/package.json +20 -20
  58. package/src/app.tsx +2 -0
  59. package/src/components/customer/link.tsx +1 -1
  60. package/src/components/discount/discount-info.tsx +178 -0
  61. package/src/components/invoice/table.tsx +140 -48
  62. package/src/components/invoice-pdf/styles.ts +6 -0
  63. package/src/components/invoice-pdf/template.tsx +59 -33
  64. package/src/components/metadata/form.tsx +14 -5
  65. package/src/components/payment-link/actions.tsx +42 -0
  66. package/src/components/price/form.tsx +91 -65
  67. package/src/components/product/vendor-config.tsx +5 -3
  68. package/src/components/promotion/active-redemptions.tsx +534 -0
  69. package/src/components/promotion/currency-multi-select.tsx +350 -0
  70. package/src/components/promotion/currency-restrictions.tsx +117 -0
  71. package/src/components/promotion/product-select.tsx +292 -0
  72. package/src/components/promotion/promotion-code-form.tsx +534 -0
  73. package/src/components/subscription/portal/list.tsx +6 -1
  74. package/src/components/subscription/vendor-service-list.tsx +13 -2
  75. package/src/locales/en.tsx +227 -0
  76. package/src/locales/zh.tsx +222 -1
  77. package/src/pages/admin/billing/subscriptions/detail.tsx +5 -0
  78. package/src/pages/admin/products/coupons/applicable-products.tsx +166 -0
  79. package/src/pages/admin/products/coupons/create.tsx +612 -0
  80. package/src/pages/admin/products/coupons/detail.tsx +538 -0
  81. package/src/pages/admin/products/coupons/edit.tsx +127 -0
  82. package/src/pages/admin/products/coupons/index.tsx +210 -3
  83. package/src/pages/admin/products/index.tsx +22 -3
  84. package/src/pages/admin/products/products/detail.tsx +12 -2
  85. package/src/pages/admin/products/promotion-codes/actions.tsx +103 -0
  86. package/src/pages/admin/products/promotion-codes/create.tsx +235 -0
  87. package/src/pages/admin/products/promotion-codes/detail.tsx +416 -0
  88. package/src/pages/admin/products/promotion-codes/list.tsx +247 -0
  89. package/src/pages/admin/products/promotion-codes/verification-config.tsx +327 -0
  90. package/src/pages/admin/products/vendors/index.tsx +17 -5
  91. package/src/pages/customer/subscription/detail.tsx +5 -0
  92. package/vite.config.ts +4 -3
@@ -35,7 +35,12 @@ export default {
35
35
  const items = subscription!.items as TLineItemExpanded[];
36
36
  const trialing = true;
37
37
  const billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
38
- const fastCheckoutAmount = getFastCheckoutAmount(items, 'subscription', paymentCurrency.id, false);
38
+ const fastCheckoutAmount = await getFastCheckoutAmount({
39
+ items,
40
+ mode: 'subscription',
41
+ currencyId: paymentCurrency.id,
42
+ trialing: false,
43
+ });
39
44
 
40
45
  if (paymentMethod.type === 'arcblock') {
41
46
  const delegation = await isDelegationSufficientForPayment({
@@ -37,7 +37,12 @@ export default {
37
37
  const items = subscription!.items as TLineItemExpanded[];
38
38
  const trialing = false;
39
39
  const billingThreshold = Number(subscription!.billing_thresholds?.amount_gte || 0);
40
- const fastCheckoutAmount = getFastCheckoutAmount(items, 'subscription', paymentCurrency.id, false);
40
+ const fastCheckoutAmount = await getFastCheckoutAmount({
41
+ items,
42
+ mode: 'subscription',
43
+ currencyId: paymentCurrency.id,
44
+ trialing: false,
45
+ });
41
46
 
42
47
  if (paymentMethod.type === 'arcblock') {
43
48
  const delegation = await isDelegationSufficientForPayment({
@@ -52,7 +52,12 @@ export default {
52
52
  const trialing = trialInDays > 0 || trialEnd > now;
53
53
  const billingThreshold = Number(checkoutSession.subscription_data?.billing_threshold_amount || 0);
54
54
  const minStakeAmount = Number(checkoutSession.subscription_data?.min_stake_amount || 0);
55
- const fastCheckoutAmount = getFastCheckoutAmount(items, checkoutSession.mode, paymentCurrency.id, trialing);
55
+ const fastCheckoutAmount = await getFastCheckoutAmount({
56
+ items,
57
+ mode: checkoutSession.mode,
58
+ currencyId: paymentCurrency.id,
59
+ trialing,
60
+ });
56
61
 
57
62
  if (paymentMethod.type === 'arcblock') {
58
63
  const delegation = await isDelegationSufficientForPayment({
@@ -22,6 +22,7 @@ import {
22
22
  isDonationCheckoutSession,
23
23
  getSubscriptionLineItems,
24
24
  } from '../../libs/session';
25
+ import { getDiscountRecordsForCheckout } from '../../libs/discount/coupon';
25
26
  import {
26
27
  expandSubscriptionItems,
27
28
  getSubscriptionPaymentAddress,
@@ -268,7 +269,26 @@ export async function ensureSetupIntent(checkoutSessionId: string, skipInvoice?:
268
269
  if (checkoutSession.invoice_id) {
269
270
  invoice = await Invoice.findByPk(checkoutSession.invoice_id);
270
271
  } else {
271
- // generate empty invoice here
272
+ // Get discount information for this checkout session
273
+ let discountInfo: { appliedDiscounts: string[]; discountBreakdown: Array<{ amount: string; discount: string }> } = {
274
+ appliedDiscounts: [],
275
+ discountBreakdown: []
276
+ };
277
+
278
+ try {
279
+ discountInfo = await getDiscountRecordsForCheckout({
280
+ checkoutSessionId: checkoutSession.id,
281
+ customerId: customer.id,
282
+ });
283
+ } catch (error) {
284
+ logger.warn('Failed to get discount records for setup intent checkout session', {
285
+ checkoutSessionId: checkoutSession.id,
286
+ customerId: customer.id,
287
+ error: error.message,
288
+ });
289
+ }
290
+
291
+ // generate empty invoice here with discount information
272
292
  const result = await ensureInvoiceAndItems({
273
293
  customer,
274
294
  currency: paymentCurrency,
@@ -294,14 +314,20 @@ export async function ensureSetupIntent(checkoutSessionId: string, skipInvoice?:
294
314
 
295
315
  default_payment_method_id: subscription.default_payment_method_id as string,
296
316
 
317
+ discounts: discountInfo.appliedDiscounts,
318
+ total_discount_amounts: discountInfo.discountBreakdown,
319
+
297
320
  custom_fields: checkoutSession.invoice_creation?.invoice_data?.custom_fields || [],
298
321
  footer: checkoutSession.invoice_creation?.invoice_data?.footer || '',
299
322
  metadata: checkoutSession.invoice_creation?.invoice_data?.metadata || {},
300
- } as Invoice,
323
+ } as unknown as Invoice,
301
324
  });
302
325
  invoice = result.invoice;
303
326
 
304
- logger.info(`Invoice created for checkoutSession ${checkoutSession.id} in setup mode: ${invoice.id}`);
327
+ logger.info(`Invoice created for checkoutSession ${checkoutSession.id} in setup mode: ${invoice.id}`, {
328
+ hasDiscounts: discountInfo.appliedDiscounts.length > 0,
329
+ discountCount: discountInfo.appliedDiscounts.length,
330
+ });
305
331
 
306
332
  // persist invoice id
307
333
  await checkoutSession.update({ invoice_id: invoice.id });
@@ -436,7 +462,41 @@ export async function ensureInvoiceForCheckout({
436
462
  const trialEnd = Number(checkoutSession.subscription_data?.trial_end || 0);
437
463
  const now = dayjs().unix();
438
464
  const invoiceItems = lineItems || (await Price.expand(checkoutSession.line_items, { product: true }));
439
- const totalAmount = getCheckoutAmount(invoiceItems, checkoutSession.currency_id, trialInDays > 0 || trialEnd > now);
465
+
466
+ // Get discount records for this checkout session
467
+ let discountInfo: { appliedDiscounts: string[]; discountBreakdown: Array<{ amount: string; discount: string }> } = {
468
+ appliedDiscounts: [],
469
+ discountBreakdown: []
470
+ };
471
+
472
+ // Prepare discount configuration for getCheckoutAmount
473
+ let discountConfig;
474
+ try {
475
+ discountInfo = await getDiscountRecordsForCheckout({
476
+ checkoutSessionId: checkoutSession.id,
477
+ customerId: customer.id,
478
+ });
479
+
480
+ // Apply discount if we have discount records
481
+ if (discountInfo.appliedDiscounts.length > 0 && checkoutSession.discounts?.length) {
482
+ const firstDiscount = checkoutSession.discounts[0];
483
+ if (firstDiscount?.coupon && firstDiscount?.promotion_code) {
484
+ discountConfig = {
485
+ promotionCodeId: firstDiscount.promotion_code,
486
+ couponId: firstDiscount.coupon,
487
+ customerId: customer.id,
488
+ };
489
+ }
490
+ }
491
+ } catch (error) {
492
+ logger.warn('Failed to get discount records for checkout session', {
493
+ checkoutSessionId: checkoutSession.id,
494
+ customerId: customer.id,
495
+ error: error.message,
496
+ });
497
+ }
498
+
499
+ const totalAmount = await getCheckoutAmount(invoiceItems, checkoutSession.currency_id, trialInDays > 0 || trialEnd > now, discountConfig);
440
500
  const { invoice, items } = await ensureInvoiceAndItems({
441
501
  customer,
442
502
  currency: currency as PaymentCurrency,
@@ -461,7 +521,8 @@ export async function ensureInvoiceForCheckout({
461
521
  payment_intent_id: paymentIntent?.id,
462
522
  checkout_session_id: checkoutSession.id,
463
523
 
464
- total: totalAmount.subtotal,
524
+ total: totalAmount.total,
525
+ subtotal: totalAmount.subtotal,
465
526
 
466
527
  default_payment_method_id: (subscription?.default_payment_method_id ||
467
528
  paymentIntent?.payment_method_id) as string,
@@ -469,10 +530,20 @@ export async function ensureInvoiceForCheckout({
469
530
  custom_fields: checkoutSession.invoice_creation?.invoice_data?.custom_fields || [],
470
531
  footer: checkoutSession.invoice_creation?.invoice_data?.footer || '',
471
532
  metadata,
533
+
534
+ discounts: discountInfo.appliedDiscounts,
535
+ total_discount_amounts: discountInfo.discountBreakdown,
536
+
472
537
  ...(props || {}),
473
- } as Invoice,
538
+ } as unknown as Invoice,
539
+ });
540
+
541
+ logger.info('Invoice created for checkoutSession', {
542
+ checkoutSessionId: checkoutSession.id,
543
+ invoiceId: invoice.id,
544
+ hasDiscounts: discountInfo.appliedDiscounts.length > 0,
545
+ discountCount: discountInfo.appliedDiscounts.length,
474
546
  });
475
- logger.info('Invoice created for checkoutSession', { checkoutSessionId: checkoutSession.id, invoiceId: invoice.id });
476
547
 
477
548
  // only update invoice_id for single invoice
478
549
  if (!isGroupInvoice) {
@@ -691,7 +762,7 @@ export async function getDelegationTxClaim({
691
762
  requiredStake?: boolean;
692
763
  requiredAmount?: string;
693
764
  }) {
694
- const amount = getFastCheckoutAmount(items, mode, paymentCurrency.id);
765
+ const amount = await getFastCheckoutAmount({ items, mode, currencyId: paymentCurrency.id });
695
766
  const address = toDelegateAddress(userDid, wallet.address);
696
767
  const tokenLimits = await getTokenLimitsForDelegation(items, paymentMethod, paymentCurrency, address, amount);
697
768
  let tokenRequirements = await getTokenRequirements({
@@ -1010,7 +1081,7 @@ export async function getTokenRequirements({
1010
1081
  requiredStake,
1011
1082
  }: TokenRequirementArgs) {
1012
1083
  const tokenRequirements = [];
1013
- let amount = getFastCheckoutAmount(items, mode, paymentCurrency.id, !!trialing);
1084
+ let amount = await getFastCheckoutAmount({ items, mode, currencyId: paymentCurrency.id, trialing: !!trialing });
1014
1085
 
1015
1086
  // If the app has not staked, we need to add the gas fee to the amount
1016
1087
  if ((await hasStakedForGas(paymentMethod)) === false) {
@@ -73,7 +73,12 @@ export default {
73
73
  const trialing = trialInDays > 0 || trialEnd > now;
74
74
  const billingThreshold = Number(checkoutSession.subscription_data?.billing_threshold_amount || 0);
75
75
  const minStakeAmount = Number(checkoutSession.subscription_data?.min_stake_amount || 0);
76
- const fastCheckoutAmount = getFastCheckoutAmount(items, checkoutSession.mode, paymentCurrency.id, trialing);
76
+ const fastCheckoutAmount = await getFastCheckoutAmount({
77
+ items,
78
+ mode: checkoutSession.mode,
79
+ currencyId: paymentCurrency.id,
80
+ trialing,
81
+ });
77
82
  const claimsList: any[] = [];
78
83
 
79
84
  const allSubscriptionIds = subscriptions.map((sub) => sub.id);
@@ -240,7 +245,12 @@ export default {
240
245
  paymentCurrency.id
241
246
  );
242
247
  const trialing = trialInDays > 0 || trialEnd > now;
243
- const fastCheckoutAmount = getFastCheckoutAmount(items, checkoutSession.mode, paymentCurrency.id, trialing);
248
+ const fastCheckoutAmount = await getFastCheckoutAmount({
249
+ items,
250
+ mode: checkoutSession.mode,
251
+ currencyId: paymentCurrency.id,
252
+ trialing,
253
+ });
244
254
  // check token balance
245
255
  const tokenBalanceCheck = await checkTokenBalance({
246
256
  paymentMethod,