payment-kit 1.18.37 → 1.18.39

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.
Files changed (51) hide show
  1. package/api/src/integrations/stripe/handlers/invoice.ts +22 -0
  2. package/api/src/integrations/stripe/handlers/payment-intent.ts +9 -1
  3. package/api/src/integrations/stripe/handlers/subscription.ts +137 -1
  4. package/api/src/queues/payment.ts +4 -0
  5. package/api/src/routes/subscriptions.ts +24 -17
  6. package/blocklet.yml +1 -1
  7. package/package.json +20 -20
  8. package/src/app.tsx +124 -125
  9. package/src/components/chart.tsx +19 -5
  10. package/src/components/customer/notification-preference.tsx +6 -5
  11. package/src/components/date-range-picker.tsx +1 -16
  12. package/src/components/drawer-form.tsx +4 -3
  13. package/src/components/filter-toolbar.tsx +49 -34
  14. package/src/components/info-card.tsx +9 -7
  15. package/src/components/invoice-pdf/pdf.tsx +1 -1
  16. package/src/components/layout/admin.tsx +2 -2
  17. package/src/components/metadata/form.tsx +3 -2
  18. package/src/components/metadata/list.tsx +1 -1
  19. package/src/components/payment-link/chrome.tsx +0 -1
  20. package/src/components/payment-link/item.tsx +1 -1
  21. package/src/components/payment-link/preview.tsx +3 -1
  22. package/src/components/price/currency-select.tsx +1 -1
  23. package/src/components/pricing-table/payment-settings.tsx +1 -1
  24. package/src/components/pricing-table/preview.tsx +3 -1
  25. package/src/components/pricing-table/product-item.tsx +1 -1
  26. package/src/components/subscription/portal/actions.tsx +134 -24
  27. package/src/components/subscription/portal/list.tsx +3 -2
  28. package/src/components/uploader.tsx +2 -1
  29. package/src/components/webhook/attempts.tsx +1 -1
  30. package/src/global.css +0 -7
  31. package/src/pages/admin/developers/webhooks/detail.tsx +2 -2
  32. package/src/pages/admin/index.tsx +2 -2
  33. package/src/pages/admin/overview.tsx +1 -1
  34. package/src/pages/admin/payments/intents/detail.tsx +5 -2
  35. package/src/pages/admin/payments/payouts/detail.tsx +1 -1
  36. package/src/pages/admin/payments/refunds/detail.tsx +1 -1
  37. package/src/pages/admin/products/prices/list.tsx +7 -2
  38. package/src/pages/admin/products/pricing-tables/create.tsx +2 -2
  39. package/src/pages/admin/settings/payment-methods/index.tsx +7 -4
  40. package/src/pages/admin/settings/vault-config/index.tsx +3 -2
  41. package/src/pages/checkout/pricing-table.tsx +1 -1
  42. package/src/pages/customer/index.tsx +4 -3
  43. package/src/pages/customer/invoice/detail.tsx +1 -1
  44. package/src/pages/customer/payout/detail.tsx +1 -1
  45. package/src/pages/customer/recharge/account.tsx +3 -4
  46. package/src/pages/customer/recharge/subscription.tsx +3 -4
  47. package/src/pages/customer/subscription/detail.tsx +4 -1
  48. package/src/pages/customer/subscription/embed.tsx +2 -2
  49. package/src/pages/integrations/donations/preview.tsx +12 -10
  50. package/src/pages/integrations/index.tsx +1 -1
  51. package/src/pages/integrations/overview.tsx +2 -4
@@ -19,6 +19,7 @@ import {
19
19
  TEventExpanded,
20
20
  TInvoiceItem,
21
21
  } from '../../../store/models';
22
+ import { handleSubscriptionOnPaymentFailure } from './subscription';
22
23
 
23
24
  export async function handleStripeInvoicePaid(invoice: Invoice, event: TEventExpanded) {
24
25
  logger.info('invoice paid on stripe event', { locale: invoice.id });
@@ -91,6 +92,16 @@ export async function syncStripeInvoice(invoice: Invoice) {
91
92
  )
92
93
  );
93
94
  logger.info('stripe invoice synced', { locale: invoice.id, remote: stripeInvoice.id });
