payment-kit 1.13.128 → 1.13.129

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.
@@ -8,7 +8,7 @@ import { wallet } from '../../libs/auth';
8
8
  import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/payment';
9
9
  import { getFastCheckoutAmount } from '../../libs/session';
10
10
  import { OCAP_PAYMENT_TX_TYPE, getTxMetadata } from '../../libs/util';
11
- import { subscriptionQueue } from '../../queues/subscription';
11
+ import { addSubscriptionJob } from '../../queues/subscription';
12
12
  import type { TLineItemExpanded } from '../../store/models';
13
13
  import { ensureSetupIntent, getAuthPrincipalClaim, getTokenRequirements } from './shared';
14
14
 
@@ -136,14 +136,7 @@ export default {
136
136
  });
137
137
 
138
138
  await checkoutSession.update({ status: 'complete', payment_status: 'paid' });
139
-
140
- // FIXME: for trialing subscriptions should we do this after 1st normal cycle
141
- subscriptionQueue.push({
142
- id: subscription.id,
143
- job: { subscriptionId: subscription.id, action: 'cycle' },
144
- // our next invoice should be generated at the end of current period, either trailing or normal
145
- runAt: subscription.trail_end || subscription.current_period_end,
146
- });
139
+ await addSubscriptionJob(subscription, 'cycle', false, subscription.trail_end);
147
140
 
148
141
  return { hash: txHash };
149
142
  }
@@ -10,7 +10,7 @@ import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/paymen
10
10
  import { getFastCheckoutAmount } from '../../libs/session';
11
11
  import { OCAP_PAYMENT_TX_TYPE, getTxMetadata } from '../../libs/util';
12
12
  import { invoiceQueue } from '../../queues/invoice';
13
- import { subscriptionQueue } from '../../queues/subscription';
13
+ import { addSubscriptionJob } from '../../queues/subscription';
14
14
  import type { TLineItemExpanded } from '../../store/models';
15
15
  import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim, getTokenRequirements } from './shared';
16
16
 
@@ -137,12 +137,7 @@ export default {
137
137
  if (invoice) {
138
138
  invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
139
139
  }
140
- subscriptionQueue.push({
141
- id: subscription.id,
142
- job: { subscriptionId: subscription.id, action: 'cycle' },
143
- // our next invoice should be generated at the end of current period, either trailing or normal
144
- runAt: subscription.trail_end || subscription.current_period_end,
145
- });
140
+ await addSubscriptionJob(subscription, 'cycle', false, subscription.trail_end);
146
141
 
147
142
  return { hash: txHash };
148
143
  }
@@ -9,7 +9,7 @@ import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/paymen
9
9
  import { getFastCheckoutAmount } from '../../libs/session';
10
10
  import { OCAP_PAYMENT_TX_TYPE, getTxMetadata } from '../../libs/util';
11
11
  import { invoiceQueue } from '../../queues/invoice';
12
- import { subscriptionQueue } from '../../queues/subscription';
12
+ import { addSubscriptionJob } from '../../queues/subscription';
13
13
  import type { TLineItemExpanded } from '../../store/models';
14
14
  import { ensureSubscription, getAuthPrincipalClaim, getTokenRequirements } from './shared';
15
15
 
@@ -118,12 +118,7 @@ export default {
118
118
  });
119
119
  }
120
120
  if (subscription) {
121
- subscriptionQueue.push({
122
- id: subscription.id,
123
- job: { subscriptionId: subscription.id, action: 'cycle' },
124
- // our next invoice should be generated at the end of current period, either trailing or normal
125
- runAt: subscription.trail_end || subscription.current_period_end,
126
- });
121
+ await addSubscriptionJob(subscription, 'cycle', false, subscription.trail_end);
127
122
  }
128
123
 
129
124
  return { hash: txHash };
@@ -12,15 +12,11 @@ import dayjs from '../libs/dayjs';
12
12
  import logger from '../libs/logger';
13
13
  import { isDelegationSufficientForPayment } from '../libs/payment';
14
14
  import { authenticate } from '../libs/security';
