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