payment-kit 1.21.16 → 1.22.0
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/index.ts +3 -1
- package/api/src/integrations/blocklet/user.ts +2 -2
- package/api/src/integrations/ethereum/token.ts +4 -5
- package/api/src/integrations/stripe/handlers/invoice.ts +31 -26
- package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -1
- package/api/src/integrations/stripe/handlers/setup-intent.ts +231 -0
- package/api/src/integrations/stripe/handlers/subscription.ts +31 -9
- package/api/src/integrations/stripe/resource.ts +30 -1
- package/api/src/integrations/stripe/setup.ts +1 -1
- package/api/src/libs/auth.ts +7 -6
- package/api/src/libs/env.ts +1 -1
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +1 -0
- package/api/src/libs/notification/template/subscription-will-renew.ts +1 -1
- package/api/src/libs/payment.ts +11 -6
- package/api/src/libs/refund.ts +1 -1
- package/api/src/libs/remote-signer.ts +93 -0
- package/api/src/libs/security.ts +1 -1
- package/api/src/libs/subscription.ts +4 -7
- package/api/src/libs/util.ts +18 -1
- package/api/src/libs/vendor-util/adapters/didnames-adapter.ts +17 -9
- package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +11 -6
- package/api/src/queues/payment.ts +2 -2
- package/api/src/queues/payout.ts +1 -1
- package/api/src/queues/refund.ts +2 -2
- package/api/src/queues/subscription.ts +1 -1
- package/api/src/queues/usage-record.ts +1 -1
- package/api/src/queues/vendors/status-check.ts +1 -1
- package/api/src/queues/webhook.ts +1 -1
- package/api/src/routes/auto-recharge-configs.ts +1 -1
- package/api/src/routes/checkout-sessions.ts +4 -6
- package/api/src/routes/connect/change-payer.ts +148 -0
- package/api/src/routes/connect/collect-batch.ts +1 -1
- package/api/src/routes/connect/collect.ts +1 -1
- package/api/src/routes/connect/pay.ts +1 -1
- package/api/src/routes/connect/recharge-account.ts +1 -1
- package/api/src/routes/connect/recharge.ts +1 -1
- package/api/src/routes/connect/shared.ts +62 -23
- package/api/src/routes/customers.ts +1 -1
- package/api/src/routes/integrations/stripe.ts +1 -1
- package/api/src/routes/invoices.ts +141 -2
- package/api/src/routes/meter-events.ts +9 -12
- package/api/src/routes/payment-currencies.ts +1 -1
- package/api/src/routes/payment-intents.ts +2 -2
- package/api/src/routes/payment-links.ts +2 -1
- package/api/src/routes/payouts.ts +1 -1
- package/api/src/routes/products.ts +1 -0
- package/api/src/routes/subscriptions.ts +130 -3
- package/api/src/store/models/types.ts +1 -1
- package/api/tests/setup.ts +11 -0
- package/api/third.d.ts +0 -2
- package/blocklet.yml +1 -1
- package/jest.config.js +2 -2
- package/package.json +26 -26
- package/src/components/invoice/table.tsx +2 -2
- package/src/components/invoice-pdf/template.tsx +30 -0
- package/src/components/subscription/payment-method-info.tsx +222 -0
- package/src/global.css +4 -0
- package/src/libs/util.ts +1 -1
- package/src/locales/en.tsx +13 -0
- package/src/locales/zh.tsx +13 -0
- package/src/pages/admin/billing/invoices/detail.tsx +5 -3
- package/src/pages/admin/billing/subscriptions/detail.tsx +16 -0
- package/src/pages/admin/overview.tsx +14 -14
- package/src/pages/customer/invoice/detail.tsx +59 -17
- package/src/pages/customer/subscription/detail.tsx +21 -2
package/api/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import 'express-async-errors';
|
|
|
2
2
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
|
-
import fallback from '@blocklet/sdk/lib/middlewares/fallback';
|
|
5
|
+
import { fallback } from '@blocklet/sdk/lib/middlewares/fallback';
|
|
6
6
|
import cookieParser from 'cookie-parser';
|
|
7
7
|
import cors from 'cors';
|
|
8
8
|
import dotenv from 'dotenv-flow';
|
|
@@ -53,6 +53,7 @@ import rechargeHandlers from './routes/connect/recharge';
|
|
|
53
53
|
import rechargeAccountHandlers from './routes/connect/recharge-account';
|
|
54
54
|
import setupHandlers from './routes/connect/setup';
|
|
55
55
|
import subscribeHandlers from './routes/connect/subscribe';
|
|
56
|
+
import changePayerHandlers from './routes/connect/change-payer';
|
|
56
57
|
import { initialize } from './store/models';
|
|
57
58
|
import { sequelize } from './store/sequelize';
|
|
58
59
|
|
|
@@ -92,6 +93,7 @@ handlers.attach(Object.assign({ app: router }, delegationHandlers));
|
|
|
92
93
|
handlers.attach(Object.assign({ app: router }, overdraftProtectionHandlers));
|
|
93
94
|
handlers.attach(Object.assign({ app: router }, reStakeHandlers));
|
|
94
95
|
handlers.attach(Object.assign({ app: router }, autoRechargeAuthorizationHandlers));
|
|
96
|
+
handlers.attach(Object.assign({ app: router }, changePayerHandlers));
|
|
95
97
|
router.use('/api', routes);
|
|
96
98
|
|
|
97
99
|
const isProduction = process.env.BLOCKLET_MODE === 'production';
|
|
@@ -32,6 +32,6 @@ const handleUserUpdate = async ({ user }: { user: any }) => {
|
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
export function initUserHandler() {
|
|
36
|
-
notification.on('user.updated', handleUserUpdate);
|
|
35
|
+
export async function initUserHandler() {
|
|
36
|
+
await notification.on('user.updated', handleUserUpdate);
|
|
37
37
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { JsonRpcProvider, TransactionReceipt, ethers } from 'ethers';
|
|
2
2
|
|
|
3
3
|
import { ethWallet } from '../../libs/auth';
|
|
4
|
+
import { RemoteSigner } from '../../libs/remote-signer';
|
|
4
5
|
import { getApproveFunction } from './contract';
|
|
5
6
|
import erc20Abi from './erc20-abi.json';
|
|
6
7
|
|
|
@@ -92,12 +93,11 @@ export async function transferErc20FromUser(
|
|
|
92
93
|
user: string,
|
|
93
94
|
amount: string
|
|
94
95
|
): Promise<TransactionReceipt> {
|
|
95
|
-
const
|
|
96
|
-
const signer = wallet.connect(provider);
|
|
96
|
+
const signer = new RemoteSigner(ethWallet, provider);
|
|
97
97
|
const contract = new ethers.Contract(contractAddress, erc20Abi, signer);
|
|
98
98
|
|
|
99
99
|
// @ts-ignore
|
|
100
|
-
const res = await contract.transferFrom(user,
|
|
100
|
+
const res = await contract.transferFrom(user, ethWallet.address, amount);
|
|
101
101
|
|
|
102
102
|
// Wait for the transaction to be mined
|
|
103
103
|
const receipt = await res.wait();
|
|
@@ -111,8 +111,7 @@ export async function sendErc20ToUser(
|
|
|
111
111
|
user: string,
|
|
112
112
|
amount: string
|
|
113
113
|
): Promise<TransactionReceipt> {
|
|
114
|
-
const
|
|
115
|
-
const signer = wallet.connect(provider);
|
|
114
|
+
const signer = new RemoteSigner(ethWallet, provider);
|
|
116
115
|
const contract = new ethers.Contract(contractAddress, erc20Abi, signer);
|
|
117
116
|
|
|
118
117
|
// @ts-ignore
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import env from '@blocklet/sdk/lib/env';
|
|
1
|
+
import { env } from '@blocklet/sdk/lib/env';
|
|
2
2
|
import merge from 'lodash/merge';
|
|
3
3
|
import pick from 'lodash/pick';
|
|
4
4
|
import pWaitFor from 'p-wait-for';
|
|
@@ -91,7 +91,7 @@ export function getStripeInvoicePeriod(invoice: any) {
|
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
export async function syncStripeInvoice(invoice: Invoice) {
|
|
94
|
+
export async function syncStripeInvoice(invoice: Invoice, sync = true) {
|
|
95
95
|
if (!invoice.metadata?.stripe_id) {
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
@@ -103,6 +103,28 @@ export async function syncStripeInvoice(invoice: Invoice) {
|
|
|
103
103
|
|
|
104
104
|
const client = await method.getStripeClient();
|
|
105
105
|
const stripeInvoice = await client.invoices.retrieve(invoice.metadata.stripe_id);
|
|
106
|
+
const updates = [
|
|
107
|
+
'amount_due',
|
|
108
|
+
'amount_paid',
|
|
109
|
+
'amount_remaining',
|
|
110
|
+
'last_finalization_error',
|
|
111
|
+
'paid_out_of_band',
|
|
112
|
+
'paid',
|
|
113
|
+
'status',
|
|
114
|
+
'status_transitions',
|
|
115
|
+
'subtotal_excluding_tax',
|
|
116
|
+
'subtotal',
|
|
117
|
+
'tax',
|
|
118
|
+
'total_discount_amounts',
|
|
119
|
+
'total',
|
|
120
|
+
];
|
|
121
|
+
if (!sync && stripeInvoice.status !== 'paid' && invoice.status === 'uncollectible') {
|
|
122
|
+
// remove status from updates
|
|
123
|
+
const statusIndex = updates.indexOf('status');
|
|
124
|
+
if (statusIndex !== -1) {
|
|
125
|
+
updates.splice(statusIndex, 1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
106
128
|
if (stripeInvoice) {
|
|
107
129
|
const processDiscounts = await processInvoiceDiscounts(
|
|
108
130
|
stripeInvoice,
|
|
@@ -111,27 +133,9 @@ export async function syncStripeInvoice(invoice: Invoice) {
|
|
|
111
133
|
);
|
|
112
134
|
await invoice.update(
|
|
113
135
|
// @ts-ignore
|
|
114
|
-
merge(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
'amount_paid',
|
|
118
|
-
'amount_remaining',
|
|
119
|
-
'last_finalization_error',
|
|
120
|
-
'paid_out_of_band',
|
|
121
|
-
'paid',
|
|
122
|
-
'status_transitions',
|
|
123
|
-
'status',
|
|
124
|
-
'subtotal_excluding_tax',
|
|
125
|
-
'subtotal',
|
|
126
|
-
'tax',
|
|
127
|
-
'total_discount_amounts',
|
|
128
|
-
'total',
|
|
129
|
-
]),
|
|
130
|
-
getStripeInvoicePeriod(stripeInvoice),
|
|
131
|
-
{
|
|
132
|
-
total_discount_amounts: processDiscounts,
|
|
133
|
-
}
|
|
134
|
-
)
|
|
136
|
+
merge(pick(stripeInvoice, updates), getStripeInvoicePeriod(stripeInvoice), {
|
|
137
|
+
total_discount_amounts: processDiscounts,
|
|
138
|
+
})
|
|
135
139
|
);
|
|
136
140
|
logger.info('stripe invoice synced', { locale: invoice.id, remote: stripeInvoice.id });
|
|
137
141
|
const failedStatuses = ['uncollectible', 'finalization_failed', 'payment_failed'];
|
|
@@ -416,7 +420,8 @@ export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe)
|
|
|
416
420
|
}
|
|
417
421
|
|
|
418
422
|
if (event.type === 'invoice.finalized') {
|
|
419
|
-
|
|
423
|
+
// invoice from draft to open
|
|
424
|
+
await invoice.update({ status: 'open', status_transitions: event.data.object.status_transitions });
|
|
420
425
|
logger.info('invoice finalized on stripe event', { locale: invoice.id });
|
|
421
426
|
return;
|
|
422
427
|
}
|
|
@@ -442,14 +447,14 @@ export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe)
|
|
|
442
447
|
|
|
443
448
|
if (event.type === 'invoice.finalization_failed') {
|
|
444
449
|
await invoice.update({
|
|
445
|
-
status: '
|
|
450
|
+
status: 'uncollectible',
|
|
446
451
|
last_finalization_error: event.data.object.last_finalization_error,
|
|
447
452
|
});
|
|
448
453
|
logger.info('invoice finalization failed on stripe event', { locale: invoice.id });
|
|
449
454
|
}
|
|
450
455
|
|
|
451
456
|
if (event.type === 'invoice.payment_failed') {
|
|
452
|
-
await invoice.update({ status: '
|
|
457
|
+
await invoice.update({ status: 'uncollectible' });
|
|
453
458
|
logger.info('invoice payment failed on stripe event', { locale: invoice.id });
|
|
454
459
|
}
|
|
455
460
|
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type Stripe from 'stripe';
|
|
2
|
+
import { Op } from 'sequelize';
|
|
2
3
|
|
|
3
4
|
import logger from '../../../libs/logger';
|
|
4
5
|
import {
|
|
5
6
|
AutoRechargeConfig,
|
|
6
7
|
CheckoutSession,
|
|
8
|
+
Customer,
|
|
9
|
+
Invoice,
|
|
7
10
|
Lock,
|
|
11
|
+
PaymentCurrency,
|
|
12
|
+
PaymentMethod,
|
|
8
13
|
SetupIntent,
|
|
9
14
|
Subscription,
|
|
10
15
|
TEventExpanded,
|
|
@@ -150,6 +155,230 @@ async function handleAutoRechargeOnSetupSucceeded(event: TEventExpanded, stripeI
|
|
|
150
155
|
}
|
|
151
156
|
}
|
|
152
157
|
|
|
158
|
+
async function handleUpdateStripePaymentMethodOnSetupSucceeded(event: TEventExpanded, stripeIntentId: string) {
|
|
159
|
+
const { metadata } = event.data.object;
|
|
160
|
+
|
|
161
|
+
if (metadata?.action !== 'update_payment_method' || !metadata?.subscription_id) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (event.type !== 'setup_intent.succeeded') {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const subscription = await Subscription.findByPk(metadata.subscription_id);
|
|
170
|
+
if (!subscription) {
|
|
171
|
+
logger.warn('subscription not found for update payment method', {
|
|
172
|
+
id: event.id,
|
|
173
|
+
stripeIntentId,
|
|
174
|
+
subscriptionId: metadata.subscription_id,
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
180
|
+
if (!paymentMethod || paymentMethod.type !== 'stripe') {
|
|
181
|
+
logger.warn('stripe payment method not found for subscription', {
|
|
182
|
+
id: event.id,
|
|
183
|
+
stripeIntentId,
|
|
184
|
+
subscriptionId: subscription.id,
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id;
|
|
190
|
+
if (!stripeSubscriptionId) {
|
|
191
|
+
logger.warn('stripe subscription id not found', {
|
|
192
|
+
id: event.id,
|
|
193
|
+
stripeIntentId,
|
|
194
|
+
subscriptionId: subscription.id,
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const client = paymentMethod.getStripeClient();
|
|
200
|
+
const stripePaymentMethodId = event.data.object.payment_method as string;
|
|
201
|
+
|
|
202
|
+
await client.subscriptions.update(stripeSubscriptionId, {
|
|
203
|
+
default_payment_method: stripePaymentMethodId,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
logger.info('stripe payment method updated via webhook', {
|
|
207
|
+
subscriptionId: subscription.id,
|
|
208
|
+
stripeIntentId,
|
|
209
|
+
paymentMethod: stripePaymentMethodId,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function handleBatchOverduePaymentOnSetupSucceeded(event: TEventExpanded, stripeIntentId: string) {
|
|
214
|
+
const { metadata } = event.data.object;
|
|
215
|
+
|
|
216
|
+
if (metadata?.action !== 'pay_overdue_batch' || !metadata?.currency_id) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (event.type !== 'setup_intent.succeeded') {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const paymentCurrency = await PaymentCurrency.findByPk(metadata.currency_id);
|
|
225
|
+
if (!paymentCurrency) {
|
|
226
|
+
logger.warn('payment currency not found for batch overdue payment', {
|
|
227
|
+
id: event.id,
|
|
228
|
+
stripeIntentId,
|
|
229
|
+
currencyId: metadata.currency_id,
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
235
|
+
if (!paymentMethod || paymentMethod.type !== 'stripe') {
|
|
236
|
+
logger.warn('stripe payment method not found or invalid type', {
|
|
237
|
+
id: event.id,
|
|
238
|
+
stripeIntentId,
|
|
239
|
+
currencyId: metadata.currency_id,
|
|
240
|
+
paymentMethodType: paymentMethod?.type,
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let invoices: Invoice[];
|
|
246
|
+
|
|
247
|
+
if (metadata.invoices) {
|
|
248
|
+
let invoiceIds: string[];
|
|
249
|
+
try {
|
|
250
|
+
invoiceIds = JSON.parse(metadata.invoices);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
logger.error('failed to parse invoices from setup intent metadata', {
|
|
253
|
+
id: event.id,
|
|
254
|
+
stripeIntentId,
|
|
255
|
+
metadata,
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
invoices = await Invoice.findAll({
|
|
261
|
+
where: {
|
|
262
|
+
id: { [Op.in]: invoiceIds },
|
|
263
|
+
currency_id: metadata.currency_id,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const subscriptionIds = [...new Set(invoices.map((inv) => inv.subscription_id).filter(Boolean) as string[])];
|
|
268
|
+
|
|
269
|
+
if (subscriptionIds.length > 0) {
|
|
270
|
+
const additionalInvoices = await Invoice.findAll({
|
|
271
|
+
where: {
|
|
272
|
+
subscription_id: { [Op.in]: subscriptionIds },
|
|
273
|
+
currency_id: metadata.currency_id,
|
|
274
|
+
status: { [Op.in]: ['open', 'uncollectible'] },
|
|
275
|
+
id: { [Op.notIn]: invoiceIds },
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
invoices = [...invoices, ...additionalInvoices];
|
|
280
|
+
|
|
281
|
+
logger.info('found additional overdue invoices for subscriptions', {
|
|
282
|
+
stripeIntentId,
|
|
283
|
+
subscriptionIds,
|
|
284
|
+
originalCount: invoiceIds.length,
|
|
285
|
+
additionalCount: additionalInvoices.length,
|
|
286
|
+
totalCount: invoices.length,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
} else if (metadata.subscription_id) {
|
|
290
|
+
invoices = await Invoice.findAll({
|
|
291
|
+
where: {
|
|
292
|
+
subscription_id: metadata.subscription_id,
|
|
293
|
+
currency_id: metadata.currency_id,
|
|
294
|
+
status: { [Op.in]: ['open', 'uncollectible'] },
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
} else if (metadata.customer_id) {
|
|
298
|
+
const customer = await Customer.findByPkOrDid(metadata.customer_id);
|
|
299
|
+
if (!customer) {
|
|
300
|
+
logger.warn('customer not found for batch overdue payment', {
|
|
301
|
+
id: event.id,
|
|
302
|
+
stripeIntentId,
|
|
303
|
+
customerId: metadata.customer_id,
|
|
304
|
+
});
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
invoices = await Invoice.findAll({
|
|
309
|
+
where: {
|
|
310
|
+
customer_id: customer.id,
|
|
311
|
+
currency_id: metadata.currency_id,
|
|
312
|
+
status: { [Op.in]: ['open', 'uncollectible'] },
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
} else {
|
|
316
|
+
logger.error('invalid metadata for batch overdue payment, must provide invoices, subscription_id or customer_id', {
|
|
317
|
+
id: event.id,
|
|
318
|
+
stripeIntentId,
|
|
319
|
+
metadata,
|
|
320
|
+
});
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (invoices.length === 0) {
|
|
325
|
+
logger.warn('no invoices found for batch overdue payment', {
|
|
326
|
+
id: event.id,
|
|
327
|
+
stripeIntentId,
|
|
328
|
+
metadata,
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const validInvoices = invoices.filter((inv) => inv.metadata?.stripe_id);
|
|
334
|
+
if (validInvoices.length === 0) {
|
|
335
|
+
logger.warn('no valid stripe invoices found for batch overdue payment', {
|
|
336
|
+
id: event.id,
|
|
337
|
+
stripeIntentId,
|
|
338
|
+
totalInvoices: invoices.length,
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const client = paymentMethod.getStripeClient();
|
|
344
|
+
const stripePaymentMethodId = event.data.object.payment_method as string;
|
|
345
|
+
|
|
346
|
+
const payResults = await Promise.all(
|
|
347
|
+
validInvoices.map(async (invoice) => {
|
|
348
|
+
try {
|
|
349
|
+
await client.invoices.pay(invoice.metadata!.stripe_id, {
|
|
350
|
+
payment_method: stripePaymentMethodId,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
logger.info('stripe invoice payment initiated via webhook', {
|
|
354
|
+
invoiceId: invoice.id,
|
|
355
|
+
stripeInvoiceId: invoice.metadata!.stripe_id,
|
|
356
|
+
stripeIntentId,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
return { invoiceId: invoice.id, success: true };
|
|
360
|
+
} catch (err) {
|
|
361
|
+
logger.error('failed to pay stripe invoice via webhook', {
|
|
362
|
+
invoiceId: invoice.id,
|
|
363
|
+
stripeInvoiceId: invoice.metadata!.stripe_id,
|
|
364
|
+
stripeIntentId,
|
|
365
|
+
error: err,
|
|
366
|
+
});
|
|
367
|
+
return { invoiceId: invoice.id, success: false, error: err.message };
|
|
368
|
+
}
|
|
369
|
+
})
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
logger.info('batch overdue payment completed via webhook', {
|
|
373
|
+
stripeIntentId,
|
|
374
|
+
currencyId: metadata.currency_id,
|
|
375
|
+
totalInvoices: invoices.length,
|
|
376
|
+
validInvoices: validInvoices.length,
|
|
377
|
+
succeeded: payResults.filter((r) => r.success).length,
|
|
378
|
+
failed: payResults.filter((r) => !r.success).length,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
153
382
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
154
383
|
export async function handleSetupIntentEvent(event: TEventExpanded, _: Stripe) {
|
|
155
384
|
const stripeIntentId = event.data.object.id;
|
|
@@ -159,4 +388,6 @@ export async function handleSetupIntentEvent(event: TEventExpanded, _: Stripe) {
|
|
|
159
388
|
await handleSetupIntentOnSetupSucceeded(event, stripeIntentId);
|
|
160
389
|
await handleCheckoutSessionOnSetupSucceeded(event, stripeIntentId);
|
|
161
390
|
await handleAutoRechargeOnSetupSucceeded(event, stripeIntentId);
|
|
391
|
+
await handleUpdateStripePaymentMethodOnSetupSucceeded(event, stripeIntentId);
|
|
392
|
+
await handleBatchOverduePaymentOnSetupSucceeded(event, stripeIntentId);
|
|
162
393
|
}
|
|
@@ -197,11 +197,25 @@ export async function handleSubscriptionOnPaymentFailure(
|
|
|
197
197
|
eventType: string,
|
|
198
198
|
client: Stripe
|
|
199
199
|
) {
|
|
200
|
-
if (!subscription ||
|
|
201
|
-
logger.warn('Subscription is
|
|
200
|
+
if (!subscription || subscription.isImmutable()) {
|
|
201
|
+
logger.warn('Subscription is immutable or not found', { subscription: subscription.id });
|
|
202
202
|
return;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
|
|
206
|
+
if (paymentMethod && paymentMethod.type !== 'stripe') {
|
|
207
|
+
logger.info('Subscription payment method is not stripe', {
|
|
208
|
+
subscription: subscription.id,
|
|
209
|
+
paymentMethod: paymentMethod?.type,
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
logger.info('start handle stripe subscription on payment failure', {
|
|
215
|
+
subscription: subscription.id,
|
|
216
|
+
eventType,
|
|
217
|
+
});
|
|
218
|
+
|
|
205
219
|
const now = dayjs().unix();
|
|
206
220
|
const { interval } = subscription.pending_invoice_item_interval;
|
|
207
221
|
const dueUnit = getDueUnit(interval);
|
|
@@ -256,13 +270,25 @@ export async function handleSubscriptionOnPaymentFailure(
|
|
|
256
270
|
// sync to stripe
|
|
257
271
|
if (subscription.payment_details?.stripe?.subscription_id && client) {
|
|
258
272
|
try {
|
|
259
|
-
|
|
260
|
-
|
|
273
|
+
if (paymentMethod && paymentMethod.type === 'stripe') {
|
|
274
|
+
if (cancelSubscription) {
|
|
275
|
+
await client.subscriptions.cancel(subscription.payment_details.stripe.subscription_id, {
|
|
276
|
+
cancellation_details: {
|
|
277
|
+
comment: 'exceed_current_period',
|
|
278
|
+
feedback: 'other',
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
logger.info('subscription in Stripe has canceled after payment failed', {
|
|
282
|
+
subscription: subscription.id,
|
|
283
|
+
stripeSubscription: subscription.payment_details.stripe.subscription_id,
|
|
284
|
+
eventType,
|
|
285
|
+
});
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
261
288
|
const stripeUpdates: any = {
|
|
262
289
|
cancellation_details: {
|
|
263
290
|
comment: 'past_due',
|
|
264
291
|
feedback: 'other',
|
|
265
|
-
reason: 'payment_failed',
|
|
266
292
|
},
|
|
267
293
|
};
|
|
268
294
|
|
|
@@ -271,10 +297,6 @@ export async function handleSubscriptionOnPaymentFailure(
|
|
|
271
297
|
} else if (cancelUpdates.cancel_at_period_end) {
|
|
272
298
|
stripeUpdates.cancel_at_period_end = true;
|
|
273
299
|
}
|
|
274
|
-
if (cancelSubscription) {
|
|
275
|
-
stripeUpdates.cancel_at = now;
|
|
276
|
-
stripeUpdates.cancel_at_period_end = false;
|
|
277
|
-
}
|
|
278
300
|
|
|
279
301
|
await client.subscriptions.update(subscription.payment_details.stripe.subscription_id, stripeUpdates);
|
|
280
302
|
logger.info(`[${eventType}] Updated subscription in Stripe after payment failed`, {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-continue */
|
|
2
2
|
/* eslint-disable no-await-in-loop */
|
|
3
|
-
import env from '@blocklet/sdk/lib/env';
|
|
3
|
+
import { env } from '@blocklet/sdk/lib/env';
|
|
4
4
|
import merge from 'lodash/merge';
|
|
5
5
|
import omit from 'lodash/omit';
|
|
6
6
|
import pick from 'lodash/pick';
|
|
@@ -855,6 +855,35 @@ export async function ensureStripeSetupIntentForAutoRecharge(
|
|
|
855
855
|
return setupIntent;
|
|
856
856
|
}
|
|
857
857
|
|
|
858
|
+
export async function ensureStripeSetupIntentForInvoicePayment(
|
|
859
|
+
customer: Customer,
|
|
860
|
+
method: PaymentMethod,
|
|
861
|
+
metadata: Record<string, string>
|
|
862
|
+
) {
|
|
863
|
+
const client = method.getStripeClient();
|
|
864
|
+
const stripeCustomer = await ensureStripeCustomer(customer, method);
|
|
865
|
+
|
|
866
|
+
const setupIntent = await client.setupIntents.create({
|
|
867
|
+
customer: stripeCustomer.id,
|
|
868
|
+
payment_method_types: ['card'],
|
|
869
|
+
usage: 'off_session',
|
|
870
|
+
metadata: {
|
|
871
|
+
appPid: env.appPid,
|
|
872
|
+
customer_id: customer.id,
|
|
873
|
+
action: 'pay_overdue_batch',
|
|
874
|
+
...metadata,
|
|
875
|
+
},
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
logger.info('stripe setup intent created for invoice payment', {
|
|
879
|
+
customerId: customer.id,
|
|
880
|
+
setupIntentId: setupIntent.id,
|
|
881
|
+
metadata,
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
return setupIntent;
|
|
885
|
+
}
|
|
886
|
+
|
|
858
887
|
export async function updateAutoRechargeConfigPaymentMethod(params: {
|
|
859
888
|
stripePaymentMethodId: string;
|
|
860
889
|
autoRechargeConfig: AutoRechargeConfig;
|
package/api/src/libs/auth.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
|
|
3
3
|
import AuthStorage from '@arcblock/did-connect-storage-nedb';
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
import { BlockletService } from '@blocklet/sdk/service/auth';
|
|
6
|
+
import { getWallet } from '@blocklet/sdk/lib/wallet';
|
|
7
|
+
import { WalletAuthenticator } from '@blocklet/sdk/lib/wallet-authenticator';
|
|
8
|
+
import { WalletHandlers } from '@blocklet/sdk/lib/wallet-handler';
|
|
8
9
|
import type { Request } from 'express';
|
|
9
10
|
import type { LiteralUnion } from 'type-fest';
|
|
10
11
|
import type { WalletObject } from '@ocap/wallet';
|
|
@@ -15,7 +16,7 @@ import logger from './logger';
|
|
|
15
16
|
export const wallet: WalletObject = getWallet();
|
|
16
17
|
export const ethWallet: WalletObject = getWallet('ethereum');
|
|
17
18
|
export const authenticator = new WalletAuthenticator();
|
|
18
|
-
export const handlers = new
|
|
19
|
+
export const handlers = new WalletHandlers({
|
|
19
20
|
authenticator,
|
|
20
21
|
tokenStorage: new AuthStorage({
|
|
21
22
|
dbPath: path.join(env.dataDir, 'auth.db'),
|
|
@@ -24,7 +25,7 @@ export const handlers = new WalletHandler({
|
|
|
24
25
|
}),
|
|
25
26
|
});
|
|
26
27
|
|
|
27
|
-
export const blocklet = new
|
|
28
|
+
export const blocklet = new BlockletService();
|
|
28
29
|
|
|
29
30
|
export async function getVaultAddress() {
|
|
30
31
|
try {
|
package/api/src/libs/env.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import env from '@blocklet/sdk/lib/env';
|
|
1
|
+
import { env } from '@blocklet/sdk/lib/env';
|
|
2
2
|
|
|
3
3
|
export const paymentStatCronTime: string = '0 1 0 * * *'; // 默认每天一次,计算前一天的
|
|
4
4
|
export const subscriptionCronTime: string = process.env.SUBSCRIPTION_CRON_TIME || '0 */30 * * * *'; // 默认每 30 min 行一次
|
|
@@ -92,6 +92,7 @@ export class SubscriptionTrialWillEndEmailTemplate extends BaseSubscriptionEmail
|
|
|
92
92
|
const paymentAddress = subscription.payment_details?.[paymentMethod.type]?.payer ?? undefined;
|
|
93
93
|
const balance = await getTokenByAddress(paymentAddress, paymentMethod, paymentCurrency);
|
|
94
94
|
|
|
95
|
+
// @ts-ignore
|
|
95
96
|
paymentDetail.balance = +fromUnitToToken(balance, paymentCurrency.decimal);
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -100,7 +100,7 @@ export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTem
|
|
|
100
100
|
const paymentAddress = getSubscriptionPaymentAddress(subscription, paymentInfoResult.paymentMethod!.type);
|
|
101
101
|
const balance = await getTokenByAddress(paymentAddress, paymentInfoResult.paymentMethod!, paymentCurrency);
|
|
102
102
|
|
|
103
|
-
paymentDetail.balanceFormatted = fromUnitToToken(balance, paymentCurrency.decimal);
|
|
103
|
+
paymentDetail.balanceFormatted = fromUnitToToken(balance || '0', paymentCurrency.decimal);
|
|
104
104
|
paymentDetail.balance = +paymentDetail.balanceFormatted;
|
|
105
105
|
}
|
|
106
106
|
|
package/api/src/libs/payment.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/indent */
|
|
2
2
|
import { isEthereumDid } from '@arcblock/did';
|
|
3
3
|
import { toDelegateAddress } from '@arcblock/did-util';
|
|
4
|
-
import { sign } from '@arcblock/jwt';
|
|
5
4
|
import { getWalletDid } from '@blocklet/sdk/lib/did';
|
|
6
5
|
import type { DelegateState, TokenLimit } from '@ocap/client';
|
|
7
6
|
import { toTxHash } from '@ocap/mcrypto';
|
|
@@ -25,7 +24,13 @@ import {
|
|
|
25
24
|
import type { TPaymentCurrency } from '../store/models/payment-currency';
|
|
26
25
|
import { blocklet, ethWallet, wallet, getVaultAddress } from './auth';
|
|
27
26
|
import logger from './logger';
|
|
28
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
formatLinkWithLocale,
|
|
29
|
+
getBlockletJson,
|
|
30
|
+
getUserOrAppInfo,
|
|
31
|
+
OCAP_PAYMENT_TX_TYPE,
|
|
32
|
+
resolveAddressChainTypes,
|
|
33
|
+
} from './util';
|
|
29
34
|
import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from './constants';
|
|
30
35
|
import { getTokenByAddress } from '../integrations/arcblock/stake';
|
|
31
36
|
import { isCreditMetered } from './session';
|
|
@@ -378,7 +383,7 @@ export function isCreditSufficientForPayment(args: {
|
|
|
378
383
|
return { sufficient: true, balance };
|
|
379
384
|
}
|
|
380
385
|
|
|
381
|
-
export function getGasPayerExtra(txBuffer: Buffer, headers?: { [key: string]: string }) {
|
|
386
|
+
export async function getGasPayerExtra(txBuffer: Buffer, headers?: { [key: string]: string }) {
|
|
382
387
|
if (headers && headers['x-gas-payer-sig'] && headers['x-gas-payer-pk']) {
|
|
383
388
|
return { headers };
|
|
384
389
|
}
|
|
@@ -386,7 +391,7 @@ export function getGasPayerExtra(txBuffer: Buffer, headers?: { [key: string]: st
|
|
|
386
391
|
const txHash = toTxHash(txBuffer);
|
|
387
392
|
return {
|
|
388
393
|
headers: {
|
|
389
|
-
'x-gas-payer-sig':
|
|
394
|
+
'x-gas-payer-sig': await wallet.signJWT({ txHash }),
|
|
390
395
|
'x-gas-payer-pk': wallet.publicKey,
|
|
391
396
|
},
|
|
392
397
|
};
|
|
@@ -564,7 +569,7 @@ export async function isBalanceSufficientForRefund(args: {
|
|
|
564
569
|
throw new Error(`isBalanceSufficientForRefund: Payment method ${paymentMethod.type} not supported`);
|
|
565
570
|
}
|
|
566
571
|
|
|
567
|
-
export async function getDonationBenefits(paymentLink: PaymentLink, url?: string) {
|
|
572
|
+
export async function getDonationBenefits(paymentLink: PaymentLink, url?: string, locale?: string) {
|
|
568
573
|
const { donation_settings: donationSettings } = paymentLink;
|
|
569
574
|
if (!donationSettings) {
|
|
570
575
|
return [];
|
|
@@ -585,7 +590,7 @@ export async function getDonationBenefits(paymentLink: PaymentLink, url?: string
|
|
|
585
590
|
percent: (Number(share) * 100) / total,
|
|
586
591
|
name: name || info?.name || '',
|
|
587
592
|
avatar: avatar || info?.avatar || '',
|
|
588
|
-
url: info?.url || '',
|
|
593
|
+
url: formatLinkWithLocale(info?.url || '', locale),
|
|
589
594
|
type: info?.type || 'user',
|
|
590
595
|
};
|
|
591
596
|
} catch (error) {
|
package/api/src/libs/refund.ts
CHANGED