95
+ const failedStatuses = ['uncollectible', 'finalization_failed', 'payment_failed'];
96
+ if (failedStatuses.includes(stripeInvoice.status || '')) {
97
+ logger.info('Invoice failed', { invoiceId: invoice.id });
98
+ if (invoice.subscription_id) {
99
+ const subscription = await Subscription.findByPk(invoice.subscription_id);
100
+ if (subscription) {
101
+ handleSubscriptionOnPaymentFailure(subscription, 'syncStripeInvoice', client);
102
+ }
103
+ }
104
+ }
94
105
  }
95
106
  }
96
107
 
@@ -376,4 +387,15 @@ export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe)
376
387
  await invoice.update({ status: 'payment_failed' });
377
388
  logger.info('invoice payment failed on stripe event', { locale: invoice.id });
378
389
  }
390
+
391
+ const failedEvents = ['invoice.marked_uncollectible', 'invoice.finalization_failed', 'invoice.payment_failed'];
392
+ if (failedEvents.includes(event.type)) {
393
+ logger.info('Invoice failed', { invoiceId: invoice.id });
394
+ if (invoice.subscription_id) {
395
+ const subscription = await Subscription.findByPk(invoice.subscription_id);
396
+ if (subscription) {
397
+ handleSubscriptionOnPaymentFailure(subscription, event.type, client);
398
+ }
399
+ }
400
+ }
379
401
  }
@@ -7,9 +7,10 @@ import type Stripe from 'stripe';
7
7
  import dayjs from '../../../libs/dayjs';
8
8
  import logger from '../../../libs/logger';
9
9
  import { handlePaymentSucceed } from '../../../queues/payment';
10
- import { Invoice, PaymentIntent, PaymentMethod, TEventExpanded } from '../../../store/models';
10
+ import { Invoice, PaymentIntent, PaymentMethod, Subscription, TEventExpanded } from '../../../store/models';
11
11
  import { handleStripeInvoiceCreated } from './invoice';
12
12
  import { events } from '../../../libs/event';
13
+ import { handleSubscriptionOnPaymentFailure } from './subscription';
13
14
 
14
15
  export async function handleStripePaymentSucceed(paymentIntent: PaymentIntent, event?: TEventExpanded) {
15
16
  const triggerRenew = paymentIntent.status !== 'succeeded';
@@ -184,6 +185,13 @@ export async function handlePaymentIntentEvent(event: TEventExpanded, client: St
184
185
  attempted: true,
185
186
  status_transitions: { ...invoice.status_transitions, marked_uncollectible_at: dayjs().unix() },
186
187
  });
188
+
189
+ if (invoice.subscription_id) {
190
+ const subscription = await Subscription.findByPk(invoice.subscription_id);
191
+ if (subscription) {
192
+ handleSubscriptionOnPaymentFailure(subscription, event.type, client);
193
+ }
194
+ }
187
195
  }
188
196
  }
189
197
  }
@@ -1,8 +1,15 @@
1
1
  import pick from 'lodash/pick';
2
2
  import type Stripe from 'stripe';
3
3
 
4
+ import dayjs from '../../../libs/dayjs';
4
5
  import logger from '../../../libs/logger';
5
- import { finalizeStripeSubscriptionUpdate } from '../../../libs/subscription';
6
+ import {
7
+ finalizeStripeSubscriptionUpdate,
8
+ getDaysUntilCancel,
9
+ getDaysUntilDue,
10
+ getDueUnit,
11
+ shouldCancelSubscription,
12
+ } from '../../../libs/subscription';
6
13
  import { CheckoutSession, PaymentMethod, Subscription, TEventExpanded } from '../../../store/models';
7
14
  import { getCheckoutSessionSubscriptionIds } from '../../../libs/session';
8
15
  import { createEvent } from '../../../libs/audit';
@@ -139,3 +146,132 @@ export async function handleSubscriptionEvent(event: TEventExpanded, _: Stripe)
139
146
  }
140
147
  }
141
148
  }
