payment-kit 1.13.72 → 1.13.74

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 (66) hide show
  1. package/api/src/{schedule → crons}/base.ts +1 -1
  2. package/api/src/index.ts +7 -7
  3. package/api/src/integrations/stripe/handlers/customer.ts +24 -0
  4. package/api/src/integrations/stripe/handlers/index.ts +4 -0
  5. package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -1
  6. package/api/src/integrations/stripe/resource.ts +1 -1
  7. package/api/src/libs/audit.ts +34 -28
  8. package/api/src/libs/payment.ts +48 -4
  9. package/api/src/libs/queue/index.ts +18 -1
  10. package/api/src/libs/queue/store.ts +6 -5
  11. package/api/src/libs/session.ts +13 -12
  12. package/api/src/libs/subscription.ts +26 -0
  13. package/api/src/libs/util.ts +5 -1
  14. package/api/src/{jobs → queues}/checkout-session.ts +23 -1
  15. package/api/src/{jobs → queues}/invoice.ts +15 -6
  16. package/api/src/{jobs → queues}/payment.ts +182 -30
  17. package/api/src/{jobs → queues}/subscription.ts +36 -104
  18. package/api/src/{jobs → queues}/webhook.ts +2 -0
  19. package/api/src/routes/checkout-sessions.ts +72 -26
  20. package/api/src/routes/connect/collect.ts +2 -2
  21. package/api/src/routes/connect/pay.ts +1 -1
  22. package/api/src/routes/connect/setup.ts +10 -3
  23. package/api/src/routes/connect/shared.ts +98 -49
  24. package/api/src/routes/connect/subscribe.ts +10 -4
  25. package/api/src/routes/pricing-table.ts +2 -0
  26. package/api/src/routes/subscription-items.ts +1 -1
  27. package/api/src/routes/subscriptions.ts +434 -13
  28. package/api/src/store/migrate.ts +0 -1
  29. package/api/src/store/migrations/20231204-subupdate.ts +50 -0
  30. package/api/src/store/migrations/20231220-setup-intent.ts +22 -0
  31. package/api/src/store/models/checkout-session.ts +8 -0
  32. package/api/src/store/models/customer.ts +52 -15
  33. package/api/src/store/models/invoice-item.ts +6 -1
  34. package/api/src/store/models/invoice.ts +41 -22
  35. package/api/src/store/models/payment-intent.ts +4 -0
  36. package/api/src/store/models/setup-intent.ts +4 -0
  37. package/api/src/store/models/subscription-item.ts +0 -4
  38. package/api/src/store/models/subscription.ts +77 -44
  39. package/api/src/store/models/types.ts +1 -0
  40. package/api/src/store/sequelize.ts +6 -0
  41. package/api/third.d.ts +2 -0
  42. package/blocklet.yml +1 -1
  43. package/jest.config.js +14 -0
  44. package/package.json +24 -19
  45. package/src/components/blockchain/tx.tsx +20 -11
  46. package/src/components/checkout/form/index.tsx +1 -1
  47. package/src/components/invoice/table.tsx +58 -19
  48. package/src/components/layout/admin.tsx +17 -5
  49. package/src/components/portal/invoice/list.tsx +12 -8
  50. package/src/components/portal/subscription/list.tsx +114 -77
  51. package/src/components/subscription/status.tsx +21 -19
  52. package/src/global.css +4 -0
  53. package/src/locales/en.tsx +14 -1
  54. package/src/locales/zh.tsx +14 -0
  55. package/src/pages/admin/customers/customers/detail.tsx +47 -3
  56. package/src/pages/admin/overview.tsx +21 -1
  57. package/src/pages/admin/payments/intents/detail.tsx +12 -3
  58. package/src/pages/customer/invoice.tsx +15 -1
  59. package/src/pages/customer/subscription/index.tsx +9 -2
  60. package/tests/api/libs/subscription.spec.ts +45 -0
  61. /package/api/src/{schedule → crons}/index.ts +0 -0
  62. /package/api/src/{schedule → crons}/interface/diff.ts +0 -0
  63. /package/api/src/{schedule → crons}/subscription-trail-will-end.ts +0 -0
  64. /package/api/src/{schedule → crons}/subscription-will-renew.ts +0 -0
  65. /package/api/src/{jobs → queues}/event.ts +0 -0
  66. /package/api/src/{jobs → queues}/notification.ts +0 -0
