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.
- package/api/src/libs/session.ts +4 -65
- package/api/src/libs/subscription.ts +69 -1
- package/api/src/queues/checkout-session.ts +1 -18
- package/api/src/queues/subscription.ts +116 -78
- package/api/src/queues/usage-record.ts +155 -0
- package/api/src/routes/checkout-sessions.ts +7 -8
- package/api/src/routes/connect/setup.ts +2 -9
- package/api/src/routes/connect/subscribe.ts +2 -7
- package/api/src/routes/connect/update.ts +2 -7
- package/api/src/routes/subscriptions.ts +12 -30
- package/api/src/routes/usage-records.ts +33 -20
- package/api/src/store/migrations/20240202-usage-billed.ts +22 -0
- package/api/src/store/models/checkout-session.ts +1 -0
- package/api/src/store/models/price.ts +10 -0
- package/api/src/store/models/usage-record.ts +41 -17
- package/api/tests/libs/session.spec.ts +0 -77
- package/api/tests/libs/subscription.spec.ts +83 -1
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/libs/util.ts +4 -1
|
@@ -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 {
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
method
|
|
92
|
-
|
|
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]:
|
|
98
|
-
[Op.lt]:
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
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.
|
|
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.
|
|
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": "
|
|
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 (
|
|
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
|
|