payment-kit 1.13.91 → 1.13.93

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 (41) hide show
  1. package/api/src/crons/index.ts +8 -1
  2. package/api/src/index.ts +2 -0
  3. package/api/src/libs/audit.ts +28 -34
  4. package/api/src/libs/env.ts +1 -0
  5. package/api/src/libs/payment.ts +2 -11
  6. package/api/src/libs/session.ts +1 -1
  7. package/api/src/libs/util.ts +8 -5
  8. package/api/src/queues/subscription.ts +166 -112
  9. package/api/src/routes/checkout-sessions.ts +41 -39
  10. package/api/src/routes/connect/collect.ts +12 -12
  11. package/api/src/routes/connect/setup.ts +8 -11
  12. package/api/src/routes/connect/shared.ts +81 -20
  13. package/api/src/routes/connect/subscribe.ts +8 -11
  14. package/api/src/routes/connect/update.ts +134 -0
  15. package/api/src/routes/pricing-table.ts +9 -121
  16. package/api/src/routes/subscriptions.ts +416 -141
  17. package/api/src/store/models/index.ts +3 -0
  18. package/api/src/store/models/invoice.ts +2 -0
  19. package/api/src/store/models/pricing-table.ts +125 -1
  20. package/api/src/store/models/subscription.ts +4 -0
  21. package/api/src/store/models/types.ts +8 -0
  22. package/api/tests/libs/util.spec.ts +6 -6
  23. package/blocklet.yml +3 -1
  24. package/package.json +7 -7
  25. package/src/app.tsx +12 -4
  26. package/src/components/checkout/form/address.tsx +41 -34
  27. package/src/components/checkout/form/index.tsx +1 -1
  28. package/src/components/checkout/pricing-table.tsx +205 -0
  29. package/src/components/payment-link/product-select.tsx +13 -3
  30. package/src/components/portal/invoice/list.tsx +1 -1
  31. package/src/components/portal/subscription/actions.tsx +153 -0
  32. package/src/components/portal/subscription/list.tsx +21 -150
  33. package/src/components/subscription/metrics.tsx +46 -0
  34. package/src/contexts/products.tsx +2 -1
  35. package/src/libs/util.ts +43 -0
  36. package/src/locales/en.tsx +15 -1
  37. package/src/locales/zh.tsx +16 -2
  38. package/src/pages/admin/billing/subscriptions/detail.tsx +2 -34
  39. package/src/pages/checkout/pricing-table.tsx +9 -158
  40. package/src/pages/customer/subscription/{index.tsx → detail.tsx} +6 -36
  41. package/src/pages/customer/subscription/update.tsx +281 -0
@@ -1,7 +1,8 @@
1
1
  import Cron from '@abtnode/cron';
2
2
 
3
- import { notificationCronTime } from '../libs/env';
3
+ import { notificationCronTime, subscriptionCronTime } from '../libs/env';
4
4
  import logger from '../libs/logger';
5
+ import { startSubscriptionQueue } from '../queues/subscription';
5
6
  import { SubscriptionTrailWillEndSchedule } from './subscription-trail-will-end';
6
7
  import { SubscriptionWillRenewSchedule } from './subscription-will-renew';
7
8
 
@@ -21,6 +22,12 @@ function init() {
21
22
  fn: () => new SubscriptionTrailWillEndSchedule().run(),
22
23
  options: { runOnInit: true },
23
24
  },
25
+ {
26
+ name: 'subscription.schedule.retry',
27
+ time: subscriptionCronTime,
28
+ fn: startSubscriptionQueue,
29
+ options: { runOnInit: false },
30
+ },
24
31
  ],
