payment-kit 1.13.195 → 1.13.196

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.
@@ -2,10 +2,35 @@ import pick from 'lodash/pick';
2
2
  import type Stripe from 'stripe';
3
3
 
4
4
  import logger from '../../../libs/logger';
5
- import { CheckoutSession, Subscription, TEventExpanded } from '../../../store/models';
5
+ import { CheckoutSession, PaymentMethod, Subscription, TEventExpanded } from '../../../store/models';
6
6
 
7
- export async function handleStripeSubscriptionSucceed(subscription: Subscription) {
8
- await subscription.update({ status: 'active' });
7
+ export async function handleStripeSubscriptionSucceed(subscription: Subscription, status: string) {
8
+ if (!subscription.payment_details?.stripe?.subscription_id) {
9
+ return;
10
+ }
11
+
12
+ // do not move to active if actual setup is not done
13
+ const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
14
+ const client = method!.getStripeClient();
15
+ const result: any = await client.subscriptions.retrieve(subscription.payment_details.stripe.subscription_id, {
16
+ expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],
17
+ });
18
+ if (result.pending_setup_intent && result.pending_setup_intent.status !== 'succeeded') {
19
+ logger.warn('subscription can not active because stripe setup not done', {
20
+ id: subscription.id,
21
+ status: result.pending_setup_intent.status,
22
+ });
23
+ return;
24
+ }
25
+ if (result.latest_invoice?.payment_intent && result.latest_invoice.payment_intent !== 'succeeded') {
26
+ logger.warn('subscription can not active because stripe payment not done', {
27
+ id: subscription.id,
28
+ status: result.latest_invoice.payment_intent.status,
29
+ });
30
+ return;
31
+ }
32
+
33
+ await subscription.update({ status });
9
34
  logger.info('subscription become active on stripe event', { id: subscription.id, status: subscription.status });
10
35
 
11
36
  const checkoutSession = await CheckoutSession.findOne({ where: { payment_intent_id: subscription.id } });
@@ -36,8 +61,11 @@ export async function handleSubscriptionEvent(event: TEventExpanded, _: Stripe)
36
61
  logger.info('received subscription event', { id: event.id, type: event.type, localSubscriptionId });
37
62
 
38
63
  if (event.type === 'customer.subscription.updated') {
39
- if (event.data.previous_attributes?.status === 'incomplete' && event.data.object.status === 'active') {
40
- await handleStripeSubscriptionSucceed(subscription);
64
+ if (
65
+ event.data.previous_attributes?.status === 'incomplete' &&
66
+ ['active', 'trialing'].includes(event.data.object.status)
67
+ ) {
68
+ await handleStripeSubscriptionSucceed(subscription, event.data.object.status);
41
69
  return;
42
70
  }
43
71
 
@@ -60,12 +88,20 @@ export async function handleSubscriptionEvent(event: TEventExpanded, _: Stripe)
60
88
  }
61
89
 
62
90
  if (event.type === 'customer.subscription.deleted') {
63
- await subscription.update({ status: 'canceled', ended_at: event.data.object.ended_at });
64
- logger.info('subscription ended on stripe event', { id: subscription.id });
91
+ if (['incomplete', 'incomplete_expired'].includes(subscription.status)) {
92
+ logger.warn('subscription not ended on stripe event', { id: subscription.id });
93
+ } else {
94
+ await subscription.update({ status: 'canceled', ended_at: event.data.object.ended_at });
95
+ logger.info('subscription ended on stripe event', { id: subscription.id });
96
+ }
65
97
  }
66
98
 
67
99
  if (event.type === 'customer.subscription.paused') {
68
- await subscription.update({ status: 'paused', pause_collection: event.data.object.pause_collection });
69
- logger.info('subscription paused on stripe event', { id: subscription.id });
100
+ if (['incomplete', 'incomplete_expired'].includes(subscription.status)) {
101
+ logger.warn('subscription not paused on stripe event', { id: subscription.id });
102
+ } else {
103
+ await subscription.update({ status: 'paused', pause_collection: event.data.object.pause_collection });
104
+ logger.info('subscription paused on stripe event', { id: subscription.id });
105
+ }
70
106
  }
71
107
  }
@@ -446,7 +446,7 @@ export async function batchHandleStripeSubscriptions() {
446
446
  }
447
447
 
448
448
  await subscription.update(updates);
449
- logger.warn('stripe subscription synced', { local: subscription.id, stripe: subscriptionId, updates });
449
+ logger.info('stripe subscription synced', { local: subscription.id, stripe: subscriptionId, updates });
450
450
  } else {
451
451
  logger.warn('stripe subscription missing', { local: subscription.id, stripe: subscriptionId });
452
452
  }
@@ -2,7 +2,7 @@ import env from '@blocklet/sdk/lib/env';
2
2
 