15
- import {
16
- expandLineItems,
17
- getPriceUintAmountByCurrency,
18
- getSubscriptionCreateSetup,
19
- isLineItemAligned,
20
- } from '../libs/session';
15
+ import { expandLineItems, getPriceUintAmountByCurrency, isLineItemAligned } from '../libs/session';
16
+ import { getSubscriptionCreateSetup } from '../libs/subscription';
21
17
  import { MAX_SUBSCRIPTION_ITEM_COUNT, formatMetadata } from '../libs/util';
22
18
  import { invoiceQueue } from '../queues/invoice';
23
- import { subscriptionQueue } from '../queues/subscription';
19
+ import { addSubscriptionJob, subscriptionQueue } from '../queues/subscription';
24
20
  import type { TLineItemExpanded } from '../store/models';
25
21
  import { Customer } from '../store/models/customer';
26
22
  import { Invoice } from '../store/models/invoice';
@@ -230,22 +226,21 @@ router.put('/:id/cancel', authPortal, async (req, res) => {
230
226
  updates.cancel_at = doc.current_period_end;
231
227
  updates.cancelation_details = { reason: 'cancellation_requested', feedback, comment };
232
228
  updates.canceled_at = dayjs().unix();
229
+ await addSubscriptionJob(doc, 'cancel', true, updates.cancel_at);
233
230
  } else if (at === 'now') {
234
231
  updates.status = 'canceled';
235
232
  updates.cancel_at = dayjs().unix();
236
233
  updates.canceled_at = dayjs().unix();
234
+ await addSubscriptionJob(doc, 'cancel', true, updates.cancel_at);
237
235
  } else if (at === 'current_period_end') {
238
236
  updates.cancel_at_period_end = true;
239
237
  updates.cancel_at = doc.current_period_end;
240
238
  updates.canceled_at = dayjs().unix();
239
+ await addSubscriptionJob(doc, 'cancel', true, updates.cancel_at);
241
240
  } else {
242
241
  updates.cancel_at = dayjs(time).unix();
243
242
  updates.canceled_at = dayjs().unix();
244
- subscriptionQueue.push({
245
- id: `cancel-${doc.id}`,
246
- job: { subscriptionId: doc.id, action: 'cancel' },
247
- runAt: updates.cancel_at,
248
- });
243
+ await addSubscriptionJob(doc, 'cancel', updates.cancel_at < doc.current_period_end, updates.cancel_at);
249
244
  }
250
245
 
251
246
  if (doc.payment_details?.stripe?.subscription_id) {
@@ -289,14 +284,10 @@ router.put('/:id/recover', authPortal, async (req, res) => {
289
284
 
290
285
  // reschedule jobs
291
286
  subscriptionQueue
292
- .cancel(`cancel-${doc.id}`)
287
+ .delete(`cancel-${doc.id}`)
293
288
  .then(() => logger.info('subscription cancel job is canceled'))
294
289
  .catch((err) => logger.error('subscription cancel job failed to cancel', { error: err }));
295
- subscriptionQueue.push({
296
- id: doc.id,
297
- job: { subscriptionId: doc.id, action: 'cycle' },
298
- runAt: doc.current_period_end,
299
- });
290
+ await addSubscriptionJob(doc, 'cycle');
300
291
 
301
292
  return res.json(doc);
302
293
  });
@@ -334,11 +325,7 @@ router.put('/:id/pause', auth, async (req, res) => {
334
325
  });
335
326
 
336
327
  if (timestamp) {
337
- subscriptionQueue.push({
338
- id: `resume-${doc.id}`,
339
- job: { subscriptionId: doc.id, action: 'resume' },
340
- runAt: timestamp,
341
- });
328
+ await addSubscriptionJob(doc, 'resume', false, timestamp);
342
329
  }
343
330
 
344
331
  return res.json(doc);
@@ -358,7 +345,7 @@ router.put('/:id/resume', auth, async (req, res) => {
358
345
  await doc.update({ status: 'active', pause_collection: undefined });
359
346
 
360
347
  subscriptionQueue
361
- .cancel(`resume-${doc.id}`)
348
+ .delete(`resume-${doc.id}`)
362
349
  .then(() => logger.info('subscription resume job is canceled'))
363
350
  .catch((err) => logger.error('subscription resume job failed to cancel', { error: err }));
364
351
 
@@ -852,12 +839,7 @@ router.put('/:id', authPortal, async (req, res) => {
852
839
 
853
840
  if (invoice.status === 'paid') {
854
841
  await subscriptionQueue.delete(subscription.id);
855
- subscriptionQueue.push({
856
- id: subscription.id,
857
- job: { subscriptionId: subscription.id, action: 'cycle' },
858
- // our next invoice should be generated at the end of current period, either trailing or normal
859
- runAt: subscription.trail_end || subscription.current_period_end,
860
- });
842
+ await addSubscriptionJob(subscription, 'cycle', false, subscription.trail_end);
861
843
  } else {
862
844
  await subscription.update({ status: 'past_due' });
863
845
  logger.info('subscription past_due on invoice paid', {
@@ -7,6 +7,7 @@ import { Op } from 'sequelize';
7
7
  import { forwardUsageRecordToStripe } from '../integrations/stripe/resource';
8
8
  import dayjs from '../libs/dayjs';
9
9
  import { authenticate } from '../libs/security';
10
+ import { usageRecordQueue } from '../queues/usage-record';
10
11
  import { Invoice, Price, Subscription, SubscriptionItem, UsageRecord } from '../store/models';
11
12
 
12
13
  const router = Router();
@@ -20,21 +21,35 @@ router.post('/', auth, async (req, res) => {
20
21
  return res.status(400).json({ error: `SubscriptionItem not found: ${raw.subscription_item_id}` });
21
22
  }
22
23
 
24
+ const subscription = await Subscription.findByPk(item.subscription_id);
25
+ if (!subscription) {
26
+ return res.status(400).json({ error: `Subscription not found: ${item.subscription_id}` });
27
+ }
28
+ if (raw.timestamp) {
29
+ if (raw.timestamp < subscription.current_period_start || raw.timestamp > subscription.current_period_end) {
30
+ return res.status(400).json({ error: '`timestamp` must be within the current period' });
31
+ }
32
+ }
33
+
23
34
  // @ts-ignore
24
35
  if (!raw.timestamp || raw.timestamp === 'now') {
25
36
  raw.timestamp = dayjs().unix();
26
37
  }
27
38
 
28
- let doc = await UsageRecord.findOne({ where: { timestamp: raw.timestamp } });
39
+ let doc = await UsageRecord.findOne({
40
+ where: { timestamp: raw.timestamp, subscription_item_id: raw.subscription_item_id },
41
+ });
29
42
  if (doc) {
43
+ if (doc.billed) {
44
+ return res.status(400).json({ error: 'UsageRecord is immutable because already billed' });
45
+ }
30
46
  if (req.body.action === 'increment') {
31
47
  await doc.increment('quantity', { by: raw.quantity });
32
48
  } else {
33
- const subscription = await Subscription.findByPk(item.subscription_id);
34
- if (subscription?.billing_thresholds) {
49
+ if (subscription.billing_thresholds?.amount_gte) {
35
50
  return res
36
51
  .status(400)
37
- .json({ error: 'UsageRecord action must be increment for subscriptions with billing_thresholds' });
52
+ .json({ error: 'UsageRecord action must be `increment` for subscriptions with billing_thresholds' });
38
53
  }
39
54
  await doc.update({ quantity: raw.quantity });
40
55
  }
@@ -43,6 +58,13 @@ router.post('/', auth, async (req, res) => {
43
58
  doc = await UsageRecord.create(raw as UsageRecord);
44
59
  }
45
60
 
61
+ if (subscription.billing_thresholds?.amount_gte) {
62
+ usageRecordQueue.push({
63
+ id: `usage-${subscription.id}`,
64
+ job: { subscriptionId: subscription.id, subscriptionItemId: item.id },
65
+ });
66
+ }
67
+
46
68
  await forwardUsageRecordToStripe(item, {
47
69
  quantity: Number(raw.quantity),
48
70
  timestamp: raw.timestamp,
@@ -75,16 +97,6 @@ router.get('/summary', auth, async (req, res) => {
75
97
  return res.status(400).json({ error: `SubscriptionItem not found: ${query.subscription_item_id}` });
76
98
  }
77
99
 
78
- // const subscription = await Subscription.findByPk(item.subscription_id);
79
- // const result = await UsageRecord.getSummary(
80
- // item.id,
81
- // subscription?.current_period_start as number,
82
- // subscription?.current_period_end as number,
83
- // // @ts-ignore
84
- // item.price.recurring.aggregate_usage
85
- // );
86
- // return res.json({ result });
87
-
88
100
  const { rows, count } = await Invoice.findAndCountAll({
89
101
  where: { subscription_id: item.subscription_id },
90
102
  attributes: ['id', 'period_end', 'period_start'],
@@ -99,13 +111,14 @@ router.get('/summary', auth, async (req, res) => {
99
111
  livemode: invoice.livemode,
100
112
  invoice_id: invoice.id,
101
113
  subscription_item_id: item.id,
102
- total_usage: await UsageRecord.getSummary(
103
- item.id,
104
- invoice.period_start,
105
- invoice.period_end,
114
+ total_usage: await UsageRecord.getSummary({
115
+ id: item.id,
116
+ start: invoice.period_start,
117
+ end: invoice.period_end,
106
118
  // @ts-ignore
107
- item.price.recurring.aggregate_usage
108
- ),
119
+ method: item.price.recurring.aggregate_usage,
120
+ dryRun: true,
121
+ }),
109
122
  period: {
110
123
  start: invoice.period_start,
111
124
  end: invoice.period_end,
@@ -0,0 +1,22 @@
1
+ /* eslint-disable no-await-in-loop */
2
+ import { DataTypes } from 'sequelize';
3
+
4
+ import { Migration, safeApplyColumnChanges } from '../migrate';
5
+
6
+ export const up: Migration = async ({ context }) => {
7
+ await safeApplyColumnChanges(context, {
8
+ usage_records: [
9
+ {
10
+ name: 'billed',
11
+ field: {
12
+ type: DataTypes.BOOLEAN,
13
+ defaultValue: false,
14
+ },
15
+ },
16
+ ],
17
+ });
18
+ };
19
+
20
+ export const down: Migration = async ({ context }) => {
21
+ await context.removeColumn('usage_records', 'billed');
22
+ };
@@ -159,6 +159,7 @@ export class CheckoutSession extends Model<InferAttributes<CheckoutSession>, Inf
159
159
  description: string;
160
160
  trial_period_days: number;
161
161
  billing_cycle_anchor?: number;
162
+ billing_threshold_amount?: number;
162
163
  metadata?: Record<string, any>;
163
164
  proration_behavior?: LiteralUnion<'create_prorations' | 'none', string>;
164
165
  trial_end?: number;
@@ -230,6 +230,16 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
230
230
  return 'standard';
231
231
  }
232
232
 
233
+ public transformQuantity(quantity: number) {
234
+ if (this.transform_quantity) {
235
+ if (this.transform_quantity.round === 'up') {
236
+ return Math.ceil(quantity / this.transform_quantity.divide_by);
237
+ }
238
+ return Math.floor(quantity / this.transform_quantity.divide_by);
239
+ }
240
+ return quantity;
241
+ }
242
+
233
243
  public static formatBeforeSave(price: Partial<TPrice & { model: string }>) {
234
244
  if (price.type) {
235
245
  if (price.type === 'recurring') {
@@ -10,6 +10,7 @@ export class UsageRecord extends Model<InferAttributes<UsageRecord>, InferCreati
10
10
  declare id: CreationOptional<string>;
11
11
 
12
12
  declare livemode: boolean;
13
+ declare billed: boolean;
13
14
 
14
15
  // The timestamp when this usage occurred.
15
16
  declare timestamp: number;
@@ -68,13 +69,22 @@ export class UsageRecord extends Model<InferAttributes<UsageRecord>, InferCreati
68
69
  };
69
70
 
70
71
  public static initialize(sequelize: any) {
71
- this.init(UsageRecord.GENESIS_ATTRIBUTES, {
72
- sequelize,
73
- modelName: 'UsageRecord',
74
- tableName: 'usage_records',
75
- createdAt: 'created_at',
76
- updatedAt: 'updated_at',
77
- });
72
+ this.init(
73
+ {
74
+ ...UsageRecord.GENESIS_ATTRIBUTES,
75
+ billed: {
76
+ type: DataTypes.BOOLEAN,
77
+ defaultValue: false,
78
+ },
79
+ },
80
+ {
81
+ sequelize,
82
+ modelName: 'UsageRecord',
83
+ tableName: 'usage_records',
84
+ createdAt: 'created_at',
85
+ updatedAt: 'updated_at',
86
+ }
87
+ );
78
88
  }
79
89
 
80
90
  public static associate(models: any) {
@@ -84,44 +94,58 @@ export class UsageRecord extends Model<InferAttributes<UsageRecord>, InferCreati
84
94
  });
85
95
  }
86
96
 
87
- public static async getSummary(
88
- subscription_item_id: string,
89
- period_start: number,
90
- period_end: number,
91
- method: LiteralUnion<'sum' | 'last_during_period' | 'max' | 'last_ever', string>
92
- ): Promise<number> {
97
+ public static async getSummary({
98
+ id,
99
+ start,
100
+ end,
101
+ method,
102
+ dryRun,
103
+ }: {
104
+ id: string;
105
+ start: number;
106
+ end: number;
107
+ method: LiteralUnion<'sum' | 'last_during_period' | 'max' | 'last_ever', string>;
108
+ dryRun: boolean;
109
+ }): Promise<number> {
93
110
  const query = {
94
111
  where: {
95
- subscription_item_id,
112
+ subscription_item_id: id,
113
+ billed: false,
96
114
  timestamp: {
97
- [Op.gte]: period_start,
98
- [Op.lt]: period_end,
115
+ [Op.gte]: start,
116
+ [Op.lt]: end,
99
117
  },
100
118
  },
101
119
  order: [['timestamp', 'DESC']],
102
120
  };
103
121
 
122
+ const update = () => (dryRun ? Promise.resolve() : UsageRecord.update({ billed: true }, query));
123
+
104
124
  if (method === 'sum') {
105
125
  const sum = await this.sum('quantity', query);
126
+ await update();
106
127
  return sum || 0;
107
128
  }
108
129
 
109
130
  if (method === 'max') {
110
131
  const max = await this.max('quantity', query);
132
+ await update();
111
133
  return (max as number) || 0;
112
134
  }
113
135
 
114
136
  if (method === 'last_during_period') {
115
137
  // @ts-ignore
116
138
  const item = await this.findOne(query);
139
+ await update();
117
140
  return item ? item.quantity : 0;
118
141
  }
119
142
 
120
143
  if (method === 'last_ever') {
121
144
  const item = await this.findOne({
122
- where: { subscription_item_id },
145
+ where: { subscription_item_id: id },
123
146
  order: [['timestamp', 'DESC']],
124
147
  });
148
+ await update();
125
149
  return item ? item.quantity : 0;
126
150
  }
127
151
 
@@ -1,10 +1,8 @@
1
- import dayjs from '../../src/libs/dayjs';
2
1
  import {
3
2
  getCheckoutMode,
4
3
  getPriceCurrencyOptions,
5
4
  getPriceUintAmountByCurrency,
6
5
  getRecurringPeriod,
7
- getSubscriptionCreateSetup,
8
6
  } from '../../src/libs/session';
9
7
  import type { TLineItemExpanded } from '../../src/store/models';
10
8
 
@@ -137,78 +135,3 @@ describe('getRecurringPeriod', () => {
137
135
  expect(result).toBe(0);
138
136
  });
139
137
  });
140
-
141
- describe('getSubscriptionCreateSetup', () => {
142
- const currencies = [
143
- {
144
- currency_id: 'usd',
145
- unit_amount: '1',
146
- },
147
- ];
148
-
149
- it('should calculate setup for recurring licensed price type', () => {
150
- const items = [
151
- {
152
- price: { type: 'one_time', currency_options: currencies },
153
- quantity: 1,
154
- },
155
- {
156
- price: {
157
- type: 'recurring',
158
- currency_options: currencies,
159
- recurring: { interval: 'day', interval_count: '1', usage_type: 'licensed' },
160
- },
161
- quantity: 2,
162
- },
163
- ];
164
- const result = getSubscriptionCreateSetup(items as any, 'usd');
165
- expect(result.amount.setup).toBe('3');
166
- });
167
-
168
- it('should not calculate setup for recurring metered price type', () => {
169
- const items = [
170
- {
171
- price: { type: 'one_time', currency_options: currencies },
172
- quantity: 1,
173
- },
174
- {
175
- price: {
176
- type: 'recurring',
177
- currency_options: currencies,
178
- recurring: { interval: 'day', interval_count: '1', usage_type: 'metered' },
179
- },
180
- quantity: 2,
181
- },
182
- ];
183
- const result = getSubscriptionCreateSetup(items as any, 'usd');
184
- expect(result.amount.setup).toBe('1');
185
- });
186
-
187
- it('should calculate cycle duration for recurring price type', () => {
188
- const items = [
189
- {
190
- price: { type: 'recurring', currency_options: currencies, recurring: { interval: 'day', interval_count: '1' } },
191
- quantity: 2,
192
- },
193
- ];
194
- const result = getSubscriptionCreateSetup(items as any, 'usd');
195
- expect(result.cycle.duration).toBe(24 * 60 * 60 * 1000);
196
- });
197
-
198
- it('should calculate trial period when trialInDays is provided', () => {
199
- const items = [
200
- {
201
- price: { type: 'recurring', currency_options: currencies, recurring: { interval: 'day', interval_count: '1' } },
202
- quantity: 2,
203
- },
204
- ];
205
- const result = getSubscriptionCreateSetup(items as any, 'usd', 7);
206
- const now = dayjs().unix();
207
- expect(result.trail.start).toBe(now);
208
- expect(result.trail.end).toBe(
209
- dayjs()
210
- .add(7 * 24 * 60 * 60 * 1000, 'millisecond')
211
- .unix()
212
- );
213
- });
214
- });
@@ -1,4 +1,11 @@
1
- import { getDaysUntilDue, getDueUnit, getMaxRetryCount, getMinRetryMail } from '../../src/libs/subscription';
1
+ import dayjs from '../../src/libs/dayjs';
2
+ import {
3
+ getDaysUntilDue,
4
+ getDueUnit,
5
+ getMaxRetryCount,
6
+ getMinRetryMail,
7
+ getSubscriptionCreateSetup,
8
+ } from '../../src/libs/subscription';
2
9
 
3
10
  describe('getDueUnit', () => {
4
11
  it('should return 60 for recurring interval of "hour"', () => {
@@ -87,3 +94,78 @@ describe('getMinRetryMail', () => {
87
94
  expect(result).toBe(15);
88
95
  });
89
96
  });
97
+
98
+ describe('getSubscriptionCreateSetup', () => {
99
+ const currencies = [
100
+ {
101
+ currency_id: 'usd',
102
+ unit_amount: '1',
103
+ },
104
+ ];
105
+
106
+ it('should calculate setup for recurring licensed price type', () => {
107
+ const items = [
108
+ {
109
+ price: { type: 'one_time', currency_options: currencies },
110
+ quantity: 1,
111
+ },
112
+ {
113
+ price: {
114
+ type: 'recurring',
115
+ currency_options: currencies,
116
+ recurring: { interval: 'day', interval_count: '1', usage_type: 'licensed' },
117
+ },
118
+ quantity: 2,
119
+ },
120
+ ];
121
+ const result = getSubscriptionCreateSetup(items as any, 'usd');
122
+ expect(result.amount.setup).toBe('3');
123
+ });
124
+
125
+ it('should not calculate setup for recurring metered price type', () => {
126
+ const items = [
127
+ {
128
+ price: { type: 'one_time', currency_options: currencies },
129
+ quantity: 1,
130
+ },
131
+ {
132
+ price: {
133
+ type: 'recurring',
134
+ currency_options: currencies,
135
+ recurring: { interval: 'day', interval_count: '1', usage_type: 'metered' },
136
+ },
137
+ quantity: 2,
138
+ },
139
+ ];
140
+ const result = getSubscriptionCreateSetup(items as any, 'usd');
141
+ expect(result.amount.setup).toBe('1');
142
+ });
143
+
144
+ it('should calculate cycle duration for recurring price type', () => {
145
+ const items = [
146
+ {
147
+ price: { type: 'recurring', currency_options: currencies, recurring: { interval: 'day', interval_count: '1' } },
148
+ quantity: 2,
149
+ },
150
+ ];
151
+ const result = getSubscriptionCreateSetup(items as any, 'usd');
152
+ expect(result.cycle.duration).toBe(24 * 60 * 60 * 1000);
153
+ });
154
+
155
+ it('should calculate trial period when trialInDays is provided', () => {
156
+ const items = [
157
+ {
158
+ price: { type: 'recurring', currency_options: currencies, recurring: { interval: 'day', interval_count: '1' } },
159
+ quantity: 2,
160
+ },
161
+ ];
162
+ const result = getSubscriptionCreateSetup(items as any, 'usd', 7);
163
+ const now = dayjs().unix();
164
+ expect(result.trail.start).toBe(now);
165
+ expect(result.trail.end).toBe(
166
+ dayjs()
167
+ .add(7 * 24 * 60 * 60 * 1000, 'millisecond')
168
+ .unix()
169
+ );
170
+ });
171
+ });
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.13.128
17
+ version: 1.13.129
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.13.128",
3
+ "version": "1.13.129",
4
4
  "scripts": {
5
5
  "dev": "cross-env COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev",
6
6
  "eject": "vite eject",
@@ -50,7 +50,7 @@
50
50
  "@arcblock/jwt": "^1.18.108",
51
51
  "@arcblock/ux": "^2.9.23",
52
52
  "@blocklet/logger": "1.16.23-beta-aeb9f5bd",
53
- "@blocklet/payment-react": "1.13.128",
53
+ "@blocklet/payment-react": "1.13.129",
54
54
  "@blocklet/sdk": "1.16.23-beta-aeb9f5bd",
55
55
  "@blocklet/ui-react": "^2.9.23",
56
56
  "@blocklet/uploader": "^0.0.69",
@@ -110,7 +110,7 @@
110
110
  "devDependencies": {
111
111
  "@abtnode/types": "1.16.23-beta-aeb9f5bd",
112
112
  "@arcblock/eslint-config-ts": "^0.2.4",
113
- "@blocklet/payment-types": "1.13.128",
113
+ "@blocklet/payment-types": "1.13.129",
114
114
  "@types/cookie-parser": "^1.4.6",
115
115
  "@types/cors": "^2.8.17",
116
116
  "@types/dotenv-flow": "^3.3.3",
@@ -149,5 +149,5 @@
149
149
  "parser": "typescript"
150
150
  }
151
151
  },
152
- "gitHead": "b518cfcb827855501a03f0675cb9025e75b8538e"
152
+ "gitHead": "4ad81bea01de3268d9cf0bda552a0a0a67abd85d"
153
153
  }
package/src/libs/util.ts CHANGED
@@ -185,7 +185,10 @@ export function isPriceRecurringAligned(list: LineItem[], products: TProductExpa
185
185
  }
186
186
 
187
187
  // If the interval and interval_count are different, the recurring is not aligned
188
- if (recurring?.interval !== x.recurring?.interval || recurring?.interval_count !== x.recurring?.interval_count) {
188
+ if (
189
+ String(recurring?.interval) !== String(x.recurring?.interval) ||
190
+ Number(recurring?.interval_count) !== Number(x.recurring?.interval_count)
191
+ ) {
189
192
  return false;
190
193
  }
191
194