25
32
  onError: (error: Error, name: string) => {
26
33
  logger.error('run job failed', { name, error: error.message, stack: error.stack });
package/api/src/index.ts CHANGED
@@ -26,6 +26,7 @@ import collectHandlers from './routes/connect/collect';
26
26
  import payHandlers from './routes/connect/pay';
27
27
  import setupHandlers from './routes/connect/setup';
28
28
  import subscribeHandlers from './routes/connect/subscribe';
29
+ import updateHandlers from './routes/connect/update';
29
30
  import { initialize } from './store/models';
30
31
  import { sequelize } from './store/sequelize';
31
32
 
@@ -53,6 +54,7 @@ handlers.attach(Object.assign({ app: router }, collectHandlers));
53
54
  handlers.attach(Object.assign({ app: router }, payHandlers));
54
55
  handlers.attach(Object.assign({ app: router }, setupHandlers));
55
56
  handlers.attach(Object.assign({ app: router }, subscribeHandlers));
57
+ handlers.attach(Object.assign({ app: router }, updateHandlers));
56
58
 
57
59
  router.use('/api', routes);
58
60
 
@@ -16,24 +16,21 @@ export async function createEvent(scope: string, type: LiteralUnion<EventType, s
16
16
  data.previous_attributes = pick(model._previousDataValues, options.fields);
17
17
  }
18
18
 
19
- const event = await Event.create(
20
- {
21
- type,
22
- api_version: API_VERSION,
23
- livemode: !!model.livemode,
24
- object_id: model.id,
25
- object_type: scope,
26
- data,
27
- request: {
28
- // FIXME:
29
- id: '',
30
- idempotency_key: '',
31
- },
32
- metadata: {},
33
- pending_webhooks: 99, // force all events goto the event queue
19
+ const event = await Event.create({
20
+ type,
21
+ api_version: API_VERSION,
22
+ livemode: !!model.livemode,
23
+ object_id: model.id,
24
+ object_type: scope,
25
+ data,
26
+ request: {
27
+ // FIXME:
28
+ id: '',
29
+ idempotency_key: '',
34
30
  },
35
- { transaction: null }
36
- );
31
+ metadata: {},
32
+ pending_webhooks: 99, // force all events goto the event queue
33
+ });
37
34
 
38
35
  events.emit('event.created', { id: event.id });
39
36
  events.emit(event.type, data.object);
@@ -61,24 +58,21 @@ export async function createStatusEvent(
61
58
  }
62
59
 
63
60
  const suffix = config[data.object.status];
64
- const event = await Event.create(
65
- {
66
- type: [prefix, suffix].join('.'),
67
- api_version: API_VERSION,
68
- livemode: !!model.livemode,
69
- object_id: model.id,
70
- object_type: scope,
71
- data,
72
- request: {
73
- // FIXME:
74
- id: '',
75
- idempotency_key: '',
76
- },
77
- metadata: {},
78
- pending_webhooks: 99, // force all events goto the event queue
61
+ const event = await Event.create({
62
+ type: [prefix, suffix].join('.'),
63
+ api_version: API_VERSION,
64
+ livemode: !!model.livemode,
65
+ object_id: model.id,
66
+ object_type: scope,
67
+ data,
68
+ request: {
69
+ // FIXME:
70
+ id: '',
71
+ idempotency_key: '',
79
72
  },
80
- { transaction: null }
81
- );
73
+ metadata: {},
74
+ pending_webhooks: 99, // force all events goto the event queue
75
+ });
82
76
 
83
77
  events.emit('event.created', { id: event.id });
84
78
  events.emit(event.type, data.object);
@@ -1,5 +1,6 @@
1
1
  import env from '@blocklet/sdk/lib/env';
2
2
 
3
+ export const subscriptionCronTime: string = process.env.SUBSCRIPTION_CRON_TIME || '0 */30 * * * *'; // 默认每 30 min 行一次
3
4
  export const notificationCronTime: string = process.env.NOTIFICATION_CRON_TIME || '0 5 */6 * * *'; // 默认每6个小时执行一次
4
5
  export const notificationCronConcurrency: number = Number(process.env.NOTIFICATION_CRON_CONCURRENCY) || 8; // 默认并发数为 8
5
6
 
@@ -8,15 +8,7 @@ import { BN, fromUnitToToken } from '@ocap/util';
8
8
  import cloneDeep from 'lodash/cloneDeep';
9
9
  import type { LiteralUnion } from 'type-fest';
10
10
 
11
- import {
12
- CheckoutSession,
13
- Invoice,
14
- PaymentCurrency,
15
- PaymentIntent,
16
- PaymentMethod,
17
- TCustomer,
18
- TLineItemExpanded,
19
- } from '../store/models';
11
+ import { Invoice, PaymentCurrency, PaymentIntent, PaymentMethod, TCustomer, TLineItemExpanded } from '../store/models';
20
12
  import type { TPaymentCurrency } from '../store/models/payment-currency';
21
13
  import { blocklet, wallet } from './auth';
22
14
  import { OCAP_PAYMENT_TX_TYPE } from './util';
@@ -209,7 +201,7 @@ export async function getPaymentDetail(userDid: string, invoice: Invoice): Promi
209
201
  }
210
202
 
211
203
  export async function getTokenLimitsForDelegation(
212
- checkoutSession: CheckoutSession,
204
+ items: TLineItemExpanded[],
213
205
  paymentMethod: PaymentMethod,
214
206
  paymentCurrency: PaymentCurrency,
215
207
  address: string,
@@ -218,7 +210,6 @@ export async function getTokenLimitsForDelegation(
218
210
  const client = paymentMethod.getOcapClient();
219
211
  const { state } = await client.getDelegateState({ address });
220
212
 
221
- const items = checkoutSession.line_items as TLineItemExpanded[];
222
213
  const hasMetered = items.some((x) => x.price.recurring?.usage_type === 'metered');
223
214
  const allowance = hasMetered ? '0' : amount;
224
215
 
@@ -109,7 +109,7 @@ export function getSubscriptionCreateSetup(items: TLineItemExpanded[], currencyI
109
109
  setup = setup.add(new BN(unit).mul(new BN(x.quantity)));
110
110
  if (price.type === 'recurring') {
111
111
  if (trialInDays === 0) {
112
- subscription = setup.add(new BN(unit).mul(new BN(x.quantity)));
112
+ subscription = subscription.add(new BN(unit).mul(new BN(x.quantity)));
113
113
  }
114
114
  }
115
115
  });
@@ -152,15 +152,18 @@ export function getTxMetadata(extra: Record<string, any> = {}): any {
152
152
  };
153
153
  }
154
154
 
155
- export function getMetadataFromQuery(query: Record<string, any> = {}): Record<string, any> {
156
- const metadata: Record<string, any> = {};
155
+ export function getDataObjectFromQuery(
156
+ query: Record<string, any> = {},
157
+ prefix: string = 'metadata'
158
+ ): Record<string, any> {
159
+ const result: Record<string, any> = {};
157
160
  Object.keys(query).forEach((key) => {
158
- if (key.startsWith('metadata.') && query[key]) {
159
- metadata[key.replace('metadata.', '')] = query[key];
161
+ if (key.startsWith(`${prefix}.`) && query[key]) {
162
+ result[key.replace(`${prefix}.`, '')] = query[key];
160
163
  }
161
164
  });
162
165
 
163
- return metadata;
166
+ return result;
164
167
  }
165
168
 
166
169
  // @FIXME: 这个应该封装在某个通用类库里面 @jianchao @wangshijun
@@ -20,116 +20,41 @@ type SubscriptionJob = {
20
20
 
21
21
  const EXPECTED_SUBSCRIPTION_STATUS = ['trialing', 'active', 'paused', 'past_due'];
22
22
 
23
- // generate invoice for subscription periodically
24
- export const handleSubscription = async (job: SubscriptionJob) => {
25
- logger.info('handle subscription', job);
26
-
27
- const subscription = await Subscription.findByPk(job.subscriptionId);
28
- if (!subscription) {
29
- logger.warn(`Subscription not found: ${job.subscriptionId}`);
30
- return;
31
- }
32
- if (EXPECTED_SUBSCRIPTION_STATUS.includes(subscription.status) === false) {
33
- logger.warn(`Subscription status not expected: ${job.subscriptionId}`);
34
- return;
35
- }
36
- const supportAutoCharge = await PaymentMethod.supportAutoCharge(subscription.default_payment_method_id);
37
- if (supportAutoCharge === false) {
38
- logger.warn(`Subscription does not support auto charge: ${job.subscriptionId}`);
39
- return;
40
- }
41
-
42
- const now = dayjs().unix();
43
-
44
- // Do we need to cancel the subscription
45
- if (subscription.isImmutable() === false) {
46
- if (subscription.cancel_at_period_end) {
47
- await subscription.update({ status: 'canceled', canceled_at: now });
48
- logger.warn(`Subscription canceled on period end: ${job.subscriptionId}`);
49
- return;
50
- }
51
- if (subscription.cancel_at && subscription.cancel_at <= now) {
52
- await subscription.update({ status: 'canceled', canceled_at: now });
53
- logger.warn(`Subscription canceled on schedule: ${job.subscriptionId}`);
54
- return;
55
- }
56
- }
57
-
58
- // Do we need to resume the subscription
59
- if (subscription.pause_collection?.resumes_at && subscription.pause_collection?.resumes_at <= now) {
60
- await subscription.update({ status: 'active', pause_collection: undefined });
61
- logger.warn(`Subscription resumed as scheduled: ${job.subscriptionId}`);
62
- }
63
-
64
- // can we create new invoice for this subscription?
65
- if (subscription.status === 'trialing' && subscription.trail_end && subscription.trail_end > now) {
66
- logger.warn(`Subscription trialing period not ended: ${job.subscriptionId}`);
67
- subscriptionQueue.push({
68
- id: subscription.id,
69
- job: { subscriptionId: subscription.id, action: 'cycle' },
70
- runAt: subscription.trail_end,
71
- });
72
- return;
73
- }
74
- if (subscription.status === 'active' && subscription.current_period_end > now) {
75
- logger.warn(`Subscription current period not ended: ${job.subscriptionId}`);
76
- subscriptionQueue.push({
77
- id: subscription.id,
78
- job: { subscriptionId: subscription.id, action: 'cycle' },
79
- runAt: subscription.current_period_end,
80
- });
81
- return;
82
- }
83
-
84
- if (subscription.isActive() === false) {
85
- logger.warn(`Subscription not active: ${job.subscriptionId}, so new invoice is skipped`);
86
- return;
87
- }
88
-
23
+ const handleSubscriptionInvoice = async (
24
+ subscription: Subscription,
25
+ filter: (x: any) => boolean,
26
+ status: string,
27
+ reason: 'cycle' | 'cancel',
28
+ start: number,
29
+ end: number,
30
+ offset: number
31
+ ) => {
89
32
  // Do we still have the customer
90
33
  const customer = await Customer.findByPk(subscription.customer_id);
91
34
  if (!customer) {
92
35
  logger.warn(`Customer ${subscription.customer_id} not found for subscription: ${subscription.id}`);
93
- return;
36
+ return null;
94
37
  }
95
38
 
96
39
  // Do we still have the currency
97
40
  const currency = await PaymentCurrency.findByPk(subscription.currency_id);
98
41
  if (!currency) {
99
42
  logger.warn(`Currency ${subscription.currency_id} not found for subscription: ${subscription.id}`);
100
- return;
43
+ return null;
101
44
  }
102
45
 
103
- // get setup for next subscription period
104
- const previousPeriodEnd =
105
- subscription.status === 'trialing' ? subscription.trail_end : subscription.current_period_end;
106
- const setup = getSubscriptionCycleSetup(subscription.pending_invoice_item_interval, previousPeriodEnd as number);
107
-
108
46
  // check if invoice already created
109
47
  const exist = await Invoice.findOne({
110
48
  where: {
111
49
  subscription_id: subscription.id,
112
- period_start: setup.period.start,
113
- period_end: setup.period.end,
50
+ period_start: start,
51
+ period_end: end,
52
+ billing_reason: `subscription_${reason}`,
114
53
  },
115
54
  });
116
55
  if (exist) {
117
- logger.warn(`Invoice already created for subscription ${subscription.id} for next billing cycle: ${exist.id}`);
118
- return;
119
- }
120
-
121
- // set invoice status if subscription paused
122
- let status = 'open';
123
- if (subscription.pause_collection) {
124
- if (subscription.pause_collection.behavior === 'mark_uncollectible') {
125
- status = 'uncollectible';
126
- }
127
- if (subscription.pause_collection.behavior === 'void') {
128
- status = 'void';
129
- }
130
- if (subscription.pause_collection.behavior === 'keep_as_draft') {
131
- status = 'draft';
132
- }
56
+ logger.warn(`Invoice already created for subscription ${subscription.id} for ${reason}: ${exist.id}`);
57
+ return null;
133
58
  }
134
59
 
135
60
  // expand subscription items
@@ -141,16 +66,14 @@ export const handleSubscription = async (job: SubscriptionJob) => {
141
66
 
142
67
  // get usage summaries for this billing cycle
143
68
  expandedItems = await Promise.all(
144
- expandedItems.map(async (x: any) => {
69
+ expandedItems.filter(filter).map(async (x: any) => {
145
70
  // For metered billing, we need to get usage summary for this billing cycle
146
71
  // @link https://stripe.com/docs/products-prices/pricing-models#usage-types
147
72
  if (x.price.recurring?.usage_type === 'metered') {
148
- const duration = setup.cycle / 1000;
149
73
  const rawQuantity = await UsageRecord.getSummary(
150
74
  x.id,
151
- // FIXME: this causes inconsistency when subscription is paused or billing_cycle_anchor reset
152
- setup.period.start - duration,
153
- setup.period.end - duration,
75
+ start - offset,
76
+ end - offset,
154
77
  x.price.recurring?.aggregate_usage
155
78
  );
156
79
  if (x.price.transform_quantity) {
@@ -169,8 +92,8 @@ export const handleSubscription = async (job: SubscriptionJob) => {
169
92
  transformQuantity: x.price.transform_quantity,
170
93
  rawQuantity,
171
94
  quantity: x.quantity,
172
- start: setup.period.start - duration,
173
- end: setup.period.end - duration,
95
+ start: start - offset,
96
+ end: end - offset,
174
97
  usage: x.price.recurring?.aggregate_usage,
175
98
  });
176
99
 
@@ -182,6 +105,9 @@ export const handleSubscription = async (job: SubscriptionJob) => {
182
105
  return x;
183
106
  })
184
107
  );
108
+ if (expandedItems.length === 0) {
109
+ return null;
110
+ }
185
111
 
186
112
  const amount = getSubscriptionCycleAmount(expandedItems, currency.id);
187
113
 
@@ -193,13 +119,13 @@ export const handleSubscription = async (job: SubscriptionJob) => {
193
119
  lineItems: expandedItems,
194
120
  props: {
195
121
  livemode: subscription.livemode,
196
- description: 'Subscription cycle',
122
+ description: `Subscription ${reason}`,
197
123
  statement_descriptor: getStatementDescriptor(expandedItems),
198
- period_start: setup.period.start,
199
- period_end: setup.period.end,
124
+ period_start: start,
125
+ period_end: end,
200
126
  auto_advance: true,
201
127
  status,
202
- billing_reason: 'subscription_cycle',
128
+ billing_reason: `subscription_${reason}`,
203
129
  currency_id: subscription.currency_id,
204
130
  total: amount.total,
205
131
  payment_settings: subscription.payment_settings,
@@ -208,17 +134,74 @@ export const handleSubscription = async (job: SubscriptionJob) => {
208
134
  } as Invoice,
209
135
  });
210
136
 
211
- // schedule invoice job
212
- invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: true } });
213
- logger.info(`Invoice job scheduled for new billing cycle: ${invoice.id}`);
137
+ return invoice;
138
+ };
139
+
140
+ const handleSubscriptionBeforeCancel = async (subscription: Subscription) => {
141
+ const invoice = await handleSubscriptionInvoice(
142
+ subscription,
143
+ (x) => x.price.recurring?.usage_type === 'metered', // include only metered items
144
+ 'open',
145
+ 'cancel',
146
+ subscription.current_period_start as number,
147
+ subscription.current_period_end as number,
148
+ 0
149
+ );
214
150
 
215
- // persist invoice id
216
- await subscription.update({
217
- latest_invoice_id: invoice.id,
218
- current_period_start: setup.period.start,
219
- current_period_end: setup.period.end,
220
- });
221
- logger.info(`Subscription updated for new billing cycle: ${subscription.id}`);
151
+ if (invoice) {
152
+ // schedule invoice job
153
+ invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: true } });
154
+ logger.info(`Invoice job scheduled before cancel: ${invoice.id}`);
155
+
156
+ // persist invoice id
157
+ await subscription.update({ latest_invoice_id: invoice.id });
158
+ logger.info(`Subscription updated before cancel: ${subscription.id}`);
159
+ }
160
+ };
161
+
162
+ const handleSubscriptionWhenActive = async (subscription: Subscription) => {
163
+ // get setup for next subscription period
164
+ const previousPeriodEnd =
165
+ subscription.status === 'trialing' ? subscription.trail_end : subscription.current_period_end;
166
+ const setup = getSubscriptionCycleSetup(subscription.pending_invoice_item_interval, previousPeriodEnd as number);
167
+
168
+ // set invoice status if subscription paused
169
+ let status = 'open';
170
+ if (subscription.pause_collection) {
171
+ if (subscription.pause_collection.behavior === 'mark_uncollectible') {
172
+ status = 'uncollectible';
173
+ }
174
+ if (subscription.pause_collection.behavior === 'void') {
175
+ status = 'void';
176
+ }
177
+ if (subscription.pause_collection.behavior === 'keep_as_draft') {
178
+ status = 'draft';
179
+ }
180
+ }
181
+
182
+ const invoice = await handleSubscriptionInvoice(
183
+ subscription,
184
+ () => true, // include all items
185
+ status,
186
+ 'cycle',
187
+ setup.period.start,
188
+ setup.period.end,
189
+ setup.cycle / 1000
190
+ );
191
+
192
+ if (invoice) {
193
+ // schedule invoice job
194
+ invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: true } });
195
+ logger.info(`Invoice job scheduled for new billing cycle: ${invoice.id}`);
196
+
197
+ // persist invoice id
198
+ await subscription.update({
199
+ latest_invoice_id: invoice.id,
200
+ current_period_start: setup.period.start,
201
+ current_period_end: setup.period.end,
202
+ });
203
+ logger.info(`Subscription updated for new billing cycle: ${subscription.id}`);
204
+ }
222
205
 
223
206
  // schedule next billing cycle if we are not in terminal state
224
207
  if (subscription.isActive()) {
@@ -231,6 +214,77 @@ export const handleSubscription = async (job: SubscriptionJob) => {
231
214
  }
232
215
  };
233
216
 
217
+ // generate invoice for subscription periodically
218
+ export const handleSubscription = async (job: SubscriptionJob) => {
219
+ logger.info('handle subscription', job);
220
+
221
+ const subscription = await Subscription.findByPk(job.subscriptionId);
222
+ if (!subscription) {
223
+ logger.warn(`Subscription not found: ${job.subscriptionId}`);
224
+ return;
225
+ }
226
+ if (EXPECTED_SUBSCRIPTION_STATUS.includes(subscription.status) === false) {
227
+ logger.warn(`Subscription status not expected: ${job.subscriptionId}`);
228
+ return;
229
+ }
230
+ const supportAutoCharge = await PaymentMethod.supportAutoCharge(subscription.default_payment_method_id);
231
+ if (supportAutoCharge === false) {
232
+ logger.warn(`Subscription does not support auto charge: ${job.subscriptionId}`);
233
+ return;
234
+ }
235
+
236
+ const now = dayjs().unix();
237
+
238
+ // Do we need to cancel the subscription
239
+ if (subscription.isImmutable() === false) {
240
+ if (subscription.cancel_at_period_end) {
241
+ await handleSubscriptionBeforeCancel(subscription);
242
+ await subscription.update({ status: 'canceled', canceled_at: now });
243
+ logger.warn(`Subscription canceled on period end: ${job.subscriptionId}`);
244
+ return;
245
+ }
246
+ if (subscription.cancel_at && subscription.cancel_at <= now) {
247
+ await handleSubscriptionBeforeCancel(subscription);
248
+ await subscription.update({ status: 'canceled', canceled_at: now });
249
+ logger.warn(`Subscription canceled on schedule: ${job.subscriptionId}`);
250
+ return;
251
+ }
252
+ }
253
+
254
+ // Do we need to resume the subscription
255
+ if (subscription.pause_collection?.resumes_at && subscription.pause_collection?.resumes_at <= now) {
256
+ await subscription.update({ status: 'active', pause_collection: undefined });
257
+ logger.warn(`Subscription resumed as scheduled: ${job.subscriptionId}`);
258
+ }
259
+
260
+ // can we create new invoice for this subscription?
261
+ if (subscription.status === 'trialing' && subscription.trail_end && subscription.trail_end > now) {
262
+ logger.warn(`Subscription trialing period not ended: ${job.subscriptionId}`);
263
+ subscriptionQueue.push({
264
+ id: subscription.id,
265
+ job: { subscriptionId: subscription.id, action: 'cycle' },
266
+ runAt: subscription.trail_end,
267
+ });
268
+ return;
269
+ }
270
+ if (subscription.status === 'active' && subscription.current_period_end > now) {
271
+ logger.warn(`Subscription current period not ended: ${job.subscriptionId}`);
272
+ subscriptionQueue.push({
273
+ id: subscription.id,
274
+ job: { subscriptionId: subscription.id, action: 'cycle' },
275
+ runAt: subscription.current_period_end,
276
+ });
277
+ return;
278
+ }
279
+
280
+ if (subscription.isActive() === false) {
281
+ logger.warn(`Subscription not active: ${job.subscriptionId}, so new invoice is skipped`);
282
+ return;
283
+ }
284
+
285
+ await handleSubscriptionWhenActive(subscription);
286
+ };
287
+
234
288
  export const subscriptionQueue = createQueue<SubscriptionJob>({
235
289
  name: 'subscription',
236
290
  onJob: handleSubscription,