payment-kit 1.13.174 → 1.13.176

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 (40) hide show
  1. package/api/src/crons/index.ts +20 -1
  2. package/api/src/integrations/blockchain/stake.ts +99 -1
  3. package/api/src/integrations/stripe/handlers/invoice.ts +4 -0
  4. package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -1
  5. package/api/src/integrations/stripe/resource.ts +63 -12
  6. package/api/src/libs/audit.ts +3 -3
  7. package/api/src/libs/env.ts +2 -0
  8. package/api/src/libs/notification/template/subscription-will-renew.ts +1 -1
  9. package/api/src/libs/subscription.ts +35 -2
  10. package/api/src/queues/checkout-session.ts +98 -94
  11. package/api/src/queues/invoice.ts +23 -13
  12. package/api/src/queues/notification.ts +13 -11
  13. package/api/src/queues/payment.ts +27 -21
  14. package/api/src/queues/refund.ts +5 -5
  15. package/api/src/queues/subscription.ts +210 -49
  16. package/api/src/routes/checkout-sessions.ts +8 -40
  17. package/api/src/routes/connect/change-payment.ts +52 -38
  18. package/api/src/routes/connect/change-plan.ts +51 -39
  19. package/api/src/routes/connect/collect-batch.ts +1 -0
  20. package/api/src/routes/connect/collect.ts +2 -1
  21. package/api/src/routes/connect/pay.ts +1 -0
  22. package/api/src/routes/connect/setup.ts +70 -56
  23. package/api/src/routes/connect/shared.ts +162 -17
  24. package/api/src/routes/connect/subscribe.ts +60 -54
  25. package/api/src/routes/invoices.ts +5 -0
  26. package/api/src/routes/payment-intents.ts +6 -2
  27. package/api/src/store/models/subscription.ts +1 -6
  28. package/api/src/store/models/types.ts +5 -1
  29. package/api/tests/libs/subscription.spec.ts +85 -3
  30. package/blocklet.yml +1 -1
  31. package/package.json +4 -4
  32. package/src/app.tsx +2 -1
  33. package/src/components/customer/link.tsx +22 -8
  34. package/src/components/event/list.tsx +1 -3
  35. package/src/pages/admin/billing/subscriptions/detail.tsx +12 -1
  36. package/src/pages/admin/payments/intents/detail.tsx +1 -1
  37. package/src/pages/admin/products/products/index.tsx +3 -0
  38. package/src/pages/customer/invoice/detail.tsx +7 -3
  39. package/src/pages/customer/subscription/detail.tsx +14 -5
  40. /package/api/src/libs/notification/template/{subscription-cacceled.ts → subscription-canceled.ts} +0 -0
@@ -1,10 +1,17 @@
1
1
  import Cron from '@abtnode/cron';
2
2
 
3
- import { batchHandleStripeInvoices, batchHandleStripeSubscriptions } from '../integrations/stripe/resource';
3
+ import { checkStakeRevokeTx } from '../integrations/blockchain/stake';
4
+ import {
5
+ batchHandleStripeInvoices,
6
+ batchHandleStripePayments,
7
+ batchHandleStripeSubscriptions,
8
+ } from '../integrations/stripe/resource';
4
9
  import {
5
10
  expiredSessionCleanupCronTime,
6
11
  notificationCronTime,
12
+ revokeStakeCronTime,
7
13
  stripeInvoiceCronTime,
14
+ stripePaymentCronTime,
8
15
  stripeSubscriptionCronTime,
9
16
  subscriptionCronTime,
10
17
  } from '../libs/env';
@@ -58,12 +65,24 @@ function init() {
58
65
  fn: batchHandleStripeInvoices,
59
66
  options: { runOnInit: false },
60
67
  },
