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.
@@ -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
- stripeInvoices.forEach(async (invoice) => {
354
+ for (const invoice of stripeInvoices) {
351
355
  const stripeInvoiceId = invoice.metadata?.stripe_id;
352
356
  if (!stripeInvoiceId) {
353
- return;
357
+ continue;
354
358
  }
355
359
 
356
360
  const method = stripeMethods.find((m) => m.livemode === invoice.livemode);
357
361
  if (!method) {
358
- return;
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
  }
@@ -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.PAYMENT_STRIPE_CRON_TIME || '0 */30 * * * *'; // 默认每 30min 执行一次
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
- res.json(doc?.toJSON());
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
- res.json(doc);
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(null);
132
+ res.status(500).json({ error: `Failed to get customer: ${err.message}` });
129
133
  }
130
134
  });
131
135
 
@@ -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(null);
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(null);
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
- res.json(doc);
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
- res.json({ ...doc?.toJSON(), checkoutSession, invoice, subscription });
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(null);
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
- res.json(doc);
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
- res.json(doc);
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;
@@ -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
- res.json(await getExpandedPrice(req.params.id as string));
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
- res.json(await Product.expand(req.params.id as string));
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
- res.json(doc);
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(null);
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(null);
219
+ res.status(500).json({ error: `Failed to get subscription: ${err.message}` });
220
220
  }
221
221
  });
222
222
 
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.13.170
17
+ version: 1.13.172
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.13.170",
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.170",
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.170",
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": "4a04bc6c92a74d683a6e84035972beb914f355ce"
152
+ "gitHead": "bb4f4aec316f48c3e299fe97758b00fb4380cda8"
153
153
  }