payment-kit 1.13.170 → 1.13.172
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 +8 -1
- package/api/src/integrations/stripe/handlers/subscription.ts +1 -1
- package/api/src/integrations/stripe/resource.ts +68 -5
- package/api/src/libs/env.ts +2 -1
- package/api/src/queues/subscription.ts +3 -0
- package/api/src/routes/checkout-sessions.ts +5 -1
- package/api/src/routes/connect/shared.ts +3 -0
- package/api/src/routes/customers.ts +6 -2
- package/api/src/routes/events.ts +2 -2
- package/api/src/routes/invoices.ts +2 -2
- package/api/src/routes/payment-currencies.ts +5 -1
- package/api/src/routes/payment-intents.ts +6 -2
- package/api/src/routes/payment-links.ts +5 -1
- package/api/src/routes/payment-methods.ts +5 -1
- package/api/src/routes/prices.ts +6 -1
- package/api/src/routes/products.ts +6 -1
- package/api/src/routes/subscription-items.ts +6 -2
- package/api/src/routes/subscriptions.ts +2 -2
- package/blocklet.yml +1 -1
- package/package.json +4 -4
package/api/src/crons/index.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import Cron from '@abtnode/cron';
|
|
2
2
|
|
|
3
|
-
import { batchHandleStripeInvoices } from '../integrations/stripe/resource';
|
|
3
|
+
import { batchHandleStripeInvoices, batchHandleStripeSubscriptions } from '../integrations/stripe/resource';
|
|
4
4
|
import {
|
|
5
5
|
expiredSessionCleanupCronTime,
|
|
6
6
|
notificationCronTime,
|
|
7
7
|
stripeInvoiceCronTime,
|
|
8
|
+
stripeSubscriptionCronTime,
|
|
8
9
|
subscriptionCronTime,
|
|
9
10
|
} from '../libs/env';
|
|
10
11
|
import logger from '../libs/logger';
|
|
@@ -57,6 +58,12 @@ function init() {
|
|
|
57
58
|
fn: batchHandleStripeInvoices,
|
|
58
59
|
options: { runOnInit: false },
|
|
59
60
|
},
|
|
61
|
+
{
|
|
62
|
+
name: 'stripe.subscription.sync',
|
|
63
|
+
time: stripeSubscriptionCronTime,
|
|
64
|
+
fn: batchHandleStripeSubscriptions,
|
|
65
|
+
options: { runOnInit: false },
|
|
66
|
+
},
|
|
60
67
|
],
|
|
61
68
|
onError: (error: Error, name: string) => {
|
|
62
69
|
logger.error('run job failed', { name, error: error.message, stack: error.stack });
|
|
@@ -43,7 +43,7 @@ export async function handleSubscriptionEvent(event: TEventExpanded, _: Stripe)
|
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const fields = ['cancel_at', 'cancel_at_period_end', 'canceled_at'];
|
|
46
|
+
const fields = ['cancel_at', 'cancel_at_period_end', 'canceled_at', 'current_period_start', 'current_period_end'];
|
|
47
47
|
if (subscription.payment_settings?.payment_method_types?.includes('stripe')) {
|
|
48
48
|
fields.push('pause_collection');
|
|
49
49
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
/* eslint-disable no-continue */
|
|
2
|
+
/* eslint-disable no-await-in-loop */
|
|
1
3
|
import env from '@blocklet/sdk/lib/env';
|
|
2
4
|
import merge from 'lodash/merge';
|
|
5
|
+
import omit from 'lodash/omit';
|
|
6
|
+
import pick from 'lodash/pick';
|
|
3
7
|
import { Op } from 'sequelize';
|
|
4
8
|
|
|
5
9
|
import logger from '../../libs/logger';
|
|
@@ -347,15 +351,15 @@ export async function batchHandleStripeInvoices() {
|
|
|
347
351
|
},
|
|
348
352
|
});
|
|
349
353
|
|
|
350
|
-
|
|
354
|
+
for (const invoice of stripeInvoices) {
|
|
351
355
|
const stripeInvoiceId = invoice.metadata?.stripe_id;
|
|
352
356
|
if (!stripeInvoiceId) {
|
|
353
|
-
|
|
357
|
+
continue;
|
|
354
358
|
}
|
|
355
359
|
|
|
356
360
|
const method = stripeMethods.find((m) => m.livemode === invoice.livemode);
|
|
357
361
|
if (!method) {
|
|
358
|
-
|
|
362
|
+
continue;
|
|
359
363
|
}
|
|
360
364
|
|
|
361
365
|
const client = method.getStripeClient();
|
|
@@ -369,8 +373,6 @@ export async function batchHandleStripeInvoices() {
|
|
|
369
373
|
await client.invoices.pay(stripeInvoiceId);
|
|
370
374
|
logger.info('stripe invoice payment requested', { local: invoice.id, stripe: stripeInvoiceId });
|
|
371
375
|
|
|
372
|
-
await sleep(5000);
|
|
373
|
-
|
|
374
376
|
await syncStripeInvoice(invoice);
|
|
375
377
|
|
|
376
378
|
if (invoice.payment_intent_id) {
|
|
@@ -392,5 +394,66 @@ export async function batchHandleStripeInvoices() {
|
|
|
392
394
|
logger.error('stripe invoice finalize error', error);
|
|
393
395
|
}
|
|
394
396
|
}
|
|
397
|
+
|
|
398
|
+
await sleep(5000);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function batchHandleStripeSubscriptions() {
|
|
403
|
+
const stripeMethods = await PaymentMethod.findAll({ where: { type: 'stripe' } });
|
|
404
|
+
if (stripeMethods.length === 0) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const subscriptions = await Subscription.findAll({
|
|
409
|
+
where: {
|
|
410
|
+
status: { [Op.not]: ['canceled', 'incomplete', 'incomplete_expired'] },
|
|
411
|
+
'payment_details.stripe.subscription_id': { [Op.not]: null },
|
|
412
|
+
},
|
|
395
413
|
});
|
|
414
|
+
|
|
415
|
+
for (const subscription of subscriptions) {
|
|
416
|
+
const subscriptionId = subscription.payment_details?.stripe?.subscription_id;
|
|
417
|
+
if (!subscriptionId) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const method = stripeMethods.find((m) => m.livemode === subscription.livemode);
|
|
422
|
+
if (!method) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const client = method.getStripeClient();
|
|
427
|
+
try {
|
|
428
|
+
const exist = await client.subscriptions.retrieve(subscriptionId);
|
|
429
|
+
if (exist) {
|
|
430
|
+
const fields = [
|
|
431
|
+
'cancel_at',
|
|
432
|
+
'cancel_at_period_end',
|
|
433
|
+
'canceled_at',
|
|
434
|
+
'current_period_start',
|
|
435
|
+
'current_period_end',
|
|
436
|
+
];
|
|
437
|
+
if (subscription.payment_settings?.payment_method_types?.includes('stripe')) {
|
|
438
|
+
fields.push('pause_collection');
|
|
439
|
+
}
|
|
440
|
+
await subscription.update(pick(exist, fields) as any);
|
|
441
|
+
logger.warn('stripe subscription synced', { local: subscription.id, stripe: subscriptionId });
|
|
442
|
+
} else {
|
|
443
|
+
logger.warn('stripe subscription missing', { local: subscription.id, stripe: subscriptionId });
|
|
444
|
+
}
|
|
445
|
+
} catch (error) {
|
|
446
|
+
logger.error('stripe subscription sync error', { error, local: subscription.id, stripe: subscriptionId });
|
|
447
|
+
if (error.message.includes('No such subscription')) {
|
|
448
|
+
await subscription.update({
|
|
449
|
+
payment_details: {
|
|
450
|
+
...subscription.payment_details,
|
|
451
|
+
stripe: omit(subscription.payment_details?.stripe, ['subscription_id']),
|
|
452
|
+
},
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
await sleep(3000);
|
|
458
|
+
}
|
|
396
459
|
}
|
package/api/src/libs/env.ts
CHANGED
|
@@ -4,7 +4,8 @@ export const subscriptionCronTime: string = process.env.SUBSCRIPTION_CRON_TIME |
|
|
|
4
4
|
export const notificationCronTime: string = process.env.NOTIFICATION_CRON_TIME || '0 5 */6 * * *'; // 默认每6个小时执行一次
|
|
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
|
-
export const stripeInvoiceCronTime: string = process.env.
|
|
7
|
+
export const stripeInvoiceCronTime: string = process.env.STRIPE_INVOICE_CRON_TIME || '0 */30 * * * *'; // 默认每 30min 执行一次
|
|
8
|
+
export const stripeSubscriptionCronTime: string = process.env.STRIPE_SUBSCRIPTION_CRON_TIME || '0 10 */8 * * *'; // 默认每 8小时 执行一次
|
|
8
9
|
|
|
9
10
|
export default {
|
|
10
11
|
...env,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LiteralUnion } from 'type-fest';
|
|
2
2
|
|
|
3
3
|
import { ensurePassportRevoked } from '../integrations/blocklet/passport';
|
|
4
|
+
import { batchHandleStripeSubscriptions } from '../integrations/stripe/resource';
|
|
4
5
|
import dayjs from '../libs/dayjs';
|
|
5
6
|
import { events } from '../libs/event';
|
|
6
7
|
import { getLock } from '../libs/lock';
|
|
@@ -391,6 +392,8 @@ export const startSubscriptionQueue = async () => {
|
|
|
391
392
|
}
|
|
392
393
|
await addSubscriptionJob(x, 'cycle');
|
|
393
394
|
});
|
|
395
|
+
|
|
396
|
+
await batchHandleStripeSubscriptions();
|
|
394
397
|
};
|
|
395
398
|
|
|
396
399
|
export async function addSubscriptionJob(
|
|
@@ -392,7 +392,11 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
392
392
|
doc.url = getUrl(`/checkout/${doc.submit_type}/${doc.id}`);
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
-
|
|
395
|
+
if (doc) {
|
|
396
|
+
res.json(doc?.toJSON());
|
|
397
|
+
} else {
|
|
398
|
+
res.status(404).json(null);
|
|
399
|
+
}
|
|
396
400
|
});
|
|
397
401
|
|
|
398
402
|
// for checkout page
|
|
@@ -106,6 +106,9 @@ export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: s
|
|
|
106
106
|
throw new Error('Customer not found');
|
|
107
107
|
}
|
|
108
108
|
const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
|
|
109
|
+
if (!user) {
|
|
110
|
+
throw new Error('Seems you have not connected to this app before');
|
|
111
|
+
}
|
|
109
112
|
if (customer.did !== user.did) {
|
|
110
113
|
throw new Error('This is not your payment intent');
|
|
111
114
|
}
|
|
@@ -122,10 +122,14 @@ router.get('/me', user(), async (req, res) => {
|
|
|
122
122
|
router.get('/:id', auth, async (req, res) => {
|
|
123
123
|
try {
|
|
124
124
|
const doc = await Customer.findByPkOrDid(req.params.id as string);
|
|
125
|
-
|
|
125
|
+
if (doc) {
|
|
126
|
+
res.json(doc);
|
|
127
|
+
} else {
|
|
128
|
+
res.status(404).json(null);
|
|
129
|
+
}
|
|
126
130
|
} catch (err) {
|
|
127
131
|
console.error(err);
|
|
128
|
-
res.json(
|
|
132
|
+
res.status(500).json({ error: `Failed to get customer: ${err.message}` });
|
|
129
133
|
}
|
|
130
134
|
});
|
|
131
135
|
|
package/api/src/routes/events.ts
CHANGED
|
@@ -68,11 +68,11 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
68
68
|
if (doc) {
|
|
69
69
|
res.json(doc);
|
|
70
70
|
} else {
|
|
71
|
-
res.json(null);
|
|
71
|
+
res.status(404).json(null);
|
|
72
72
|
}
|
|
73
73
|
} catch (err) {
|
|
74
74
|
console.error(err);
|
|
75
|
-
res.json(
|
|
75
|
+
res.status(500).json({ error: `Failed to get event: ${err.message}` });
|
|
76
76
|
}
|
|
77
77
|
});
|
|
78
78
|
|
|
@@ -183,11 +183,11 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
183
183
|
expandLineItems(json.lines, products, prices);
|
|
184
184
|
res.json(json);
|
|
185
185
|
} else {
|
|
186
|
-
res.json(null);
|
|
186
|
+
res.status(404).json(null);
|
|
187
187
|
}
|
|
188
188
|
} catch (err) {
|
|
189
189
|
console.error(err);
|
|
190
|
-
res.json(
|
|
190
|
+
res.status(500).json({ error: `Failed to get invoice: ${err.message}` });
|
|
191
191
|
}
|
|
192
192
|
});
|
|
193
193
|
|
|
@@ -32,7 +32,11 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
32
32
|
where: { [Op.or]: [{ id: req.params.id }, { symbol: req.params.id }] },
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
if (doc) {
|
|
36
|
+
res.json(doc);
|
|
37
|
+
} else {
|
|
38
|
+
res.status(404).json(null);
|
|
39
|
+
}
|
|
36
40
|
});
|
|
37
41
|
|
|
38
42
|
export default router;
|
|
@@ -186,10 +186,14 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
if (doc) {
|
|
190
|
+
res.json({ ...doc.toJSON(), checkoutSession, invoice, subscription });
|
|
191
|
+
} else {
|
|
192
|
+
res.status(404).json(null);
|
|
193
|
+
}
|
|
190
194
|
} catch (err) {
|
|
191
195
|
console.error(err);
|
|
192
|
-
res.json(
|
|
196
|
+
res.status(500).json({ error: `Failed to get payment intent: ${err.message}` });
|
|
193
197
|
}
|
|
194
198
|
});
|
|
195
199
|
|
|
@@ -201,7 +201,11 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
201
201
|
doc.line_items = await Price.expand(doc.line_items);
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
|
|
204
|
+
if (doc) {
|
|
205
|
+
res.json(doc);
|
|
206
|
+
} else {
|
|
207
|
+
res.status(404).json(null);
|
|
208
|
+
}
|
|
205
209
|
});
|
|
206
210
|
|
|
207
211
|
// update
|
|
@@ -130,7 +130,11 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
130
130
|
where: { [Op.or]: [{ id: req.params.id }, { name: req.params.id }] },
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
if (doc) {
|
|
134
|
+
res.json(doc);
|
|
135
|
+
} else {
|
|
136
|
+
res.status(404).json(null);
|
|
137
|
+
}
|
|
134
138
|
});
|
|
135
139
|
|
|
136
140
|
export default router;
|
package/api/src/routes/prices.ts
CHANGED
|
@@ -200,7 +200,12 @@ router.post('/', auth, async (req, res) => {
|
|
|
200
200
|
|
|
201
201
|
// get price detail
|
|
202
202
|
router.get('/:id', auth, async (req, res) => {
|
|
203
|
-
|
|
203
|
+
const doc = await getExpandedPrice(req.params.id as string);
|
|
204
|
+
if (doc) {
|
|
205
|
+
res.json(doc);
|
|
206
|
+
} else {
|
|
207
|
+
res.status(404).json(null);
|
|
208
|
+
}
|
|
204
209
|
});
|
|
205
210
|
|
|
206
211
|
// get price used status
|
|
@@ -208,7 +208,12 @@ router.get('/search', auth, async (req, res) => {
|
|
|
208
208
|
|
|
209
209
|
// get product detail
|
|
210
210
|
router.get('/:id', auth, async (req, res) => {
|
|
211
|
-
|
|
211
|
+
const doc = await Product.expand(req.params.id as string);
|
|
212
|
+
if (doc) {
|
|
213
|
+
res.json(doc);
|
|
214
|
+
} else {
|
|
215
|
+
res.status(404).json(null);
|
|
216
|
+
}
|
|
212
217
|
});
|
|
213
218
|
|
|
214
219
|
// update product
|
|
@@ -96,10 +96,14 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
96
96
|
where: { id: req.params.id },
|
|
97
97
|
include: [{ model: Price, as: 'price' }],
|
|
98
98
|
});
|
|
99
|
-
|
|
99
|
+
if (doc) {
|
|
100
|
+
res.json(doc);
|
|
101
|
+
} else {
|
|
102
|
+
res.status(404).json(null);
|
|
103
|
+
}
|
|
100
104
|
} catch (err) {
|
|
101
105
|
console.error(err);
|
|
102
|
-
res.json(
|
|
106
|
+
res.status(500).json({ error: `Failed to get subscription item: ${err.message}` });
|
|
103
107
|
}
|
|
104
108
|
});
|
|
105
109
|
|
|
@@ -212,11 +212,11 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
212
212
|
expandLineItems(json.items, products, prices);
|
|
213
213
|
res.json(json);
|
|
214
214
|
} else {
|
|
215
|
-
res.json(null);
|
|
215
|
+
res.status(404).json(null);
|
|
216
216
|
}
|
|
217
217
|
} catch (err) {
|
|
218
218
|
console.error(err);
|
|
219
|
-
res.json(
|
|
219
|
+
res.status(500).json({ error: `Failed to get subscription: ${err.message}` });
|
|
220
220
|
}
|
|
221
221
|
});
|
|
222
222
|
|
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.172",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "cross-env COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@arcblock/jwt": "^1.18.110",
|
|
51
51
|
"@arcblock/ux": "^2.9.39",
|
|
52
52
|
"@blocklet/logger": "1.16.23",
|
|
53
|
-
"@blocklet/payment-react": "1.13.
|
|
53
|
+
"@blocklet/payment-react": "1.13.172",
|
|
54
54
|
"@blocklet/sdk": "1.16.23",
|
|
55
55
|
"@blocklet/ui-react": "^2.9.39",
|
|
56
56
|
"@blocklet/uploader": "^0.0.74",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"devDependencies": {
|
|
111
111
|
"@abtnode/types": "1.16.23",
|
|
112
112
|
"@arcblock/eslint-config-ts": "^0.2.4",
|
|
113
|
-
"@blocklet/payment-types": "1.13.
|
|
113
|
+
"@blocklet/payment-types": "1.13.172",
|
|
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": "bb4f4aec316f48c3e299fe97758b00fb4380cda8"
|
|
153
153
|
}
|