payment-kit 1.13.97 → 1.13.99

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,5 +1,6 @@
1
1
  import { Op } from 'sequelize';
2
2
 
3
+ import { createEvent } from '../libs/audit';
3
4
  import dayjs from '../libs/dayjs';
4
5
  import logger from '../libs/logger';
5
6
  import createQueue from '../libs/queue';
@@ -57,9 +58,18 @@ export const handleInvoice = async (job: InvoiceJob) => {
57
58
 
58
59
  if (invoice.subscription_id) {
59
60
  const subscription = await Subscription.findByPk(invoice.subscription_id);
60
- if (subscription && subscription.status === 'incomplete') {
61
- await subscription.start();
62
- logger.info('invoice subscription updated', subscription.id);
61
+ if (subscription) {
62
+ if (subscription.status === 'incomplete') {
63
+ await subscription.start();
64
+ logger.info('invoice subscription updated', subscription.id);
65
+ } else {
66
+ if (invoice.billing_reason === 'subscription_cycle') {
67
+ createEvent('Subscription', 'customer.subscription.renewed', subscription).catch(console.error);
68
+ }
69
+ if (invoice.billing_reason === 'subscription_update') {
70
+ createEvent('Subscription', 'customer.subscription.upgraded', subscription).catch(console.error);
71
+ }
72
+ }
63
73
  }
64
74
  }
65
75
 
@@ -78,6 +78,9 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent, invoice
78
78
  if (invoice.billing_reason === 'subscription_cycle') {
79
79
  createEvent('Subscription', 'customer.subscription.renewed', subscription).catch(console.error);
80
80
  }
81
+ if (invoice.billing_reason === 'subscription_update') {
82
+ createEvent('Subscription', 'customer.subscription.upgraded', subscription).catch(console.error);
83
+ }
81
84
  }
82
85
 
83
86
  if (invoice.checkout_session_id) {
@@ -469,9 +469,8 @@ export async function ensureInvoiceForCollect(invoiceId: string) {
469
469
  if (paymentIntent.status === 'succeeded') {
470
470
  throw new Error(`Payment intent already succeeded for invoice ${invoiceId}`);
471
471
  }
472
- if (paymentIntent.status === 'processing') {
473
- throw new Error(`Payment intent processing for invoice ${invoiceId}`);
474
- }
472
+
473
+ await paymentIntent.update({ status: 'requires_capture' });
475
474
 
476
475
  const paymentCurrency = await PaymentCurrency.findByPk(paymentIntent.currency_id);
477
476
  const paymentMethod = await PaymentMethod.findByPk(paymentIntent.payment_method_id);
@@ -405,35 +405,18 @@ const validateSubscriptionUpdateRequest = async (subscription: Subscription, ite
405
405
  throw new Error('Subscription item should not have duplicate price_id');
406
406
  }
407
407
 
408
- const addedItems = await Price.expand(items.filter((x: any) => x.price_id && !x.id) as LineItem[]);
409
- if (addedItems.some((x) => !x.price)) {
410
- throw new Error('Subscription item should not use non-exist price');
411
- }
412
- if (addedItems.some((x) => !x.price.active)) {
413
- throw new Error('Subscription item should not use archived price');
414
- }
415
- for (let i = 0; i < addedItems.length; i++) {
416
- const result = isLineItemAligned(addedItems, i);
417
- if (result.currency === false) {
418
- throw new Error('New subscription items should have same currency');
419
- }
420
- if (result.recurring === false) {
421
- throw new Error('New subscription items should have same recurring');
422
- }
423
- }
424
-
408
+ // split items into added, deleted
409
+ const addedItems = items.filter((x: any) => x.price_id && !x.id);
410
+ const deletedItems = items.filter((x: any) => x.deleted && x.id);
411
+ const updatedItems = items.filter((x: any) => !x.deleted && x.id);
425
412
  const existingItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
426
- const deletedItems = items.filter((x: any) => x.deleted && x.id).map((x: any) => x.id);
427
- if (existingItems.length - deletedItems.length + addedItems.length === 0) {
428
- throw new Error('Subscription should have at least one subscription item');
429
- }
430
-
431
- const existingExpanded = await Price.expand(
432
- existingItems.filter((x) => deletedItems.includes(x.id) === false).map((x) => x.toJSON())
433
- );
434
413
 
435
414
  // try handle cross-sell with different interval, just replace with new price that have same interval
436
- const newRecurring = addedItems.find((x) => x.price.type === 'recurring')?.price.recurring;
415
+ let addedExpanded = await Price.expand(addedItems as LineItem[]);
416
+ let existingExpanded = await Price.expand(
417
+ existingItems.filter((x) => deletedItems.some((y) => y.id === x.id) === false).map((x) => x.toJSON())
418
+ );
419
+ const newRecurring = addedExpanded.find((x) => x.price.type === 'recurring')?.price.recurring;
437
420
  await Promise.all(
438
421
  existingExpanded.map(async (x: TLineItemExpanded) => {
439
422
  const oldRecurring = x.price.recurring;
@@ -453,14 +436,23 @@ const validateSubscriptionUpdateRequest = async (subscription: Subscription, ite
453
436
  );
454
437
 
455
438
  if (newPrice) {
439
+ deletedItems.push({
440
+ id: x.id,
441
+ deleted: true,
442
+ clear_usage: false,
443
+ });
444
+ addedItems.push({
445
+ price_id: newPrice.id,
446
+ quantity: x.quantity,
447
+ });
448
+
456
449
  logger.info('Replace subscription item on update', {
457
450
  subscription: subscription.id,
458
451
  item: x.id,
459
452
  oldPrice: x.price.id,
460
453
  newPrice: newPrice.id,
461
454
  });
462
- x.price_id = newPrice.id;
463
- x.price = newPrice.toJSON();
455
+
464
456
  return x;
465
457
  }
466
458
 
@@ -471,8 +463,22 @@ const validateSubscriptionUpdateRequest = async (subscription: Subscription, ite
471
463
  })
472
464
  );
473
465
 
474
- // FIXME: following logic should be extracted
475
- const newItems: any[] = [...existingExpanded, ...addedItems];
466
+ addedExpanded = await Price.expand(addedItems as LineItem[]);
467
+ existingExpanded = await Price.expand(
468
+ existingItems.filter((x) => deletedItems.some((y) => y.id === x.id) === false).map((x) => x.toJSON())
469
+ );
470
+ if (addedExpanded.some((x) => !x.price)) {
471
+ throw new Error('Subscription item should not use non-exist price');
472
+ }
473
+ if (addedExpanded.some((x) => !x.price.active)) {
474
+ throw new Error('Subscription item should not use archived price');
475
+ }
476
+
477
+ const updatedExpanded = await Price.expand(updatedItems as LineItem[]);
478
+ const newItems: any[] = [...existingExpanded, ...addedExpanded, ...updatedExpanded];
479
+ if (newItems.length === 0) {
480
+ throw new Error('Subscription should have at least one subscription item');
481
+ }
476
482
  if (newItems.length > MAX_SUBSCRIPTION_ITEM_COUNT) {
477
483
  throw new Error(`Subscription should not have more than ${MAX_SUBSCRIPTION_ITEM_COUNT} items`);
478
484
  }
@@ -489,6 +495,9 @@ const validateSubscriptionUpdateRequest = async (subscription: Subscription, ite
489
495
  return {
490
496
  existingItems,
491
497
  addedItems,
498
+ updatedItems,
499
+ deletedItems,
500
+ addedExpanded,
492
501
  newItems,
493
502
  };
494
503
  };
@@ -684,40 +693,40 @@ router.put('/:id', authPortal, async (req, res) => {
684
693
  // handle subscription item changes
685
694
  if (Array.isArray(req.body.items) && req.body.items.length > 0) {
686
695
  // validate the request
687
- const { existingItems, addedItems, newItems } = await validateSubscriptionUpdateRequest(
688
- subscription,
689
- req.body.items
690
- );
696
+ const { existingItems, addedItems, updatedItems, deletedItems, newItems } =
697
+ await validateSubscriptionUpdateRequest(subscription, req.body.items);
691
698
 
692
699
  // update subscription items
693
- for (const item of req.body.items) {
694
- if (item.deleted) {
695
- if (item.clear_usage) {
696
- await UsageRecord.destroy({ where: { subscription_item_id: item.id } });
697
- logger.info('subscription item usage cleared', { subscription: req.params.id, item: item.id });
698
- }
699
- await SubscriptionItem.destroy({ where: { id: item.id } });
700
- logger.info('subscription item deleted', { subscription: req.params.id, item: item.id });
701
- } else {
702
- const exist = existingItems.find((x) => x.id === item.id);
703
- if (exist) {
704
- await exist.update(pick(item, ['quantity', 'metadata', 'billing_thresholds']));
705
- logger.info('subscription item updated', { subscription: req.params.id, item: item.id });
706
- } else {
707
- await SubscriptionItem.create({
708
- ...item,
709
- livemode: subscription.livemode,
710
- subscription_id: subscription.id,
711
- });
712
- logger.info('subscription item added', { subscription: req.params.id, item: item.id });
713
- }
700
+ for (const item of addedItems) {
701
+ await SubscriptionItem.create({
702
+ price_id: item.price_id as string,
703
+ quantity: item.quantity as number,
704
+ livemode: subscription.livemode,
705
+ subscription_id: subscription.id,
706
+ metadata: {},
707
+ });
708
+ logger.info('subscription item added', { subscription: req.params.id, item: item.id });
709
+ }
710
+ for (const item of updatedItems) {
711
+ const exist = existingItems.find((x) => x.id === item.id);
712
+ if (exist) {
713
+ await exist.update(pick(item, ['quantity', 'metadata', 'billing_thresholds']));
714
+ logger.info('subscription item updated', { subscription: req.params.id, item: item.id });
715
+ }
716
+ }
717
+ for (const item of deletedItems) {
718
+ if (item.clear_usage) {
719
+ await UsageRecord.destroy({ where: { subscription_item_id: item.id } });
720
+ logger.info('subscription item usage cleared', { subscription: req.params.id, item: item.id });
714
721
  }
722
+ await SubscriptionItem.destroy({ where: { id: item.id } });
723
+ logger.info('subscription item deleted', { subscription: req.params.id, item: item.id });
715
724
  }
716
725
 
717
726
  // update subscription period settings
718
727
  // HINT: if we are adding new items, we need to reset the anchor to now
719
728
  const setup = getSubscriptionCreateSetup(newItems, paymentCurrency.id, 0);
720
- if (addedItems.some((x) => x.price.type === 'recurring')) {
729
+ if (newItems.some((x) => x.price.type === 'recurring' && addedItems.find((y) => y.price_id === x.price_id))) {
721
730
  updates.pending_invoice_item_interval = setup.recurring;
722
731
  updates.current_period_start = setup.period.start;
723
732
  updates.current_period_end = setup.period.end;
@@ -980,7 +989,10 @@ router.post('/:id/update', authPortal, async (req, res) => {
980
989
  }
981
990
 
982
991
  // validate the request
983
- const { newItems } = await validateSubscriptionUpdateRequest(subscription, req.body.items);
992
+ const { newItems, addedItems, deletedItems, updatedItems } = await validateSubscriptionUpdateRequest(
993
+ subscription,
994
+ req.body.items
995
+ );
984
996
 
985
997
  // do the simulation
986
998
  const setup = getSubscriptionCreateSetup(newItems, subscription.currency_id, 0);
@@ -992,6 +1004,9 @@ router.post('/:id/update', authPortal, async (req, res) => {
992
1004
  credit: result.credit,
993
1005
  prorations: result.prorations,
994
1006
  items: newItems,
1007
+ addedItems,
1008
+ deletedItems,
1009
+ updatedItems,
995
1010
  });
996
1011
  } catch (err) {
997
1012
  console.error(err);
@@ -413,6 +413,7 @@ export type EventType = LiteralUnion<
413
413
  | 'customer.subscription.pending_update_expired'
414
414
  | 'customer.subscription.resumed'
415
415
  | 'customer.subscription.renewed'
416
+ | 'customer.subscription.upgraded'
416
417
  | 'customer.subscription.renew_failed'
417
418
  | 'customer.subscription.trial_start'
418
419
  | 'customer.subscription.trial_will_end'
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.97
17
+ version: 1.13.99
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.97",
3
+ "version": "1.13.99",
4
4
  "scripts": {
5
5
  "dev": "COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev",
6
6
  "eject": "vite eject",
@@ -110,7 +110,7 @@
110
110
  "@abtnode/types": "1.16.21",
111
111
  "@arcblock/eslint-config": "^0.2.4",
112
112
  "@arcblock/eslint-config-ts": "^0.2.4",
113
- "@did-pay/types": "1.13.97",
113
+ "@did-pay/types": "1.13.99",
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": "f9ca9bf2030d0a2552b71c66470cbaf3964df0f8"
152
+ "gitHead": "abb8f6d451350b07f5755fe4d599e8a438b3cfe1"
153
153
  }