3
3
  export const subscriptionCronTime: string = process.env.SUBSCRIPTION_CRON_TIME || '0 */30 * * * *'; // 默认每 30 min 行一次
4
4
  export const notificationCronTime: string = process.env.NOTIFICATION_CRON_TIME || '0 5 */6 * * *'; // 默认每6个小时执行一次
5
- export const expiredSessionCleanupCronTime: string = process.env.EXPIRED_SESSION_CLEANUP_CRON_TIME || '0 1 */2 * * *'; // 默认每2个小时执行一次
5
+ export const expiredSessionCleanupCronTime: string = process.env.EXPIRED_SESSION_CLEANUP_CRON_TIME || '0 1 * * * *'; // 默认每小时执行一次
6
6
  export const notificationCronConcurrency: number = Number(process.env.NOTIFICATION_CRON_CONCURRENCY) || 8; // 默认并发数为 8
7
7
  export const stripeInvoiceCronTime: string = process.env.STRIPE_INVOICE_CRON_TIME || '0 */30 * * * *'; // 默认每 30min 执行一次
8
8
  export const stripePaymentCronTime: string = process.env.STRIPE_PAYMENT_CRON_TIME || '0 */20 * * * *'; // 默认每 20min 执行一次
@@ -14,7 +14,7 @@ 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
17
+ export const CHECKOUT_SESSION_TTL = 6 * 60 * 60; // expires in 6 hours, then removed after 12 hours
18
18
 
19
19
  export const STRIPE_API_VERSION = '2023-08-16';
20
20
  export const STRIPE_ENDPOINT: string = getUrl('/api/integrations/stripe/webhook');
@@ -12,6 +12,7 @@ import {
12
12
  Invoice,
13
13
  InvoiceItem,
14
14
  PaymentIntent,
15
+ PaymentMethod,
15
16
  Price,
16
17
  SetupIntent,
17
18
  Subscription,
@@ -123,6 +124,8 @@ events.on('checkout.session.created', (checkoutSession: CheckoutSession) => {
123
124
  });
124
125
 
125
126
  events.on('checkout.session.expired', async (checkoutSession: CheckoutSession) => {
127
+ logger.info('Start cleanup on checkoutSession expired', { checkoutSession: checkoutSession.id });
128
+
126
129
  // Do some cleanup
127
130
  if (checkoutSession.invoice_id) {
128
131
  await InvoiceItem.destroy({ where: { invoice_id: checkoutSession.invoice_id } });
@@ -143,6 +146,7 @@ events.on('checkout.session.expired', async (checkoutSession: CheckoutSession) =
143
146
  });
144
147
  }
145
148
  }
149
+
146
150
  if (checkoutSession.setup_intent_id) {
147
151
  await SetupIntent.destroy({ where: { id: checkoutSession.setup_intent_id } });
148
152
  logger.info('SetupIntent for checkout session deleted on expire', {
@@ -150,14 +154,71 @@ events.on('checkout.session.expired', async (checkoutSession: CheckoutSession) =
150
154
  setupIntent: checkoutSession.setup_intent_id,
151
155
  });
152
156
  }
157
+
153
158
  if (checkoutSession.payment_intent_id && checkoutSession.payment_status !== 'paid') {
159
+ const paymentIntent = await PaymentIntent.findByPk(checkoutSession.payment_intent_id);
160
+ const stripePaymentId = paymentIntent?.payment_details?.stripe?.payment_intent_id;
161
+ if (paymentIntent && stripePaymentId) {
162
+ const method = await PaymentMethod.findByPk(paymentIntent.payment_method_id);
163
+ if (method?.type === 'stripe') {
164
+ const client = method.getStripeClient();
165
+ try {
166
+ await client.paymentIntents.cancel(stripePaymentId, { cancellation_reason: 'abandoned' });
167
+ logger.info('Stripe PaymentIntent for checkout session canceled on expire', {
168
+ checkoutSession: checkoutSession.id,
169
+ paymentIntent: checkoutSession.payment_intent_id,
170
+ stripePayment: stripePaymentId,
171
+ });
172
+ } catch (err) {
173
+ logger.error('Stripe PaymentIntent for checkout session cancel failed on expire', {
174
+ checkoutSession: checkoutSession.id,
175
+ paymentIntent: checkoutSession.payment_intent_id,
176
+ stripePayment: stripePaymentId,
177
+ error: err,
178
+ });
179
+ }
180
+ }
181
+ }
182
+
154
183
  await PaymentIntent.destroy({ where: { id: checkoutSession.payment_intent_id } });
155
184
  logger.info('PaymentIntent for checkout session deleted on expire', {
156
185
  checkoutSession: checkoutSession.id,
157
186
  paymentIntent: checkoutSession.payment_intent_id,
158
187
  });
159
188
  }
189
+
160
190
  if (checkoutSession.subscription_id) {
191
+ const subscription = await Subscription.findByPk(checkoutSession.subscription_id);
192
+ const stripeSubscriptionId = subscription?.payment_details?.stripe?.subscription_id;
193
+ if (subscription && stripeSubscriptionId) {
194
+ const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
195
+ if (method?.type === 'stripe') {
196
+ const client = method.getStripeClient();
197
+ try {
198
+ await client.subscriptions.cancel(stripeSubscriptionId, {
199
+ prorate: false,
200
+ invoice_now: false,
201
+ cancellation_details: {
202
+ comment: 'checkout_session_expired',
203
+ feedback: 'unused',
204
+ },
205
+ });
206
+ logger.info('Stripe Subscription for checkout session canceled on expire', {
207
+ checkoutSession: checkoutSession.id,
208
+ subscription: checkoutSession.subscription_id,
209
+ stripeSubscription: stripeSubscriptionId,
210
+ });
211
+ } catch (err) {
212
+ logger.error('Stripe Subscription for checkout session cancel failed on expire', {
213
+ checkoutSession: checkoutSession.id,
214
+ subscription: checkoutSession.subscription_id,
215
+ stripeSubscription: stripeSubscriptionId,
216
+ error: err,
217
+ });
218
+ }
219
+ }
220
+ }
221
+
161
222
  await SubscriptionItem.destroy({ where: { subscription_id: checkoutSession.subscription_id } });
162
223
  await Subscription.destroy({ where: { id: checkoutSession.subscription_id } });
163
224
  logger.info('Subscription and SubscriptionItem for checkout session deleted on expire', {
@@ -185,4 +246,6 @@ events.on('checkout.session.expired', async (checkoutSession: CheckoutSession) =
185
246
  }
186
247
  }
187
248
  }
249
+
250
+ logger.info('Done cleanup on checkoutSession expired', { checkoutSession: checkoutSession.id });
188
251
  });
@@ -133,7 +133,7 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
133
133
  }
