payment-kit 1.13.106 → 1.13.107
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/hooks/pre-flight.js +12 -0
- package/api/src/hooks/pre-flight.ts +18 -0
- package/api/src/hooks/pre-start.ts +7 -8
- package/api/src/integrations/blocklet/passport.ts +9 -0
- package/api/src/libs/hooks.ts +5 -8
- package/api/src/libs/logger.ts +1 -8
- package/api/src/libs/resource.ts +265 -0
- package/api/src/libs/util.ts +2 -0
- package/api/src/queues/checkout-session.ts +141 -1
- package/api/src/routes/checkout-sessions.ts +12 -2
- package/api/src/routes/passports.ts +8 -14
- package/api/src/routes/payment-links.ts +27 -12
- package/api/src/routes/prices.ts +30 -18
- package/api/src/routes/products.ts +59 -29
- package/api/src/store/models/checkout-session.ts +5 -3
- package/api/src/store/models/customer.ts +2 -2
- package/api/src/store/models/invoice-item.ts +10 -2
- package/api/src/store/models/invoice.ts +2 -2
- package/api/src/store/models/payment-intent.ts +2 -2
- package/api/src/store/models/payment-link.ts +2 -2
- package/api/src/store/models/price.ts +44 -2
- package/api/src/store/models/pricing-table.ts +2 -2
- package/api/src/store/models/product.ts +2 -2
- package/api/src/store/models/subscription-item.ts +10 -2
- package/api/src/store/models/subscription.ts +2 -2
- package/api/src/store/models/types.ts +1 -0
- package/blocklet.yml +7 -1
- package/package.json +7 -7
- package/src/components/checkout/form/index.tsx +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* eslint-disable global-require */
|
|
2
|
+
|
|
3
|
+
const isDevelopment = process.env.BLOCKLET_MODE === 'development';
|
|
4
|
+
|
|
5
|
+
if (isDevelopment) {
|
|
6
|
+
// rename `require` to skip deps resolve when bundling
|
|
7
|
+
const r = require;
|
|
8
|
+
r('ts-node').register();
|
|
9
|
+
r('../src/hooks/pre-flight');
|
|
10
|
+
} else {
|
|
11
|
+
require('../dist/hooks/pre-flight');
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import '@blocklet/sdk/lib/error-handler';
|
|
2
|
+
|
|
3
|
+
import dotenv from 'dotenv-flow';
|
|
4
|
+
|
|
5
|
+
import { ensureSqliteBinaryFile } from '../libs/hooks';
|
|
6
|
+
|
|
7
|
+
dotenv.config({ silent: true });
|
|
8
|
+
|
|
9
|
+
(async () => {
|
|
10
|
+
try {
|
|
11
|
+
await ensureSqliteBinaryFile();
|
|
12
|
+
await import('../store/migrate').then((m) => m.default());
|
|
13
|
+
process.exit(0);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
console.error('pre-flight error', err.message);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
})();
|
|
@@ -2,20 +2,19 @@ import '@blocklet/sdk/lib/error-handler';
|
|
|
2
2
|
|
|
3
3
|
import dotenv from 'dotenv-flow';
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
5
|
+
import { initPaywallResources } from '../libs/resource';
|
|
6
|
+
import { initialize } from '../store/models';
|
|
7
|
+
import { sequelize } from '../store/sequelize';
|
|
7
8
|
|
|
8
|
-
dotenv.config();
|
|
9
|
-
|
|
10
|
-
const { name } = require('../../../package.json');
|
|
9
|
+
dotenv.config({ silent: true });
|
|
11
10
|
|
|
12
11
|
(async () => {
|
|
13
12
|
try {
|
|
14
|
-
|
|
15
|
-
await
|
|
13
|
+
initialize(sequelize);
|
|
14
|
+
await initPaywallResources();
|
|
16
15
|
process.exit(0);
|
|
17
16
|
} catch (err) {
|
|
18
|
-
|
|
17
|
+
console.error('pre-start error', err.message);
|
|
19
18
|
process.exit(1);
|
|
20
19
|
}
|
|
21
20
|
})();
|
|
@@ -137,3 +137,12 @@ export async function ensurePassportRevoked(subscription: Subscription) {
|
|
|
137
137
|
merge(info, { did: checkoutSession.customer_did, passports })
|
|
138
138
|
);
|
|
139
139
|
}
|
|
140
|
+
|
|
141
|
+
export async function updatePassportExtra(name: string, updates: any) {
|
|
142
|
+
const { role } = await blocklet.getRole(name);
|
|
143
|
+
if (!role) {
|
|
144
|
+
throw new Error(`passport ${name} not found`);
|
|
145
|
+
}
|
|
146
|
+
const result = await blocklet.updateRole(name, { extra: JSON.stringify(merge(role.extra, updates)) });
|
|
147
|
+
return result.role;
|
|
148
|
+
}
|
package/api/src/libs/hooks.ts
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
1
2
|
import { spawnSync } from 'child_process';
|
|
2
3
|
import { chmodSync, existsSync, mkdirSync, symlinkSync } from 'fs';
|
|
3
4
|
import { dirname, join } from 'path';
|
|
4
5
|
|
|
5
|
-
import logger from './logger';
|
|
6
|
-
|
|
7
|
-
const { name } = require('../../../package.json');
|
|
8
|
-
|
|
9
6
|
// eslint-disable-next-line import/prefer-default-export
|
|
10
7
|
export async function ensureSqliteBinaryFile() {
|
|
11
|
-
|
|
8
|
+
console.info('ensure sqlite3 installed');
|
|
12
9
|
|
|
13
10
|
try {
|
|
14
11
|
await import('sqlite3');
|
|
15
|
-
|
|
12
|
+
console.info('sqlite3 already installed');
|
|
16
13
|
return;
|
|
17
14
|
} catch {
|
|
18
15
|
/* empty */
|
|
19
16
|
}
|
|
20
|
-
|
|
17
|
+
console.info('try install sqlite3');
|
|
21
18
|
|
|
22
19
|
const appDir = process.env.BLOCKLET_APP_DIR!;
|
|
23
20
|
|
|
@@ -31,7 +28,7 @@ export async function ensureSqliteBinaryFile() {
|
|
|
31
28
|
chmodSync(binPath, '755');
|
|
32
29
|
}
|
|
33
30
|
} catch (error) {
|
|
34
|
-
|
|
31
|
+
console.warn(error.message);
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
spawnSync('npm', ['run', 'install'], {
|
package/api/src/libs/logger.ts
CHANGED
|
@@ -8,19 +8,12 @@ interface Logger {
|
|
|
8
8
|
warn: (...args: any[]) => void;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const consoleLogger: Logger = {
|
|
12
|
-
debug: console.log,
|
|
13
|
-
info: console.log,
|
|
14
|
-
error: console.log,
|
|
15
|
-
warn: console.warn,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
11
|
const init = (label: string): Logger => {
|
|
19
12
|
const instance = createLogger(label || '');
|
|
20
13
|
return instance;
|
|
21
14
|
};
|
|
22
15
|
|
|
23
|
-
const logger =
|
|
16
|
+
const logger = init('app');
|
|
24
17
|
|
|
25
18
|
export default logger;
|
|
26
19
|
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/* eslint-disable no-continue */
|
|
2
|
+
/* eslint-disable no-await-in-loop */
|
|
3
|
+
/* eslint-disable no-console */
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
import { getPackResources, getResources } from '@blocklet/sdk/lib/component';
|
|
8
|
+
import { env } from '@blocklet/sdk/lib/config';
|
|
9
|
+
import { fromTokenToUnit } from '@ocap/util';
|
|
10
|
+
|
|
11
|
+
import { updatePassportExtra } from '../integrations/blocklet/passport';
|
|
12
|
+
import { replace } from '../locales';
|
|
13
|
+
import { createPaymentLink } from '../routes/payment-links';
|
|
14
|
+
import { createPrice } from '../routes/prices';
|
|
15
|
+
import { createProductAndPrices } from '../routes/products';
|
|
16
|
+
import { PaymentCurrency, Price, Product, nextPriceId } from '../store/models';
|
|
17
|
+
|
|
18
|
+
export async function getPackResource(type: string) {
|
|
19
|
+
const resources = await getPackResources({
|
|
20
|
+
types: [
|
|
21
|
+
{
|
|
22
|
+
did: env.componentDid,
|
|
23
|
+
type,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (resources) {
|
|
29
|
+
return resources[0] || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getResourcesByType(type: string) {
|
|
36
|
+
const pack = await getPackResource(type);
|
|
37
|
+
const resources = await getResources({ types: [{ did: env.componentDid, type }] });
|
|
38
|
+
return pack ? [pack, ...resources] : resources;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function initPaywallResources() {
|
|
42
|
+
const resources = await getResourcesByType('paywall');
|
|
43
|
+
if (!resources.length) {
|
|
44
|
+
console.info('No paywall resource found');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// TODO: use schema validation
|
|
49
|
+
for (const resource of resources) {
|
|
50
|
+
const configPath = resource.path;
|
|
51
|
+
try {
|
|
52
|
+
console.info('try import paywall resource', resource);
|
|
53
|
+
|
|
54
|
+
const config: any = JSON.parse(
|
|
55
|
+
replace(fs.readFileSync(path.join(configPath!, 'config.json'), 'utf8'), {
|
|
56
|
+
...env,
|
|
57
|
+
// @ts-ignore
|
|
58
|
+
monthPrice: resource.env?.MONTH_PRICE || '5',
|
|
59
|
+
// @ts-ignore
|
|
60
|
+
yearPrice: resource.env?.YEAR_PRICE || '30',
|
|
61
|
+
})
|
|
62
|
+
);
|
|
63
|
+
console.info('try import paywall config', config);
|
|
64
|
+
|
|
65
|
+
if (!Array.isArray(config.passports) || !config.passports.length) {
|
|
66
|
+
console.warn(`invalid paywall resource from ${configPath}: passport empty`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!config.product) {
|
|
71
|
+
console.warn(`invalid paywall resource from ${configPath}: product empty`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!config.product.price) {
|
|
75
|
+
console.warn(`invalid paywall resource from ${configPath}: product price empty`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const metadata = { source: resource.did };
|
|
80
|
+
const currency = await PaymentCurrency.findOne({
|
|
81
|
+
where: { is_base_currency: true, livemode: config.product.livemode },
|
|
82
|
+
});
|
|
83
|
+
if (!currency) {
|
|
84
|
+
console.warn(`invalid paywall resource from ${configPath}: base currency not found`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const exist = await Product.findOne({
|
|
89
|
+
where: { 'metadata.source': resource.did },
|
|
90
|
+
include: [{ model: Price, as: 'prices', order: [['created_at', 'DESC']] }],
|
|
91
|
+
});
|
|
92
|
+
if (exist) {
|
|
93
|
+
console.warn(`paywall resource already imported from path: ${configPath}`);
|
|
94
|
+
|
|
95
|
+
// @ts-ignore
|
|
96
|
+
const monthPrice = exist.prices.find(
|
|
97
|
+
// @ts-ignore
|
|
98
|
+
(price) =>
|
|
99
|
+
price.nickname === 'monthly-member-price' &&
|
|
100
|
+
price.unit_amount === fromTokenToUnit(config.product.price.month, currency.decimal).toString()
|
|
101
|
+
);
|
|
102
|
+
// @ts-ignore
|
|
103
|
+
const yearPrice = exist.prices.find(
|
|
104
|
+
// @ts-ignore
|
|
105
|
+
(price) =>
|
|
106
|
+
price.nickname === 'yearly-member-price' &&
|
|
107
|
+
price.unit_amount === fromTokenToUnit(config.product.price.year, currency.decimal).toString()
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
let newMonthPriceId = '';
|
|
111
|
+
if (!monthPrice) {
|
|
112
|
+
newMonthPriceId = nextPriceId();
|
|
113
|
+
await createPrice({
|
|
114
|
+
id: newMonthPriceId,
|
|
115
|
+
product_id: exist.id,
|
|
116
|
+
type: 'recurring',
|
|
117
|
+
model: 'standard',
|
|
118
|
+
nickname: 'monthly-member-price',
|
|
119
|
+
unit_amount: +config.product.price.month,
|
|
120
|
+
currency_id: currency.id,
|
|
121
|
+
recurring: {
|
|
122
|
+
interval: 'month',
|
|
123
|
+
interval_count: 1,
|
|
124
|
+
usage_type: 'licensed',
|
|
125
|
+
},
|
|
126
|
+
metadata,
|
|
127
|
+
});
|
|
128
|
+
console.warn(`paywall resource month price recreated from path: ${configPath}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let newYearPriceId = '';
|
|
132
|
+
if (!yearPrice) {
|
|
133
|
+
newYearPriceId = nextPriceId();
|
|
134
|
+
await createPrice({
|
|
135
|
+
id: newYearPriceId,
|
|
136
|
+
product_id: exist.id,
|
|
137
|
+
type: 'recurring',
|
|
138
|
+
model: 'standard',
|
|
139
|
+
nickname: 'yearly-member-price',
|
|
140
|
+
unit_amount: +config.product.price.year,
|
|
141
|
+
currency_id: currency.id,
|
|
142
|
+
recurring: {
|
|
143
|
+
interval: 'year',
|
|
144
|
+
interval_count: 1,
|
|
145
|
+
usage_type: 'licensed',
|
|
146
|
+
},
|
|
147
|
+
metadata,
|
|
148
|
+
});
|
|
149
|
+
console.warn(`paywall resource year price recreated from path: ${configPath}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// update upsell relation
|
|
153
|
+
if (newYearPriceId) {
|
|
154
|
+
console.warn(`paywall resource upsell recreated from path: ${configPath}`);
|
|
155
|
+
if (newMonthPriceId) {
|
|
156
|
+
await Price.update({ upsell: { upsells_to_id: newYearPriceId } }, { where: { id: newMonthPriceId } });
|
|
157
|
+
} else {
|
|
158
|
+
await Price.update({ upsell: { upsells_to_id: newYearPriceId } }, { where: { id: monthPrice.id } });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Create another payment link and redo updatePassportExtra
|
|
163
|
+
if (newMonthPriceId) {
|
|
164
|
+
await Product.update({ default_price_id: newMonthPriceId }, { where: { id: exist.id }, limit: 1 });
|
|
165
|
+
|
|
166
|
+
const link = await createPaymentLink({
|
|
167
|
+
name: `Paywall for membership of ${config.product.name}`,
|
|
168
|
+
livemode: config.product.livemode,
|
|
169
|
+
currency_id: currency.id,
|
|
170
|
+
line_items: [
|
|
171
|
+
{
|
|
172
|
+
price_id: newMonthPriceId,
|
|
173
|
+
quantity: 1,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
metadata,
|
|
177
|
+
});
|
|
178
|
+
console.info('payment link recreated for paywall resource', { link: link.id });
|
|
179
|
+
|
|
180
|
+
await Promise.all(
|
|
181
|
+
config.passports.map((x: string) =>
|
|
182
|
+
updatePassportExtra(x, {
|
|
183
|
+
payment: { product: exist.id },
|
|
184
|
+
acquire: { pay: link.id },
|
|
185
|
+
})
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
console.info('product and payment link reassociated with passport');
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
const monthPriceId = nextPriceId();
|
|
192
|
+
const yearPriceId = nextPriceId();
|
|
193
|
+
|
|
194
|
+
const product = await createProductAndPrices({
|
|
195
|
+
name: config.product.name,
|
|
196
|
+
type: 'service',
|
|
197
|
+
description: config.product.description,
|
|
198
|
+
livemode: config.product.livemode,
|
|
199
|
+
images: [],
|
|
200
|
+
features: [],
|
|
201
|
+
metadata,
|
|
202
|
+
prices: [
|
|
203
|
+
{
|
|
204
|
+
id: monthPriceId,
|
|
205
|
+
type: 'recurring',
|
|
206
|
+
model: 'standard',
|
|
207
|
+
nickname: 'monthly-member-price',
|
|
208
|
+
unit_amount: +config.product.price.month,
|
|
209
|
+
currency_id: currency.id,
|
|
210
|
+
recurring: {
|
|
211
|
+
interval: 'month',
|
|
212
|
+
interval_count: 1,
|
|
213
|
+
usage_type: 'licensed',
|
|
214
|
+
},
|
|
215
|
+
upsell: config.product.price.upsell ? { upsells_to_id: yearPriceId } : undefined,
|
|
216
|
+
metadata,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: yearPriceId,
|
|
220
|
+
type: 'recurring',
|
|
221
|
+
model: 'standard',
|
|
222
|
+
nickname: 'yearly-member-price',
|
|
223
|
+
unit_amount: +config.product.price.year,
|
|
224
|
+
currency_id: currency.id,
|
|
225
|
+
recurring: {
|
|
226
|
+
interval: 'year',
|
|
227
|
+
interval_count: 1,
|
|
228
|
+
usage_type: 'licensed',
|
|
229
|
+
},
|
|
230
|
+
metadata,
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
console.info('product created for paywall resource', { product: product.id });
|
|
235
|
+
|
|
236
|
+
const link = await createPaymentLink({
|
|
237
|
+
name: `Paywall for membership of ${config.product.name}`,
|
|
238
|
+
livemode: config.product.livemode,
|
|
239
|
+
currency_id: currency.id,
|
|
240
|
+
line_items: [
|
|
241
|
+
{
|
|
242
|
+
price_id: monthPriceId,
|
|
243
|
+
quantity: 1,
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
metadata,
|
|
247
|
+
});
|
|
248
|
+
console.info('payment link created for paywall resource', { link: link.id });
|
|
249
|
+
|
|
250
|
+
await Promise.all(
|
|
251
|
+
config.passports.map((x: string) =>
|
|
252
|
+
updatePassportExtra(x, {
|
|
253
|
+
payment: { product: product.id },
|
|
254
|
+
acquire: { pay: link.id },
|
|
255
|
+
})
|
|
256
|
+
)
|
|
257
|
+
);
|
|
258
|
+
console.info('product and payment link associated with passport');
|
|
259
|
+
}
|
|
260
|
+
console.info(`paywall resource successfully imported from path: ${configPath}`);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.error(`failed to import paywall resource from ${configPath}`, err);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
package/api/src/libs/util.ts
CHANGED
|
@@ -14,6 +14,8 @@ export const MAX_SUBSCRIPTION_ITEM_COUNT = 20;
|
|
|
14
14
|
export const MAX_RETRY_COUNT = 20; // 2^20 seconds ~~ 12 days, total retry time: 24 days
|
|
15
15
|
export const MIN_RETRY_MAIL = 13; // total retry time before sending first mail: 6 hours
|
|
16
16
|
|
|
17
|
+
export const CHECKOUT_SESSION_TTL = 24 * 60 * 60; // expires in 24 hours
|
|
18
|
+
|
|
17
19
|
export const STRIPE_API_VERSION = '2023-08-16';
|
|
18
20
|
export const STRIPE_ENDPOINT: string = getUrl('/api/integrations/stripe/webhook');
|
|
19
21
|
export const STRIPE_EVENTS: any[] = [
|
|
@@ -1,10 +1,71 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop */
|
|
2
|
+
import { Op } from 'sequelize';
|
|
3
|
+
|
|
1
4
|
import { mintNftForCheckoutSession } from '../integrations/blockchain/nft';
|
|
2
5
|
import { ensurePassportIssued, ensurePassportRevoked } from '../integrations/blocklet/passport';
|
|
6
|
+
import dayjs from '../libs/dayjs';
|
|
3
7
|
import { events } from '../libs/event';
|
|
4
8
|
import logger from '../libs/logger';
|
|
5
|
-
import
|
|
9
|
+
import createQueue from '../libs/queue';
|
|
10
|
+
import {
|
|
11
|
+
CheckoutSession,
|
|
12
|
+
Invoice,
|
|
13
|
+
InvoiceItem,
|
|
14
|
+
PaymentIntent,
|
|
15
|
+
Price,
|
|
16
|
+
SetupIntent,
|
|
17
|
+
Subscription,
|
|
18
|
+
SubscriptionItem,
|
|
19
|
+
} from '../store/models';
|
|
6
20
|
import { subscriptionQueue } from './subscription';
|
|
7
21
|
|
|
22
|
+
type CheckoutSessionJob = {
|
|
23
|
+
id: string;
|
|
24
|
+
action: 'expire';
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const checkoutSessionQueue = createQueue<CheckoutSessionJob>({
|
|
28
|
+
name: 'checkoutSession',
|
|
29
|
+
onJob: handleCheckoutSessionJob,
|
|
30
|
+
options: {
|
|
31
|
+
concurrency: 10,
|
|
32
|
+
maxRetries: 3,
|
|
33
|
+
enableScheduledJob: true,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export async function handleCheckoutSessionJob(job: CheckoutSessionJob): Promise<void> {
|
|
38
|
+
const checkoutSession = await CheckoutSession.findByPk(job.id);
|
|
39
|
+
if (!checkoutSession) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (job.action === 'expire') {
|
|
43
|
+
if (checkoutSession.status !== 'open') {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (checkoutSession.payment_status === 'paid') {
|
|
47
|
+
logger.info('Skip expire CheckoutSession since payment status is paid', {
|
|
48
|
+
checkoutSession: checkoutSession.id,
|
|
49
|
+
paymentIntent: checkoutSession.payment_intent_id,
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const now = dayjs().unix();
|
|
55
|
+
if (checkoutSession.expires_at > now) {
|
|
56
|
+
checkoutSessionQueue.push({
|
|
57
|
+
id: checkoutSession.id,
|
|
58
|
+
job: { id: checkoutSession.id, action: 'expire' },
|
|
59
|
+
runAt: checkoutSession.expires_at,
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await checkoutSession.update({ status: 'expired' });
|
|
65
|
+
logger.info('CheckoutSession expired', { checkoutSession: checkoutSession.id });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
8
69
|
// eslint-disable-next-line require-await
|
|
9
70
|
export async function startCheckoutSessionQueue() {
|
|
10
71
|
events.on('checkout.session.completed', (checkoutSession: CheckoutSession) => {
|
|
@@ -42,4 +103,83 @@ export async function startCheckoutSessionQueue() {
|
|
|
42
103
|
runAt: subscription.current_period_end,
|
|
43
104
|
});
|
|
44
105
|
});
|
|
106
|
+
|
|
107
|
+
events.on('checkout.session.created', (checkoutSession: CheckoutSession) => {
|
|
108
|
+
if (checkoutSession.expires_at) {
|
|
109
|
+
checkoutSessionQueue.push({
|
|
110
|
+
id: checkoutSession.id,
|
|
111
|
+
job: { id: checkoutSession.id, action: 'expire' },
|
|
112
|
+
runAt: checkoutSession.expires_at,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
events.on('checkout.session.expired', async (checkoutSession: CheckoutSession) => {
|
|
118
|
+
// Do some cleanup
|
|
119
|
+
if (checkoutSession.invoice_id) {
|
|
120
|
+
await InvoiceItem.destroy({ where: { invoice_id: checkoutSession.invoice_id } });
|
|
121
|
+
await Invoice.destroy({ where: { id: checkoutSession.invoice_id } });
|
|
122
|
+
logger.info('Invoice and InvoiceItem for checkout session deleted on expire', {
|
|
123
|
+
checkoutSession: checkoutSession.id,
|
|
124
|
+
invoice: checkoutSession.invoice_id,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (checkoutSession.setup_intent_id) {
|
|
128
|
+
await SetupIntent.destroy({ where: { id: checkoutSession.setup_intent_id } });
|
|
129
|
+
logger.info('SetupIntent for checkout session deleted on expire', {
|
|
130
|
+
checkoutSession: checkoutSession.id,
|
|
131
|
+
setupIntent: checkoutSession.setup_intent_id,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (checkoutSession.payment_intent_id && checkoutSession.payment_status !== 'paid') {
|
|
135
|
+
await PaymentIntent.destroy({ where: { id: checkoutSession.payment_intent_id } });
|
|
136
|
+
logger.info('PaymentIntent for checkout session deleted on expire', {
|
|
137
|
+
checkoutSession: checkoutSession.id,
|
|
138
|
+
paymentIntent: checkoutSession.payment_intent_id,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (checkoutSession.subscription_id) {
|
|
142
|
+
await SubscriptionItem.destroy({ where: { subscription_id: checkoutSession.subscription_id } });
|
|
143
|
+
await Subscription.destroy({ where: { id: checkoutSession.subscription_id } });
|
|
144
|
+
logger.info('Subscription and SubscriptionItem for checkout session deleted on expire', {
|
|
145
|
+
checkoutSession: checkoutSession.id,
|
|
146
|
+
subscription: checkoutSession.subscription_id,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// update price lock status
|
|
151
|
+
for (const item of checkoutSession.line_items) {
|
|
152
|
+
const price = await Price.findByPk(item.price_id);
|
|
153
|
+
if (price?.locked) {
|
|
154
|
+
const used = await price.isUsed();
|
|
155
|
+
if (!used) {
|
|
156
|
+
await price.update({ locked: false });
|
|
157
|
+
logger.info('Price for checkout session unlocked on expire', {
|
|
158
|
+
checkoutSession: checkoutSession.id,
|
|
159
|
+
priceId: item.price_id,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Auto populate subscription queue
|
|
167
|
+
const now = dayjs().unix();
|
|
168
|
+
const checkoutSessions = await CheckoutSession.findAll({
|
|
169
|
+
where: {
|
|
170
|
+
status: 'open',
|
|
171
|
+
expires_at: { [Op.lte]: now },
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
checkoutSessions.forEach(async (checkoutSession) => {
|
|
176
|
+
const exist = await checkoutSessionQueue.get(checkoutSession.id);
|
|
177
|
+
if (!exist) {
|
|
178
|
+
checkoutSessionQueue.push({
|
|
179
|
+
id: checkoutSession.id,
|
|
180
|
+
job: { id: checkoutSession.id, action: 'expire' },
|
|
181
|
+
runAt: checkoutSession.expires_at,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
45
185
|
}
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
isLineItemAligned,
|
|
34
34
|
} from '../libs/session';
|
|
35
35
|
import { getDaysUntilDue } from '../libs/subscription';
|
|
36
|
-
import { createCodeGenerator, formatMetadata, getDataObjectFromQuery } from '../libs/util';
|
|
36
|
+
import { CHECKOUT_SESSION_TTL, createCodeGenerator, formatMetadata, getDataObjectFromQuery } from '../libs/util';
|
|
37
37
|
import { invoiceQueue } from '../queues/invoice';
|
|
38
38
|
import { paymentQueue } from '../queues/payment';
|
|
39
39
|
import { subscriptionQueue } from '../queues/subscription';
|
|
@@ -127,7 +127,7 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
if (!raw.expires_at) {
|
|
130
|
-
raw.expires_at = dayjs().unix() +
|
|
130
|
+
raw.expires_at = dayjs().unix() + CHECKOUT_SESSION_TTL; // 24 hours after creation
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
if (raw.nft_mint_settings?.enabled) {
|
|
@@ -924,6 +924,16 @@ router.put('/:id/downsell', user, ensureCheckoutSessionOpen, async (req, res) =>
|
|
|
924
924
|
router.put('/:id/expire', auth, ensureCheckoutSessionOpen, async (req, res) => {
|
|
925
925
|
const doc = req.doc as CheckoutSession;
|
|
926
926
|
|
|
927
|
+
if (doc.status === 'complete') {
|
|
928
|
+
return res.status(400).json({ error: 'Cannot expire checkout session that is already completed' });
|
|
929
|
+
}
|
|
930
|
+
if (doc.status === 'expired') {
|
|
931
|
+
return res.status(400).json({ error: 'Cannot expire checkout session that is already expired' });
|
|
932
|
+
}
|
|
933
|
+
if (doc.payment_status === 'paid') {
|
|
934
|
+
return res.status(400).json({ error: 'Cannot expire checkout session that is already paid' });
|
|
935
|
+
}
|
|
936
|
+
|
|
927
937
|
await doc.update({ status: 'expired', expires_at: dayjs().unix() });
|
|
928
938
|
|
|
929
939
|
res.json(doc);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
-
import merge from 'lodash/merge';
|
|
3
2
|
|
|
3
|
+
import { updatePassportExtra } from '../integrations/blocklet/passport';
|
|
4
4
|
import { blocklet } from '../libs/auth';
|
|
5
5
|
import { authenticate } from '../libs/security';
|
|
6
6
|
import { PaymentLink, PricingTable, Product } from '../store/models';
|
|
@@ -13,15 +13,6 @@ router.get('/', auth, async (_, res) => {
|
|
|
13
13
|
res.json(result.roles);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
const updateRoleExtra = async (name: string, updates: any) => {
|
|
17
|
-
const { role } = await blocklet.getRole(name);
|
|
18
|
-
if (!role) {
|
|
19
|
-
throw new Error(`passport ${name} not found`);
|
|
20
|
-
}
|
|
21
|
-
const result = await blocklet.updateRole(name, { extra: JSON.stringify(merge(role.extra, updates)) });
|
|
22
|
-
return result.role;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
16
|
router.put('/assign', auth, async (req, res) => {
|
|
26
17
|
const { name, id } = req.body;
|
|
27
18
|
|
|
@@ -38,7 +29,7 @@ router.put('/assign', auth, async (req, res) => {
|
|
|
38
29
|
return res.status(400).json({ message: 'payment link is not active' });
|
|
39
30
|
}
|
|
40
31
|
|
|
41
|
-
const result = await
|
|
32
|
+
const result = await updatePassportExtra(name, { acquire: { pay: id } });
|
|
42
33
|
return res.json(result);
|
|
43
34
|
}
|
|
44
35
|
|
|
@@ -48,7 +39,7 @@ router.put('/assign', auth, async (req, res) => {
|
|
|
48
39
|
return res.status(400).json({ message: 'pricing table is not active' });
|
|
49
40
|
}
|
|
50
41
|
|
|
51
|
-
const result = await
|
|
42
|
+
const result = await updatePassportExtra(name, { acquire: { pay: id } });
|
|
52
43
|
return res.json(result);
|
|
53
44
|
}
|
|
54
45
|
|
|
@@ -59,7 +50,7 @@ router.put('/assign', auth, async (req, res) => {
|
|
|
59
50
|
}
|
|
60
51
|
|
|
61
52
|
await doc.update({ metadata: { ...doc.metadata, passport: name } });
|
|
62
|
-
const result = await
|
|
53
|
+
const result = await updatePassportExtra(name, { payment: { product: id } });
|
|
63
54
|
return res.json(result);
|
|
64
55
|
}
|
|
65
56
|
|
|
@@ -67,7 +58,10 @@ router.put('/assign', auth, async (req, res) => {
|
|
|
67
58
|
});
|
|
68
59
|
|
|
69
60
|
router.delete('/assign/:name', auth, async (req, res) => {
|
|
70
|
-
const result = await
|
|
61
|
+
const result = await updatePassportExtra(req.params.name as string, {
|
|
62
|
+
payment: { product: '' },
|
|
63
|
+
acquire: { pay: '' },
|
|
64
|
+
});
|
|
71
65
|
return res.json(result);
|
|
72
66
|
});
|
|
73
67
|
|