@@ -4,10 +4,12 @@ import dayjs from '../libs/dayjs';
4
4
  import CustomError from '../libs/error';
5
5
  import { events } from '../libs/event';
6
6
  import logger from '../libs/logger';
7
- import { getGasPayerExtra, isDelegationSufficientForPayment } from '../libs/payment';
7
+ import { getGasPayerExtra, isBalanceSufficientForPayment, isDelegationSufficientForPayment } from '../libs/payment';
8
8
  import createQueue from '../libs/queue';
9
+ import { getDaysUntilDue, getDueUnit } from '../libs/subscription';
9
10
  import { MAX_RETRY_COUNT, MIN_RETRY_MAIL, getNextRetry } from '../libs/util';
10
11
  import { CheckoutSession } from '../store/models/checkout-session';
12
+ import { Customer } from '../store/models/customer';
11
13
  import { Invoice } from '../store/models/invoice';
12
14
  import { PaymentCurrency } from '../store/models/payment-currency';
13
15
  import { PaymentIntent } from '../store/models/payment-intent';
@@ -21,7 +23,7 @@ type PaymentJob = {
21
23
  retryOnError?: boolean;
22
24
  };
23
25
 
24
- export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
26
+ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent, invoiceUpdates: any = {}) => {
25
27
  let invoice;
26
28
  if (paymentIntent.invoice_id) {
27
29
  invoice = await Invoice.findByPk(paymentIntent.invoice_id);
@@ -49,8 +51,9 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
49
51
  attempt_count: invoice.attempt_count + 1,
50
52
  attempted: true,
51
53
  status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
54
+ ...invoiceUpdates,
52
55
  });
53
- logger.info(`Invoice ${invoice.id} updated on payment done: ${paymentIntent.id}`);
56
+ logger.info(`Invoice ${invoice.id} updated on payment done: ${paymentIntent.id}`, invoiceUpdates);
54
57
  }
55
58
 
56
59
  if (invoice.subscription_id) {
@@ -59,10 +62,15 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
59
62
  if (subscription.status === 'incomplete') {
60
63
  await subscription.update({ status: subscription.trail_end ? 'trialing' : 'active' });
61
64
  logger.info(`Subscription ${subscription.id} updated on payment done ${invoice.id}`);
62
- } else {
63
- // FIXME: possible error here
64
- await subscription.update({ status: 'active' });
65
- logger.info(`Subscription ${subscription.id} moved to active after payment done ${paymentIntent.id}`);
65
+ } else if (subscription.status === 'past_due') {
66
+ if (subscription.cancel_at_period_end && subscription.cancelation_details?.reason === 'payment_failed') {
67
+ // @ts-ignore
68
+ await subscription.update({ status: 'active', cancel_at_period_end: false, cancelation_details: null });
69
+ logger.info(`Subscription ${subscription.id} moved to active after payment done ${paymentIntent.id}`);
70
+ } else {
71
+ await subscription.update({ status: 'active' });
72
+ logger.info(`Subscription ${subscription.id} moved to active after payment done ${paymentIntent.id}`);
73
+ }
66
74
  }
67
75
  }
68
76
 
@@ -84,6 +92,124 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
84
92
  }
85
93
  };
86
94
 