134
134
 
135
135
  if (!raw.expires_at) {
136
- raw.expires_at = dayjs().unix() + CHECKOUT_SESSION_TTL; // 24 hours after creation
136
+ raw.expires_at = dayjs().unix() + CHECKOUT_SESSION_TTL;
137
137
  }
138
138
 
139
139
  if (raw.nft_mint_settings?.enabled) {
@@ -803,8 +803,8 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
803
803
  trialEnds
804
804
  );
805
805
  if (stripeSubscription && subscription?.payment_details?.stripe?.subscription_id === stripeSubscription.id) {
806
- if (stripeSubscription.status === 'active' && subscription.status === 'incomplete') {
807
- await handleStripeSubscriptionSucceed(subscription);
806
+ if (['active', 'trialing'].includes(stripeSubscription.status) && subscription.status === 'incomplete') {
807
+ await handleStripeSubscriptionSucceed(subscription, stripeSubscription.status);
808
808
  }
809
809
  }
810
810
  stripeContext = {
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.195
17
+ version: 1.13.196
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.195",
3
+ "version": "1.13.196",
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.113",
51
51
  "@arcblock/ux": "^2.9.57",
52
52
  "@blocklet/logger": "1.16.24",
53
- "@blocklet/payment-react": "1.13.195",
53
+ "@blocklet/payment-react": "1.13.196",
54
54
  "@blocklet/sdk": "1.16.24",
55
55
  "@blocklet/ui-react": "^2.9.57",
56
56
  "@blocklet/uploader": "^0.0.74",
@@ -110,7 +110,7 @@
110
110
  "devDependencies": {
111
111
  "@abtnode/types": "1.16.24",
112
112
  "@arcblock/eslint-config-ts": "^0.2.4",
113
- "@blocklet/payment-types": "1.13.195",
113
+ "@blocklet/payment-types": "1.13.196",
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": "e8615414419a8dd76fb46b3dcdbb4ff53a8f049c"
152
+ "gitHead": "04bc825071dc91673a67b995f446ad7e4300e4bf"
153
153
  }
@@ -57,6 +57,9 @@ const getListKey = (props: ListProps) => {
57
57
  if (props.invoice_id) {
58
58
  return `invoice-refunds-${props.invoice_id}`;
59
59
  }
60
+ if (props.subscription_id) {
61
+ return `subscription-refunds-${props.subscription_id}`;
62
+ }
60
63
 
61
64
  return 'refunds';
62
65
  };
@@ -75,7 +78,7 @@ export default function RefundList({ customer_id, invoice_id, subscription_id, f
75
78
  const { t } = useLocaleContext();
76
79
  const navigate = useNavigate();
77
80
  const { startTransition } = useTransitionContext();
78
- const listKey = getListKey({ customer_id, invoice_id });
81
+ const listKey = getListKey({ customer_id, invoice_id, subscription_id });
79
82
 
80
83
  const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
81
84
  defaultValue: {