149
+
150
+ export async function syncStripeSubscriptionAfterRecovery(subscription: Subscription, paymentIntentId: string) {
151
+ if (!subscription.payment_details?.stripe?.subscription_id) {
152
+ return;
153
+ }
154
+
155
+ try {
156
+ const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
157
+ if (method && method.type === 'stripe') {
158
+ const client = await method.getStripeClient();
159
+ await client.subscriptions.update(subscription.payment_details.stripe.subscription_id, {
160
+ cancel_at_period_end: false,
161
+ cancel_at: null,
162
+ });
163
+
164
+ logger.info('Removed cancellation settings in Stripe after payment success', {
165
+ subscription: subscription.id,
166
+ stripeSubscription: subscription.payment_details.stripe.subscription_id,
167
+ paymentIntent: paymentIntentId,
168
+ });
169
+ }
170
+ } catch (err) {
171
+ logger.error('Failed to update Stripe subscription after payment success', {
172
+ error: err,
173
+ subscription: subscription.id,
174
+ paymentIntent: paymentIntentId,
175
+ });
176
+ }
177
+ }
178
+
179
+ export async function handleSubscriptionOnPaymentFailure(
180
+ subscription: Subscription,
181
+ eventType: string,
182
+ client: Stripe
183
+ ) {
184
+ if (!subscription || !subscription.isActive()) {
185
+ logger.warn('Subscription is not active or not found', { subscription: subscription.id });
186
+ return;
187
+ }
188
+
189
+ const now = dayjs().unix();
190
+ const { interval } = subscription.pending_invoice_item_interval;
191
+ const dueUnit = getDueUnit(interval);
192
+ const cancelUpdates: { [key: string]: any } = {};
193
+ const daysUntilCancel = getDaysUntilCancel(subscription);
194
+ const cancelSubscription = shouldCancelSubscription(subscription);
195
+
196
+ if (daysUntilCancel > 0) {
197
+ cancelUpdates.cancel_at = subscription.current_period_start + daysUntilCancel * dueUnit;
198
+ } else {
199
+ cancelUpdates.cancel_at_period_end = true;
200
+ }
201
+
202
+ if (cancelSubscription) {
203
+ await subscription.update({
204
+ status: 'canceled',
205
+ canceled_at: now,
206
+ cancelation_details: {
207
+ comment: 'exceed_current_period',
208
+ feedback: 'other',
209
+ reason: 'payment_failed',
210
+ },
211
+ });
212
+ logger.warn(`[${eventType}] Subscription moved to canceled after payment failed`, {
213
+ subscription: subscription.id,
214
+ });
215
+ } else {
216
+ // check grace period
217
+ const daysUntilDue = getDaysUntilDue(subscription);
218
+ if (typeof daysUntilDue === 'number') {
219
+ const gracePeriodStart = subscription.current_period_start;
220
+ const graceDuration = daysUntilDue ? daysUntilDue * dueUnit : 0;
221
+
222
+ if (gracePeriodStart + graceDuration <= now) {
223
+ // past grace period, set to past_due
224
+ await subscription.update({
225
+ status: 'past_due',
226
+ ...cancelUpdates,
227
+ cancelation_details: {
228
+ comment: 'past_due',
229
+ feedback: 'other',
230
+ reason: 'payment_failed',
231
+ },
232
+ });
233
+ logger.warn(`[${eventType}] Subscription moved to past_due after payment failed`, {
234
+ subscription: subscription.id,
235
+ });
236
+ }
237
+ }
238
+ }
239
+
240
+ // sync to stripe
241
+ if (subscription.payment_details?.stripe?.subscription_id && client) {
242
+ try {
243
+ const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
244
+ if (method && method.type === 'stripe') {
245
+ const stripeUpdates: any = {
246
+ cancellation_details: {
247
+ comment: 'past_due',
248
+ feedback: 'other',
249
+ reason: 'payment_failed',
250
+ },
251
+ };
252
+
253
+ if (cancelUpdates.cancel_at) {
254
+ stripeUpdates.cancel_at = cancelUpdates.cancel_at;
255
+ } else if (cancelUpdates.cancel_at_period_end) {
256
+ stripeUpdates.cancel_at_period_end = true;
257
+ }
258
+ if (cancelSubscription) {
259
+ stripeUpdates.cancel_at = now;
260
+ stripeUpdates.cancel_at_period_end = false;
261
+ }
262
+
263
+ await client.subscriptions.update(subscription.payment_details.stripe.subscription_id, stripeUpdates);
264
+ logger.info(`[${eventType}] Updated subscription in Stripe after payment failed`, {
265
+ subscription: subscription.id,
266
+ stripeSubscription: subscription.payment_details.stripe.subscription_id,
267
+ updates: stripeUpdates,
268
+ });
269
+ }
270
+ } catch (err) {
271
+ logger.error(`[${eventType}] Failed to update subscription in Stripe`, {
272
+ error: err,
273
+ subscription: subscription.id,
274
+ });
275
+ }
276
+ }
277
+ }
@@ -41,6 +41,7 @@ import { ensureOverdraftProtectionPrice } from '../libs/overdraft-protection';
41
41
  import createQueue from '../libs/queue';