68
+ {
69
+ name: 'stripe.payment.sync',
70
+ time: stripePaymentCronTime,
71
+ fn: batchHandleStripePayments,
72
+ options: { runOnInit: false },
73
+ },
61
74
  {
62
75
  name: 'stripe.subscription.sync',
63
76
  time: stripeSubscriptionCronTime,
64
77
  fn: batchHandleStripeSubscriptions,
65
78
  options: { runOnInit: false },
66
79
  },
80
+ {
81
+ name: 'customer.stake.revoked',
82
+ time: revokeStakeCronTime,
83
+ fn: checkStakeRevokeTx,
84
+ options: { runOnInit: false },
85
+ },
67
86
  ],
68
87
  onError: (error: Error, name: string) => {
69
88
  logger.error('run job failed', { name, error: error.message, stack: error.stack });
@@ -1,12 +1,15 @@
1
1
  /* eslint-disable no-continue */
2
2
  /* eslint-disable no-await-in-loop */
3
+ import assert from 'assert';
4
+
3
5
  import { toStakeAddress } from '@arcblock/did-util';
4
6
  import env from '@blocklet/sdk/lib/env';
5
7
  import { fromUnitToToken, toBN } from '@ocap/util';
6
8
 
7
9
  import { wallet } from '../../libs/auth';
10
+ import { events } from '../../libs/event';
8
11
  import logger from '../../libs/logger';
9
- import { PaymentCurrency, PaymentMethod } from '../../store/models';
12
+ import { Customer, PaymentCurrency, PaymentMethod, Subscription } from '../../store/models';
10
13
 
11
14
  export async function ensureStakedForGas() {
12
15
  const currencies = await PaymentCurrency.findAll({ where: { active: true, is_base_currency: true } });
@@ -74,3 +77,98 @@ export async function estimateMaxGasForTx(method: PaymentMethod, typeUrl = 'fg:t
74
77
 
75
78
  return '0';
76
79
  }
80
+
81
+ export async function getAllResults(dataKey: string, fn: Function) {
82
+ const results = [];
83
+ const pageSize = 40;
84
+
85
+ const { page, [dataKey]: firstPage } = await fn({ size: pageSize });
86
+ if (page.total < pageSize) {
87
+ return firstPage;
88
+ }
89
+
90
+ results.push(...firstPage);
91
+
92
+ const total = Math.floor(page.total / pageSize);
93
+ const tasks = [];
94
+ for (let i = 1; i <= total; i++) {
95
+ tasks.push(async () => {
96
+ const { [dataKey]: nextPage } = await fn({ size: pageSize, cursor: i * pageSize });
97
+ results.push(...nextPage);
98
+ });
99
+ }
100
+ await Promise.all(tasks.map((x) => x()));
101
+ assert.equal(results.length, page.total, `fetched ${dataKey} count does not match`);
102
+
103
+ return results;
104
+ }
105
+
106
+ export async function checkStakeRevokeTx() {
107
+ const methods = await PaymentMethod.findAll({ where: { type: 'arcblock' } });
108
+ if (methods.length === 0) {
109
+ return;
110
+ }
111
+
112
+ const interval = 1000 * 60 * 10;
113
+ const startDateTime = new Date(Date.now() - interval).toISOString();
114
+ const endDateTime = new Date().toISOString();
115
+
116
+ for (const method of methods) {
117
+ const client = method.getOcapClient();
118
+ const txs = await getAllResults('transactions', (paging: any) =>
119
+ client.listTransactions({
120
+ paging,
121
+ typeFilter: { types: ['revoke_stake'] },
122
+ timeFilter: {
123
+ field: 'time',
124
+ startDateTime,
125
+ endDateTime,
126
+ },
127
+ })
128
+ );
129
+
130
+ logger.info(`Found ${txs.length} revoke stake tx on chain ${method.name}`, {
131
+ interval,
132
+ startDateTime,
133
+ endDateTime,
134
+ txs: txs.map((x: any) => x.hash),
135
+ });
136
+
137
+ await Promise.all(
138
+ txs.map(async (t: any) => {
139
+ const customer = await Customer.findByPkOrDid(t.tx.from);
140
+ if (!customer) {
141
+ return;
142
+ }
143
+
144
+ const address = toStakeAddress(customer.did, wallet.address);
145
+ if (t.tx.itxJson.address !== address) {
146
+ return;
147
+ }
148
+
149
+ // Check related subscriptions in the stake
150
+ const subscriptions = await Subscription.findAll({
151
+ where: { 'payment_details.arcblock.staking.address': address },
152
+ });
153
+ if (subscriptions.length === 0) {
154
+ return;
155
+ }
156
+ logger.info(`Active subscriptions found for revoked stake on chain ${method.name}`, {
157
+ address,
158
+ customer: customer.did,
159
+ subscriptions: subscriptions.map((x) => x.id),
160
+ txHash: t.hash,
161
+ });
162
+
163
+ const { state: stake } = await client.getStakeState({ address });
164
+ const data = JSON.parse(stake.data?.value || '{}');
165
+ subscriptions
166
+ .filter((s) => s.isActive())
167
+ .filter((s) => data[s.id])
168
+ .forEach((s) => {
169
+ events.emit('customer.stake.revoked', { subscriptionId: s.id, tx: t });
170
+ });
171
+ })
172
+ );
173
+ }
174
+ }
@@ -141,7 +141,11 @@ export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subs
141
141
  if (checkoutSession) {
142
142
  await checkoutSession.update({ invoice_id: invoice.id });
143
143
  }
144
+ if (subscription) {
145
+ await subscription.update({ latest_invoice_id: invoice.id });
146
+ }
144
147
  await client.invoices.update(stripeInvoice.id, { metadata: { appPid: env.appPid, id: invoice.id } });
148
+
145
149
  logger.info('stripe invoice mirrored', { local: invoice.id, remote: stripeInvoice.id });
146
150
 
147
151
  await Promise.all(
@@ -26,7 +26,7 @@ export async function handleStripePaymentSucceed(paymentIntent: PaymentIntent, e
26
26
  }
27
27
 
28
28
  export async function syncStripePayment(paymentIntent: PaymentIntent) {
29
- if (!paymentIntent.metadata?.stripe_id) {
29
+ if (!paymentIntent?.metadata?.stripe_id) {
30
30
  return;
31
31
  }
32
32
 
@@ -346,7 +346,7 @@ export async function batchHandleStripeInvoices() {
346
346
 
347
347
  const stripeInvoices = await Invoice.findAll({
348
348
  where: {
349
- status: ['draft', 'open'],
349
+ status: ['draft', 'open', 'finalized'],
350
350
  'metadata.stripe_id': { [Op.not]: null },
351
351
  },
352
352
  });
@@ -364,22 +364,22 @@ export async function batchHandleStripeInvoices() {
364
364
 
365
365
  const client = method.getStripeClient();
366
366
  try {
367
- const exist = await client.invoices.retrieve(stripeInvoiceId);
367
+ let exist = await client.invoices.retrieve(stripeInvoiceId);
368
368
  if (exist) {
369
369
  if (exist.status === 'draft') {
370
- await client.invoices.finalizeInvoice(stripeInvoiceId);
370
+ exist = await client.invoices.finalizeInvoice(stripeInvoiceId);
371
371
  logger.info('stripe invoice finalized', { local: invoice.id, stripe: stripeInvoiceId });
372
372
  }
373
- await client.invoices.pay(stripeInvoiceId);
374
- logger.info('stripe invoice payment requested', { local: invoice.id, stripe: stripeInvoiceId });
373
+ if (exist.status !== 'paid') {
374
+ exist = await client.invoices.pay(stripeInvoiceId);
375
+ logger.info('stripe invoice payment requested', { local: invoice.id, stripe: stripeInvoiceId });
376
+ }
375
377
 
376
378
  await syncStripeInvoice(invoice);
377
379
 
378
380
  if (invoice.payment_intent_id) {
379
381
  const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
380
- if (paymentIntent) {
381
- await syncStripePayment(paymentIntent);
382
- }
382
+ await syncStripePayment(paymentIntent!);
383
383
  }
384
384
  } else {
385
385
  await Invoice.destroy({ where: { id: invoice.id } });
@@ -395,7 +395,7 @@ export async function batchHandleStripeInvoices() {
395
395
  }
396
396
  }
397
397
 
398
- await sleep(5000);
398
+ await sleep(2000);
399
399
  }
400
400
  }
401
401
 
@@ -437,8 +437,16 @@ export async function batchHandleStripeSubscriptions() {
437
437
  if (subscription.payment_settings?.payment_method_types?.includes('stripe')) {
438
438
  fields.push('pause_collection');
439
439
  }
440
- await subscription.update(pick(exist, fields) as any);
441
- logger.warn('stripe subscription synced', { local: subscription.id, stripe: subscriptionId });
440
+ const updates: any = pick(exist, fields);
441
+ if (exist.latest_invoice) {
442
+ const invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': exist.latest_invoice } });
443
+ if (invoice) {
444
+ updates.latest_invoice_id = invoice.id;
445
+ }
446
+ }
447
+
448
+ await subscription.update(updates);
449
+ logger.warn('stripe subscription synced', { local: subscription.id, stripe: subscriptionId, updates });
442
450
  } else {
443
451
  logger.warn('stripe subscription missing', { local: subscription.id, stripe: subscriptionId });
444
452
  }
@@ -454,6 +462,49 @@ export async function batchHandleStripeSubscriptions() {
454
462
  }
455
463
  }
456
464
 
457
- await sleep(3000);
465
+ await sleep(2000);
466
+ }
467
+ }
468
+
469
+ export async function batchHandleStripePayments() {
470
+ const stripeMethods = await PaymentMethod.findAll({ where: { type: 'stripe' } });
471
+ if (stripeMethods.length === 0) {
472
+ return;
473
+ }
474
+
475
+ const stripePayments = await PaymentIntent.findAll({
476
+ where: {
477
+ status: ['requires_payment_method'],
478
+ 'metadata.stripe_id': { [Op.not]: null },
479
+ },
480
+ });
481
+
482
+ for (const payment of stripePayments) {
483
+ const stripePaymentId = payment.metadata?.stripe_id;
484
+ if (!stripePaymentId) {
485
+ continue;
486
+ }
487
+
488
+ const method = stripeMethods.find((m) => m.livemode === payment.livemode);
489
+ if (!method) {
490
+ continue;
491
+ }
492
+
493
+ const client = method.getStripeClient();
494
+ try {
495
+ const exist = await client.paymentIntents.retrieve(stripePaymentId);
496
+ if (exist) {
497
+ await syncStripePayment(payment);
498
+ }
499
+ } catch (error) {
500
+ if (error.message.includes('No such payment')) {
501
+ await PaymentIntent.destroy({ where: { id: payment.id } });
502
+ logger.warn('stripe payment intent purged', { local: payment.id, stripe: stripePaymentId });
503
+ } else {
504
+ logger.error('stripe payment sync error', error);
505
+ }
506
+ }
507
+
508
+ await sleep(1000);
458
509
  }
459
510
  }
@@ -8,13 +8,13 @@ import { events } from './event';
8
8
  const API_VERSION = '2023-09-05';
9
9
 
10
10
  export async function createEvent(scope: string, type: LiteralUnion<EventType, string>, model: any, options: any = {}) {
11
- // console.log('createEvent', scope, type, model, options);
12
11
  const data: any = {
13
12
  object: model.dataValues,
14
13
  };
15
14
  if (type.endsWith('updated')) {
16
15
  data.previous_attributes = pick(model._previousDataValues, options.fields);
17
16
  }
17
+ // console.log('createEvent', scope, type, data, options);
18
18
 
19
19
  const event = await Event.create({
20
20
  type,
@@ -43,7 +43,6 @@ export async function createStatusEvent(
43
43
  model: any,
44
44
  options: any = {}
45
45
  ) {
46
- // console.log('createStatusEvent', scope, prefix, config, model, options);
47
46
  if (options.fields.includes('status') === false) {
48
47
  return;
49
48
  }
@@ -57,6 +56,7 @@ export async function createStatusEvent(
57
56
  return;
58
57
  }
59
58
 
59
+ // console.log('createStatusEvent', scope, prefix, config, data, options);
60
60
  const suffix = config[data.object.status];
61
61
  const event = await Event.create({
62
62
  type: [prefix, suffix].join('.'),
@@ -85,7 +85,6 @@ export async function createCustomEvent(
85
85
  model: any,
86
86
  options: any
87
87
  ) {
88
- // console.log('createCustomEvent', scope, prefix, type, model, options);
89
88
  const data: any = {
90
89
  object: model.dataValues,
91
90
  previous_attributes: pick(model._previousDataValues, options.fields),
@@ -96,6 +95,7 @@ export async function createCustomEvent(
96
95
  return;
97
96
  }
98
97
 
98
+ // console.log('createCustomEvent', scope, prefix, type, data, options);
99
99
  const event = await Event.create({
100
100
  type: [prefix, suffix].join('.'),
101
101
  api_version: API_VERSION,
@@ -5,7 +5,9 @@ export const notificationCronTime: string = process.env.NOTIFICATION_CRON_TIME |
5
5
  export const expiredSessionCleanupCronTime: string = process.env.EXPIRED_SESSION_CLEANUP_CRON_TIME || '0 1 */2 * * *'; // 默认每2个小时执行一次
6
6
  export const notificationCronConcurrency: number = Number(process.env.NOTIFICATION_CRON_CONCURRENCY) || 8; // 默认并发数为 8
7
7
  export const stripeInvoiceCronTime: string = process.env.STRIPE_INVOICE_CRON_TIME || '0 */30 * * * *'; // 默认每 30min 执行一次
8
+ export const stripePaymentCronTime: string = process.env.STRIPE_PAYMENT_CRON_TIME || '0 */20 * * * *'; // 默认每 20min 执行一次
8
9
  export const stripeSubscriptionCronTime: string = process.env.STRIPE_SUBSCRIPTION_CRON_TIME || '0 10 */8 * * *'; // 默认每 8小时 执行一次
10
+ export const revokeStakeCronTime: string = process.env.REVOKE_STAKE_CRON_TIME || '0 */5 * * * *'; // 默认每 5 min 行一次
9
11
 
10
12
  export default {
11
13
  ...env,
@@ -52,7 +52,7 @@ export class SubscriptionWillRenewEmailTemplate
52
52
  if (!subscription) {
53
53
  throw new Error(`Subscription not found: ${this.options.subscriptionId}`);
54
54
  }
55
- if (subscription.status !== 'active') {
55
+ if (subscription.isActive() === false) {
56
56
  throw new Error(`Subscription not active: ${this.options.subscriptionId}`);
57
57
  }
58
58
  if (subscription.isScheduledToCancel()) {
@@ -98,6 +98,33 @@ export const getMinRetryMail = (interval: string) => {
98
98
  return 15; // 18 hours
99
99
  };
100
100
 
101
+ export function getSubscriptionStakeSetup(items: TLineItemExpanded[], currencyId: string, billingThreshold = '0') {
102
+ const staking = {
103
+ licensed: new BN(0),
104
+ metered: new BN(0),
105
+ };
106
+
107
+ items.forEach((x) => {
108
+ const price = getSubscriptionItemPrice(x);
109
+ const unit = getPriceUintAmountByCurrency(price, currencyId);
110
+ const amount = new BN(unit).mul(new BN(x.quantity));
111
+ if (price.type === 'recurring' && price.recurring) {
112
+ if (price.recurring.usage_type === 'licensed') {
113
+ staking.licensed = staking.licensed.add(amount);
114
+ }
115
+ if (price.recurring.usage_type === 'metered') {
116
+ if (+billingThreshold) {
117
+ staking.metered = new BN(billingThreshold);
118
+ } else {
119
+ staking.metered = staking.metered.add(amount);
120
+ }
121
+ }
122
+ }
123
+ });
124
+
125
+ return staking;
126
+ }
127
+
101
128
  export function getSubscriptionCreateSetup(
102
129
  items: TLineItemExpanded[],
103
130
  currencyId: string,
@@ -109,8 +136,14 @@ export function getSubscriptionCreateSetup(
109
136
  items.forEach((x) => {
110
137
  const price = getSubscriptionItemPrice(x);
111
138
  const unit = getPriceUintAmountByCurrency(price, currencyId);
112
- if (price.type === 'one_time' || (price.type === 'recurring' && price.recurring?.usage_type === 'licensed')) {
113
- setup = setup.add(new BN(unit).mul(new BN(x.quantity)));
139
+ const amount = new BN(unit).mul(new BN(x.quantity));
140
+ if (price.type === 'recurring') {
141
+ if (price.recurring?.usage_type === 'licensed') {
142
+ setup = setup.add(amount);
143
+ }
144
+ }
145
+ if (price.type === 'one_time') {
146
+ setup = setup.add(amount);
114
147
  }
115
148
  });
116
149
 
@@ -67,28 +67,18 @@ export async function handleCheckoutSessionJob(job: CheckoutSessionJob): Promise
67
67
 
68
68
  // eslint-disable-next-line require-await
69
69
  export async function startCheckoutSessionQueue() {
70
- events.on('checkout.session.completed', (checkoutSession: CheckoutSession) => {
71
- ensurePassportIssued(checkoutSession).catch((err) => {
72
- logger.error('ensurePassportIssued failed', { error: err, checkoutSession: checkoutSession.id });
73
- });
74
- mintNftForCheckoutSession(checkoutSession.id).catch((err) => {
75
- logger.error('mintNftForCheckoutSession failed', { error: err, checkoutSession: checkoutSession.id });
76
- });
77
-
78
- // lock prices used
79
- Price.update(
80
- { locked: true },
81
- { where: { id: checkoutSession.line_items.map((x) => x.upsell_price_id || x.price_id) } }
82
- ).catch((err) => {
83
- logger.error('lock price on checkout session complete failed', {
84
- error: err,
85
- checkoutSession: checkoutSession.id,
86
- });
87
- });
70
+ // Auto populate subscription queue
71
+ const now = dayjs().unix();
72
+ const checkoutSessions = await CheckoutSession.findAll({
73
+ where: {
74
+ status: 'open',
75
+ expires_at: { [Op.lte]: now },
76
+ },
88
77
  });
89
78
 
90
- events.on('checkout.session.created', (checkoutSession: CheckoutSession) => {
91
- if (checkoutSession.expires_at) {
79
+ checkoutSessions.forEach(async (checkoutSession) => {
80
+ const exist = await checkoutSessionQueue.get(checkoutSession.id);
81
+ if (!exist) {
92
82
  checkoutSessionQueue.push({
93
83
  id: checkoutSession.id,
94
84
  job: { id: checkoutSession.id, action: 'expire' },
@@ -96,89 +86,103 @@ export async function startCheckoutSessionQueue() {
96
86
  });
97
87
  }
98
88
  });
89
+ }
90
+
91
+ checkoutSessionQueue.on('failed', ({ id, job, error }) => {
92
+ logger.error('CheckoutSession job failed', { id, job, error });
93
+ });
94
+
95
+ events.on('checkout.session.completed', (checkoutSession: CheckoutSession) => {
96
+ ensurePassportIssued(checkoutSession).catch((err) => {
97
+ logger.error('ensurePassportIssued failed', { error: err, checkoutSession: checkoutSession.id });
98
+ });
99
+ mintNftForCheckoutSession(checkoutSession.id).catch((err) => {
100
+ logger.error('mintNftForCheckoutSession failed', { error: err, checkoutSession: checkoutSession.id });
101
+ });
102
+
103
+ // lock prices used
104
+ Price.update(
105
+ { locked: true },
106
+ { where: { id: checkoutSession.line_items.map((x) => x.upsell_price_id || x.price_id) } }
107
+ ).catch((err) => {
108
+ logger.error('lock price on checkout session complete failed', {
109
+ error: err,
110
+ checkoutSession: checkoutSession.id,
111
+ });
112
+ });
113
+ });
99
114
 
100
- events.on('checkout.session.expired', async (checkoutSession: CheckoutSession) => {
101
- // Do some cleanup
102
- if (checkoutSession.invoice_id) {
103
- await InvoiceItem.destroy({ where: { invoice_id: checkoutSession.invoice_id } });
104
- await Invoice.destroy({ where: { id: checkoutSession.invoice_id } });
115
+ events.on('checkout.session.created', (checkoutSession: CheckoutSession) => {
116
+ if (checkoutSession.expires_at) {
117
+ checkoutSessionQueue.push({
118
+ id: checkoutSession.id,
119
+ job: { id: checkoutSession.id, action: 'expire' },
120
+ runAt: checkoutSession.expires_at,
121
+ });
122
+ }
123
+ });
124
+
125
+ events.on('checkout.session.expired', async (checkoutSession: CheckoutSession) => {
126
+ // Do some cleanup
127
+ if (checkoutSession.invoice_id) {
128
+ await InvoiceItem.destroy({ where: { invoice_id: checkoutSession.invoice_id } });
129
+ await Invoice.destroy({ where: { id: checkoutSession.invoice_id } });
130
+ logger.info('Invoice and InvoiceItem for checkout session deleted on expire', {
131
+ checkoutSession: checkoutSession.id,
132
+ invoice: checkoutSession.invoice_id,
133
+ });
134
+ } else {
135
+ // Do some reverse lookup if invoice is not related to checkout session
136
+ const invoice = await Invoice.findOne({ where: { checkout_session_id: checkoutSession.id } });
137
+ if (invoice) {
138
+ await InvoiceItem.destroy({ where: { invoice_id: invoice.id } });
139
+ await Invoice.destroy({ where: { id: invoice.id } });
105
140
  logger.info('Invoice and InvoiceItem for checkout session deleted on expire', {
106
141
  checkoutSession: checkoutSession.id,
107
- invoice: checkoutSession.invoice_id,
142
+ invoice: invoice.id,
108
143
  });
109
- } else {
110
- // Do some reverse lookup if invoice is not related to checkout session
111
- const invoice = await Invoice.findOne({ where: { checkout_session_id: checkoutSession.id } });
112
- if (invoice) {
113
- await InvoiceItem.destroy({ where: { invoice_id: invoice.id } });
114
- await Invoice.destroy({ where: { id: invoice.id } });
115
- logger.info('Invoice and InvoiceItem for checkout session deleted on expire', {
116
- checkoutSession: checkoutSession.id,
117
- invoice: invoice.id,
118
- });
119
- }
120
144
  }
121
- if (checkoutSession.setup_intent_id) {
122
- await SetupIntent.destroy({ where: { id: checkoutSession.setup_intent_id } });
123
- logger.info('SetupIntent for checkout session deleted on expire', {
124
- checkoutSession: checkoutSession.id,
125
- setupIntent: checkoutSession.setup_intent_id,
126
- });
127
- }
128
- if (checkoutSession.payment_intent_id && checkoutSession.payment_status !== 'paid') {
129
- await PaymentIntent.destroy({ where: { id: checkoutSession.payment_intent_id } });
130
- logger.info('PaymentIntent for checkout session deleted on expire', {
131
- checkoutSession: checkoutSession.id,
132
- paymentIntent: checkoutSession.payment_intent_id,
133
- });
134
- }
135
- if (checkoutSession.subscription_id) {
136
- await SubscriptionItem.destroy({ where: { subscription_id: checkoutSession.subscription_id } });
137
- await Subscription.destroy({ where: { id: checkoutSession.subscription_id } });
138
- logger.info('Subscription and SubscriptionItem for checkout session deleted on expire', {
145
+ }
146
+ if (checkoutSession.setup_intent_id) {
147
+ await SetupIntent.destroy({ where: { id: checkoutSession.setup_intent_id } });
148
+ logger.info('SetupIntent for checkout session deleted on expire', {
149
+ checkoutSession: checkoutSession.id,
150
+ setupIntent: checkoutSession.setup_intent_id,
151
+ });
152
+ }
153
+ if (checkoutSession.payment_intent_id && checkoutSession.payment_status !== 'paid') {
154
+ await PaymentIntent.destroy({ where: { id: checkoutSession.payment_intent_id } });
155
+ logger.info('PaymentIntent for checkout session deleted on expire', {
156
+ checkoutSession: checkoutSession.id,
157
+ paymentIntent: checkoutSession.payment_intent_id,
158
+ });
159
+ }
160
+ if (checkoutSession.subscription_id) {
161
+ await SubscriptionItem.destroy({ where: { subscription_id: checkoutSession.subscription_id } });
162
+ await Subscription.destroy({ where: { id: checkoutSession.subscription_id } });
163
+ logger.info('Subscription and SubscriptionItem for checkout session deleted on expire', {
164
+ checkoutSession: checkoutSession.id,
165
+ subscription: checkoutSession.subscription_id,
166
+ });
167
+ }
168
+
169
+ // update price lock status
170
+ for (const item of checkoutSession.line_items) {
171
+ const price = await Price.findByPk(item.price_id);
172
+ if (price?.locked) {
173
+ const used = await price.isUsed(false);
174
+ logger.info('Price used status recheck on expire', {
139
175
  checkoutSession: checkoutSession.id,
140
- subscription: checkoutSession.subscription_id,
176
+ priceId: item.price_id,
177
+ used,
141
178
  });
142
- }
143
-
144
- // update price lock status
145
- for (const item of checkoutSession.line_items) {
146
- const price = await Price.findByPk(item.price_id);
147
- if (price?.locked) {
148
- const used = await price.isUsed(false);
149
- logger.info('Price used status recheck on expire', {
179
+ if (!used) {
180
+ await price.update({ locked: false });
181
+ logger.info('Price for checkout session unlocked on expire', {
150
182
  checkoutSession: checkoutSession.id,
151
183
  priceId: item.price_id,
152
- used,
153
184
  });
154
- if (!used) {
155
- await price.update({ locked: false });
156
- logger.info('Price for checkout session unlocked on expire', {
157
- checkoutSession: checkoutSession.id,
158
- priceId: item.price_id,
159
- });
160
- }
161
185
  }
162
186
  }
163
- });
164
-
165
- // Auto populate subscription queue
166
- const now = dayjs().unix();
167
- const checkoutSessions = await CheckoutSession.findAll({
168
- where: {
169
- status: 'open',
170
- expires_at: { [Op.lte]: now },
171
- },
172
- });
173
-
174
- checkoutSessions.forEach(async (checkoutSession) => {
175
- const exist = await checkoutSessionQueue.get(checkoutSession.id);
176
- if (!exist) {
177
- checkoutSessionQueue.push({
178
- id: checkoutSession.id,
179
- job: { id: checkoutSession.id, action: 'expire' },
180
- runAt: checkoutSession.expires_at,
181
- });
182
- }
183
- });
184
- }
187
+ }
188
+ });