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.
- package/api/src/integrations/stripe/handlers/subscription.ts +45 -9
- package/api/src/integrations/stripe/resource.ts +1 -1
- package/api/src/libs/env.ts +1 -1
- package/api/src/libs/util.ts +1 -1
- package/api/src/queues/checkout-session.ts +63 -0
- package/api/src/routes/checkout-sessions.ts +3 -3
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/components/refund/list.tsx +4 -1
|
@@ -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
|
-
|
|
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 (
|
|
40
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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.
|
|
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
|
}
|
package/api/src/libs/env.ts
CHANGED
|
@@ -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
|
|
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 执行一次
|
package/api/src/libs/util.ts
CHANGED
|
@@ -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 =
|
|
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;
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
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.
|
|
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.
|
|
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": "
|
|
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: {
|