95
+ type Updates = {
96
+ retry: {
97
+ payment: Partial<PaymentIntent>;
98
+ invoice: Partial<Invoice>;
99
+ };
100
+ terminate: {
101
+ payment: Partial<PaymentIntent>;
102
+ invoice: Partial<Invoice>;
103
+ };
104
+ };
105
+
106
+ export const handlePaymentFailed = async (paymentIntent: PaymentIntent, invoice: Invoice, error: PaymentError) => {
107
+ const now = dayjs().unix();
108
+ const attemptCount = invoice.attempt_count + 1;
109
+
110
+ const updates: Updates = {
111
+ retry: {
112
+ payment: { status: 'requires_capture', last_payment_error: error },
113
+ invoice: {
114
+ attempt_count: attemptCount,
115
+ attempted: true,
116
+ next_payment_attempt: getNextRetry(attemptCount),
117
+ },
118
+ },
119
+ terminate: {
120
+ payment: { status: 'requires_action', last_payment_error: error },
121
+ invoice: {
122
+ status: 'uncollectible',
123
+ attempt_count: attemptCount,
124
+ attempted: true,
125
+ status_transitions: { ...invoice.status_transitions, marked_uncollectible_at: now },
126
+ },
127
+ },
128
+ };
129
+
130
+ if (!invoice.subscription_id) {
131
+ return updates.retry;
132
+ }
133
+ const subscription = await Subscription.findByPk(invoice.subscription_id);
134
+ if (!subscription) {
135
+ return updates.retry;
136
+ }
137
+
138
+ if (subscription.status !== 'active') {
139
+ return updates.retry;
140
+ }
141
+
142
+ // check current period
143
+ if (subscription.current_period_end <= now) {
144
+ await subscription.update({
145
+ status: 'canceled',
146
+ canceled_at: now,
147
+ cancelation_details: {
148
+ comment: 'exceed_current_period',
149
+ feedback: 'other',
150
+ reason: 'payment_failed',
151
+ },
152
+ });
153
+ logger.warn('Subscription moved to canceled after retry exceeds current_period_end', {
154
+ subscription: subscription.id,
155
+ payment: paymentIntent.id,
156
+ current_period_end: subscription.current_period_end,
157
+ now,
158
+ });
159
+ return updates.terminate;
160
+ }
161
+
162
+ // check days until due
163
+ const daysUntilDue = getDaysUntilDue(subscription);
164
+ if (typeof daysUntilDue === 'number') {
165
+ const dueUnit = getDueUnit(subscription.pending_invoice_item_interval.interval);
166
+ const gracePeriodStart = subscription.current_period_start;
167
+ const graceDuration = daysUntilDue ? daysUntilDue * dueUnit : 0;
168
+ logger.debug('handlePaymentFailed.checkDue', { now, daysUntilDue, dueUnit, gracePeriodStart, graceDuration });
169
+ if (gracePeriodStart + graceDuration <= now) {
170
+ await subscription.update({
171
+ status: 'past_due',
172
+ cancel_at_period_end: true,
173
+ cancelation_details: {
174
+ comment: 'past_due',
175
+ feedback: 'other',
176
+ reason: 'payment_failed',
177
+ },
178
+ });
179
+ logger.warn('Subscription moved to past_due after payment failed', {
180
+ subscription: subscription.id,
181
+ payment: paymentIntent.id,
182
+ gracePeriodStart,
183
+ graceDuration,
184
+ dueUnit,
185
+ });
186
+ return updates.terminate;
187
+ }
188
+ }
189
+
190
+ // check max retry
191
+ if (invoice.attempt_count > MAX_RETRY_COUNT) {
192
+ await subscription.update({
193
+ status: 'past_due',
194
+ cancel_at_period_end: true,
195
+ cancelation_details: {
196
+ comment: 'exceed_max_retry',
197
+ feedback: 'other',
198
+ reason: 'payment_failed',
199
+ },
200
+ });
201
+ logger.warn('Subscription moved to past_due after max retry', {
202
+ subscription: subscription.id,
203
+ payment: paymentIntent.id,
204
+ attempt_count: invoice.attempt_count,
205
+ });
206
+
207
+ return updates.terminate;
208
+ }
209
+
210
+ return updates.retry;
211
+ };
212
+
87
213
  export const handlePayment = async (job: PaymentJob) => {
88
214
  logger.info('handle payment', job);
89
215
 
@@ -116,9 +242,16 @@ export const handlePayment = async (job: PaymentJob) => {
116
242
  return;
117
243
  }
118
244
 
245
+ const customer = await Customer.findByPk(paymentIntent.customer_id);
246
+ if (!customer) {
247
+ logger.warn(`Customer not found: ${paymentIntent.customer_id}`);
248
+ return;
249
+ }
250
+
119
251
  const invoice = await Invoice.findByPk(paymentIntent.invoice_id);
120
252
  const paymentSettings = invoice?.payment_settings || job.paymentSettings;
121
253
  if (!paymentSettings) {
254
+ await paymentIntent.update({ status: 'requires_action' });
122
255
  logger.warn('Payment settings not found:', job);
123
256
  return;
124
257
  }
@@ -132,7 +265,38 @@ export const handlePayment = async (job: PaymentJob) => {
132
265
  const client = paymentMethod.getOcapClient();
133
266
  const payer = paymentSettings?.payment_method_options.arcblock?.payer;
134
267
 
135
- // check balance before capture
268
+ // if we can complete purchase with customer balance
269
+ const balance = isBalanceSufficientForPayment({
270
+ paymentMethod,
271
+ paymentCurrency,
272
+ customer,
273
+ amount: paymentIntent.amount,
274
+ });
275
+ if (balance.sufficient) {
276
+ const tmp = await customer.decreaseTokenBalance(paymentCurrency.id, paymentIntent.amount);
277
+ logger.info(`PaymentIntent capture done: ${paymentIntent.id} with customer balance`, tmp);
278
+ await paymentIntent.update({
279
+ status: 'succeeded',
280
+ amount: '0', // update payment intent amount to 0
281
+ amount_received: '0',
282
+ payment_details: {
283
+ arcblock: {
284
+ tx_hash: '',
285
+ payer: payer as string,
286
+ },
287
+ },
288
+ });
289
+
290
+ await handlePaymentSucceed(paymentIntent, {
291
+ starting_token_balance: tmp.starting,
292
+ ending_token_balance: tmp.ending,
293
+ });
294
+ return;
295
+ }
296
+
297
+ // FIXME: support partial payment from balance
298
+
299
+ // check balance before capture with transaction
136
300
  result = await isDelegationSufficientForPayment({
137
301
  paymentMethod,
138
302
  paymentCurrency,
@@ -208,7 +372,10 @@ export const handlePayment = async (job: PaymentJob) => {
208
372
  });
209
373
  }
210
374
  } else if (invoice) {
375
+ // This means we have tried to capture this invoice before, since the retry is managed by invoice queue
211
376
  const attemptCount = invoice.attempt_count + 1;
377
+
378
+ // 只有在重试次数超过阈值的时候才发送邮件,不然邮件频率太高了,初次邮件时首次失败 6 小时后
212
379
  if (attemptCount >= MIN_RETRY_MAIL && invoice.billing_reason === 'subscription_cycle') {
213
380
  events.emit('customer.subscription.renew_failed', {
214
381
  invoice,
@@ -216,34 +383,19 @@ export const handlePayment = async (job: PaymentJob) => {
216
383
  });
217
384
  }
218
385
 
219
- if (invoice.attempt_count > MAX_RETRY_COUNT) {
220
- await paymentIntent.update({ status: 'requires_action', last_payment_error: error });
221
- await invoice.update({
222
- status: 'uncollectible',
223
- attempt_count: attemptCount,
224
- attempted: true,
225
- status_transitions: { ...invoice.status_transitions, marked_uncollectible_at: dayjs().unix() },
226
- });
386
+ const updates = await handlePaymentFailed(paymentIntent, invoice, error);
387
+ await paymentIntent.update(updates.payment);
388
+ await invoice.update(updates.invoice);
227
389
 
228
- // FIXME: send email to customer, pause subscription
229
- logger.error('PaymentIntent capture failed after max retry', { id: paymentIntent.id });
230
- } else {
231
- const retryAt = getNextRetry(attemptCount);
232
-
233
- await paymentIntent.update({ status: 'requires_capture', last_payment_error: error });
234
- await invoice.update({
235
- attempt_count: attemptCount,
236
- attempted: true,
237
- next_payment_attempt: retryAt,
238
- });
239
- logger.error('PaymentIntent capture retry scheduled', { id: paymentIntent.id, retryAt });
240
-
241
- // reschedule next attempt
390
+ // reschedule next attempt
391
+ const retryAt = updates.invoice.next_payment_attempt;
392
+ if (retryAt) {
242
393
  paymentQueue.push({
243
394
  id: paymentIntent.id,
244
395
  job: { paymentIntentId: paymentIntent.id, retryOnError: job.retryOnError },
245
396
  runAt: retryAt,
246
397
  });
398
+ logger.error('PaymentIntent capture retry scheduled', { id: paymentIntent.id, retryAt });
247
399
  }
248
400
  }
249
401
  }
@@ -1,14 +1,13 @@
1
- import { BN } from '@ocap/util';
2
1
  import type { LiteralUnion } from 'type-fest';
3
2
 
4
3
  import dayjs from '../libs/dayjs';
5
4
  import logger from '../libs/logger';
6
5
  import createQueue from '../libs/queue';
7
6
  import { getStatementDescriptor, getSubscriptionCycleAmount, getSubscriptionCycleSetup } from '../libs/session';
7
+ import { ensureInvoiceAndItems } from '../routes/connect/shared';
8
8
  import { PaymentCurrency, PaymentMethod, UsageRecord } from '../store/models';
9
9
  import { Customer } from '../store/models/customer';
10
10
  import { Invoice } from '../store/models/invoice';
11
- import { InvoiceItem } from '../store/models/invoice-item';
12
11
  import { Price } from '../store/models/price';
13
12
  import { Subscription } from '../store/models/subscription';
14
13
  import { SubscriptionItem } from '../store/models/subscription-item';
@@ -19,6 +18,8 @@ type SubscriptionJob = {
19
18
  action?: LiteralUnion<'cycle' | 'cancel' | 'pause' | 'resume', string>;
20
19
  };
21
20
 
21
+ const EXPECTED_SUBSCRIPTION_STATUS = ['trialing', 'active', 'paused', 'past_due'];
22
+
22
23
  // generate invoice for subscription periodically
23
24
  export const handleSubscription = async (job: SubscriptionJob) => {
24
25
  logger.info('handle subscription', job);
@@ -28,7 +29,7 @@ export const handleSubscription = async (job: SubscriptionJob) => {
28
29
  logger.warn(`Subscription not found: ${job.subscriptionId}`);
29
30
  return;
30
31
  }
31
- if (['trialing', 'active', 'paused'].includes(subscription.status) === false) {
32
+ if (EXPECTED_SUBSCRIPTION_STATUS.includes(subscription.status) === false) {
32
33
  logger.warn(`Subscription status not expected: ${job.subscriptionId}`);
33
34
  return;
34
35
  }
@@ -41,7 +42,7 @@ export const handleSubscription = async (job: SubscriptionJob) => {
41
42
  const now = dayjs().unix();
42
43
 
43
44
  // Do we need to cancel the subscription
44
- if (subscription.status === 'active') {
45
+ if (subscription.isImmutable() === false) {
45
46
  if (subscription.cancel_at_period_end) {
46
47
  await subscription.update({ status: 'canceled', canceled_at: now });
47
48
  logger.warn(`Subscription canceled on period end: ${job.subscriptionId}`);
@@ -55,11 +56,7 @@ export const handleSubscription = async (job: SubscriptionJob) => {
55
56
  }
56
57
 
57
58
  // Do we need to resume the subscription
58
- if (
59
- subscription.status === 'paused' &&
60
- subscription.pause_collection?.resumes_at &&
61
- subscription.pause_collection?.resumes_at <= now
62
- ) {
59
+ if (subscription.pause_collection?.resumes_at && subscription.pause_collection?.resumes_at <= now) {
63
60
  await subscription.update({ status: 'active', pause_collection: undefined });
64
61
  logger.warn(`Subscription resumed as scheduled: ${job.subscriptionId}`);
65
62
  }
@@ -118,14 +115,14 @@ export const handleSubscription = async (job: SubscriptionJob) => {
118
115
 
119
116
  // set invoice status if subscription paused
120
117
  let status = 'open';
121
- if (subscription.status === 'paused') {
122
- if (subscription.pause_collection?.behavior === 'mark_uncollectible') {
118
+ if (subscription.pause_collection) {
119
+ if (subscription.pause_collection.behavior === 'mark_uncollectible') {
123
120
  status = 'uncollectible';
124
121
  }
125
- if (subscription.pause_collection?.behavior === 'void') {
122
+ if (subscription.pause_collection.behavior === 'void') {
126
123
  status = 'void';
127
124
  }
128
- if (subscription.pause_collection?.behavior === 'keep_as_draft') {
125
+ if (subscription.pause_collection.behavior === 'keep_as_draft') {
129
126
  status = 'draft';
130
127
  }
131
128
  }
@@ -166,95 +163,30 @@ export const handleSubscription = async (job: SubscriptionJob) => {
166
163
  })
167
164
  );
168
165
 
169
- const amount = getSubscriptionCycleAmount(expandedItems, currency);
170
-
171
- // create invoice
172
- const invoice = await Invoice.create({
173
- livemode: subscription.livemode,
174
- number: await customer.getInvoiceNumber(),
175
- description: 'Subscription cycle',
176
- statement_descriptor: getStatementDescriptor(expandedItems),
177
- period_start: setup.period.start,
178
- period_end: setup.period.end,
179
-
180
- auto_advance: true,
181
- paid: false,
182
- paid_out_of_band: false,
183
-
184
- status,
185
- collection_method: 'charge_automatically',
186
- billing_reason: 'subscription_cycle',
187
-
188
- currency_id: subscription.currency_id,
189
- customer_id: customer.id,
190
- payment_intent_id: '',
191
- subscription_id: subscription?.id,
192
- checkout_session_id: '',
193
-
194
- subtotal: amount.total,
195
- subtotal_excluding_tax: amount.total,
196
- tax: '0',
197
- total: amount.total,
198
- amount_due: amount.total,
199
- amount_paid: '0',
200
- amount_remaining: amount.total,
201
- amount_shipping: '0',
202
-
203
- starting_balance: '0',
204
- ending_balance: '0',
205
-
206
- attempt_count: 0,
207
- attempted: false,
208
- // next_payment_attempt: undefined,
209
-
210
- custom_fields: [],
211
- customer_address: customer.address,
212
- customer_email: customer.email,
213
- customer_name: customer.name,
214
- customer_phone: customer.phone,
215
-
216
- discounts: [],
217
- total_discount_amounts: [],
218
-
219
- due_date: undefined, // The date on which payment for this invoice is due
220
- effective_at: dayjs().unix(), // The date when this invoice is in effect
221
- status_transitions: {
222
- finalized_at: dayjs().unix(),
223
- },
224
-
225
- payment_settings: subscription.payment_settings,
226
- default_payment_method_id: subscription.default_payment_method_id as string,
227
-
228
- account_country: '',
229
- account_name: '',
230
- metadata: {},
166
+ const amount = getSubscriptionCycleAmount(expandedItems, currency.id);
167
+
168
+ const { invoice } = await ensureInvoiceAndItems({
169
+ customer,
170
+ subscription,
171
+ trailing: false,
172
+ metered: true,
173
+ lineItems: expandedItems,
174
+ props: {
175
+ livemode: subscription.livemode,
176
+ description: 'Subscription cycle',
177
+ statement_descriptor: getStatementDescriptor(expandedItems),
178
+ period_start: setup.period.start,
179
+ period_end: setup.period.end,
180
+ auto_advance: true,
181
+ status,
182
+ billing_reason: 'subscription_cycle',
183
+ currency_id: subscription.currency_id,
184
+ total: amount.total,
185
+ payment_settings: subscription.payment_settings,
186
+ default_payment_method_id: subscription.default_payment_method_id,
187
+ metadata: {},
188
+ } as Invoice,
231
189
  });
232
- logger.info(`Invoice created for subscription ${subscription.id}: ${invoice.id}`);
233
-
234
- // create invoice items
235
- await Promise.all(
236
- expandedItems.map((x: any) =>
237
- InvoiceItem.create({
238
- livemode: subscription.livemode,
239
- amount: new BN(x.price.unit_amount).mul(new BN(x.quantity)).toString(),
240
- quantity: x.quantity,
241
- description: x.price.product.name,
242
- period: { start: setup.period.start, end: setup.period.end },
243
- currency_id: subscription.currency_id,
244
- customer_id: customer.id,
245
- price_id: x.price_id,
246
- invoice_id: invoice.id,
247
- subscription_id: subscription.id,
248
- subscription_item_id: subscriptionItems.find((si) => si.price_id === x.price_id)?.id,
249
- discountable: false,
250
- discounts: [],
251
- discount_amounts: [],
252
- proration: false,
253
- proration_details: {},
254
- metadata: {},
255
- })
256
- )
257
- );
258
190
 
259
191
  // schedule invoice job
260
192
  invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: true } });
@@ -268,8 +200,8 @@ export const handleSubscription = async (job: SubscriptionJob) => {
268
200
  });
269
201
  logger.info(`Subscription updated for new billing cycle: ${subscription.id}`);
270
202
 
271
- // schedule next billing cycle if we are not cancelling
272
- if (!subscription.cancel_at_period_end) {
203
+ // schedule next billing cycle if we are not in terminal state
204
+ if (subscription.isActive()) {
273
205
  subscriptionQueue.push({
274
206
  id: subscription.id,
275
207
  job: { subscriptionId: subscription.id, action: 'cycle' },
@@ -292,7 +224,7 @@ export const subscriptionQueue = createQueue<SubscriptionJob>({
292
224
  export const startSubscriptionQueue = async () => {
293
225
  const subscriptions = await Subscription.findAll({
294
226
  where: {
295
- status: ['trialing', 'active', 'paused'],
227
+ status: EXPECTED_SUBSCRIPTION_STATUS,
296
228
  },
297
229
  });
298
230
 
@@ -101,6 +101,8 @@ export const handleWebhook = async (job: WebhookJob) => {
101
101
  runAt: getNextRetry(retryCount),
102
102
  });
103
103
  });
104
+ } else {
105
+ await event.decrement('pending_webhooks');
104
106
  }
105
107
  }
106
108
  };