42
42
  import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from '../libs/constants';
43
43
  import { getCheckoutSessionSubscriptionIds, getSubscriptionCreateSetup } from '../libs/session';
44
+ import { syncStripeSubscriptionAfterRecovery } from '../integrations/stripe/handlers/subscription';
44
45
 
45
46
  type PaymentJob = {
46
47
  paymentIntentId: string;
@@ -246,6 +247,7 @@ export const handlePaymentSucceed = async (
246
247
  cancelation_details: null,
247
248
  });
248
249
  logger.info(`Subscription ${subscription.id} recovered on payment done ${paymentIntent.id}: cancel rest`);
250
+ await syncStripeSubscriptionAfterRecovery(subscription, paymentIntent.id);
249
251
  return;
250
252
  }
251
253
  const subscriptionItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
@@ -267,11 +269,13 @@ export const handlePaymentSucceed = async (
267
269
  logger.info(
268
270
  `Subscription ${subscription.id} recovered on payment done ${paymentIntent.id}: cancel and billing cycle reset`
269
271
  );
272
+ await syncStripeSubscriptionAfterRecovery(subscription, paymentIntent.id);
270
273
  } else if (subscription.cancel_at_period_end) {
271
274
  // reset cancel_at_period_end if we are recovering from payment failed
272
275
  // @ts-ignore
273
276
  await subscription.update({ status: 'active', cancel_at_period_end: false, cancelation_details: null });
274
277
  logger.info(`Subscription ${subscription.id} recovered on payment done ${paymentIntent.id}: cancel reset`);
278
+ await syncStripeSubscriptionAfterRecovery(subscription, paymentIntent.id);
275
279
  } else {
276
280
  await subscription.update({ status: 'active' });
277
281
  logger.info(`Subscription ${subscription.id} recovered on payment done ${paymentIntent.id}`);
@@ -1742,30 +1742,37 @@ router.get('/:id/cycle-amount', authPortal, async (req, res) => {
1742
1742
  if (!currency) {
1743
1743
  return res.status(404).json({ error: 'Currency not found' });
1744
1744
  }
1745
- // get upcoming invoice
1746
- const result = await getUpcomingInvoiceAmount(subscription.id);
1747
- // get past invoices
1748
- const pastMaxAmount = await getPastInvoicesAmount(subscription.id, 'max');
1745
+ try {
1746
+ // get upcoming invoice
1747
+ const result = await getUpcomingInvoiceAmount(subscription.id);
1748
+ // get past invoices
1749
+ const pastMaxAmount = await getPastInvoicesAmount(subscription.id, 'max');
1749
1750
 
1750
- // return max amount
1751
- const nextAmount = new BN(result.amount === '0' ? result.minExpectedAmount : result.amount).toString();
1751
+ // return max amount
1752
+ const nextAmount = new BN(result.amount === '0' ? result.minExpectedAmount : result.amount).toString();
1752
1753
 
1753
- const maxAmount = new BN(pastMaxAmount.amount).lte(new BN(nextAmount)) ? nextAmount : pastMaxAmount.amount;
1754
+ const maxAmount = new BN(pastMaxAmount.amount).lte(new BN(nextAmount)) ? nextAmount : pastMaxAmount.amount;
1754
1755
 
1755
- if (req.query?.overdraftProtection) {
1756
- const { price } = await ensureOverdraftProtectionPrice(subscription.livemode);
1757
- const invoicePrice = (price?.currency_options || []).find((x: any) => x.currency_id === subscription?.currency_id);
1758
- const gas = invoicePrice?.unit_amount;
1756
+ if (req.query?.overdraftProtection) {
1757
+ const { price } = await ensureOverdraftProtectionPrice(subscription.livemode);
1758
+ const invoicePrice = (price?.currency_options || []).find(
1759
+ (x: any) => x.currency_id === subscription?.currency_id
1760
+ );
1761
+ const gas = invoicePrice?.unit_amount;
1762
+ return res.json({
1763
+ amount: new BN(maxAmount).add(new BN(gas)).toString(),
1764
+ gas,
1765
+ currency,
1766
+ });
1767
+ }
1759
1768
  return res.json({
1760
- amount: new BN(maxAmount).add(new BN(gas)).toString(),
1761
- gas,
1769
+ amount: maxAmount,
1762
1770
  currency,
1763
1771
  });
1772
+ } catch (err) {
1773
+ logger.error(err);
1774
+ return res.status(400).json({ error: err.message });
1764
1775
  }
1765
- return res.json({
1766
- amount: maxAmount,
1767
- currency,
1768
- });
1769
1776
  });
1770
1777
 
1771
1778
  // slash stake
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.18.37
17
+ version: 1.18.39
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.18.37",
3
+ "version": "1.18.39",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -45,30 +45,30 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@abtnode/cron": "^1.16.42",
48
- "@arcblock/did": "^1.20.2",
48
+ "@arcblock/did": "^1.20.6",
49
49
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
50
- "@arcblock/did-connect": "^2.13.13",
51
- "@arcblock/did-util": "^1.20.2",
52
- "@arcblock/jwt": "^1.20.2",
53
- "@arcblock/ux": "^2.13.13",
54
- "@arcblock/validator": "^1.20.2",
55
- "@blocklet/did-space-js": "^1.0.48",
50
+ "@arcblock/did-connect": "^2.13.23",
51
+ "@arcblock/did-util": "^1.20.6",
52
+ "@arcblock/jwt": "^1.20.6",
53
+ "@arcblock/ux": "^2.13.23",
54
+ "@arcblock/validator": "^1.20.6",
55
+ "@blocklet/did-space-js": "^1.0.49",
56
56
  "@blocklet/js-sdk": "^1.16.42",
57
57
  "@blocklet/logger": "^1.16.42",
58
- "@blocklet/payment-react": "1.18.37",
58
+ "@blocklet/payment-react": "1.18.39",
59
59
  "@blocklet/sdk": "^1.16.42",
60
- "@blocklet/ui-react": "^2.13.13",
61
- "@blocklet/uploader": "^0.1.84",
62
- "@blocklet/xss": "^0.1.33",
60
+ "@blocklet/ui-react": "^2.13.23",
61
+ "@blocklet/uploader": "^0.1.85",
62
+ "@blocklet/xss": "^0.1.34",
63
63
  "@mui/icons-material": "^5.16.6",
64
64
  "@mui/lab": "^5.0.0-alpha.173",
65
65
  "@mui/material": "^5.16.6",
66
66
  "@mui/system": "^5.16.6",
67
- "@ocap/asset": "^1.20.2",
68
- "@ocap/client": "^1.20.2",
69
- "@ocap/mcrypto": "^1.20.2",
70
- "@ocap/util": "^1.20.2",
71
- "@ocap/wallet": "^1.20.2",
67
+ "@ocap/asset": "^1.20.6",
68
+ "@ocap/client": "^1.20.6",
69
+ "@ocap/mcrypto": "^1.20.6",
70
+ "@ocap/util": "^1.20.6",
71
+ "@ocap/wallet": "^1.20.6",
72
72
  "@stripe/react-stripe-js": "^2.7.3",
73
73
  "@stripe/stripe-js": "^2.4.0",
74
74
  "ahooks": "^3.8.0",
@@ -123,7 +123,7 @@
123
123
  "devDependencies": {
124
124
  "@abtnode/types": "^1.16.42",
125
125
  "@arcblock/eslint-config-ts": "^0.3.3",
126
- "@blocklet/payment-types": "1.18.37",
126
+ "@blocklet/payment-types": "1.18.39",
127
127
  "@types/cookie-parser": "^1.4.7",
128
128
  "@types/cors": "^2.8.17",
129
129
  "@types/debug": "^4.1.12",
@@ -153,7 +153,7 @@
153
153
  "vite": "^5.3.5",
154
154
  "vite-node": "^2.0.4",
155
155
  "vite-plugin-babel-import": "^2.0.5",
156
- "vite-plugin-blocklet": "^0.9.31",
156
+ "vite-plugin-blocklet": "^0.9.32",
157
157
  "vite-plugin-node-polyfills": "^0.21.0",
158
158
  "vite-plugin-svgr": "^4.2.0",
159
159
  "vite-tsconfig-paths": "^4.3.2",
@@ -169,5 +169,5 @@
169
169
  "parser": "typescript"
170
170
  }
171
171
  },
172
- "gitHead": "ea43bad3259d340da14fc0dd0e5b02f2c88c5093"
172
+ "gitHead": "9e9a5c18fc160094505e902e995f5a5a052d544f"
173
173
  }