payment-kit 1.18.15 → 1.18.17
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 +2 -0
- package/api/src/libs/invoice.ts +5 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +32 -14
- package/api/src/libs/session.ts +9 -1
- package/api/src/libs/util.ts +12 -4
- package/api/src/routes/checkout-sessions.ts +286 -120
- package/api/src/routes/connect/change-payment.ts +9 -1
- package/api/src/routes/connect/change-plan.ts +9 -1
- package/api/src/routes/connect/collect-batch.ts +7 -5
- package/api/src/routes/connect/pay.ts +1 -1
- package/api/src/routes/connect/recharge-account.ts +124 -0
- package/api/src/routes/connect/setup.ts +8 -1
- package/api/src/routes/connect/shared.ts +175 -54
- package/api/src/routes/connect/subscribe.ts +11 -1
- package/api/src/routes/customers.ts +150 -7
- package/api/src/routes/donations.ts +1 -1
- package/api/src/routes/invoices.ts +47 -1
- package/api/src/routes/subscriptions.ts +0 -3
- package/blocklet.yml +2 -1
- package/package.json +16 -16
- package/src/app.tsx +11 -3
- package/src/components/info-card.tsx +6 -2
- package/src/components/info-row.tsx +1 -0
- package/src/components/invoice/recharge.tsx +85 -56
- package/src/components/invoice/table.tsx +7 -1
- package/src/components/subscription/portal/actions.tsx +1 -1
- package/src/components/subscription/portal/list.tsx +6 -0
- package/src/locales/en.tsx +9 -0
- package/src/locales/zh.tsx +9 -0
- package/src/pages/admin/payments/payouts/detail.tsx +16 -5
- package/src/pages/customer/index.tsx +226 -284
- package/src/pages/customer/invoice/detail.tsx +24 -16
- package/src/pages/customer/invoice/past-due.tsx +46 -23
- package/src/pages/customer/payout/detail.tsx +16 -5
- package/src/pages/customer/recharge/account.tsx +513 -0
- package/src/pages/customer/{recharge.tsx → recharge/subscription.tsx} +22 -19
- package/src/pages/customer/subscription/embed.tsx +16 -1
|
@@ -15,12 +15,8 @@ import type { WhereOptions } from 'sequelize';
|
|
|
15
15
|
|
|
16
16
|
import { MetadataSchema } from '../libs/api';
|
|
17
17
|
import { checkPassportForPaymentLink } from '../integrations/blocklet/passport';
|
|
18
|
-
import { handleStripePaymentSucceed } from '../integrations/stripe/handlers/payment-intent';
|
|
19
|
-
import { handleStripeSubscriptionSucceed } from '../integrations/stripe/handlers/subscription';
|
|
20
|
-
import { ensureStripePaymentIntent, ensureStripeSubscription } from '../integrations/stripe/resource';
|
|
21
18
|
import dayjs from '../libs/dayjs';
|
|
22
19
|
import logger from '../libs/logger';
|
|
23
|
-
import { isCreditSufficientForPayment, isDelegationSufficientForPayment } from '../libs/payment';
|
|
24
20
|
import { authenticate } from '../libs/security';
|
|
25
21
|
import {
|
|
26
22
|
canPayWithDelegation,
|
|
@@ -35,6 +31,7 @@ import {
|
|
|
35
31
|
getStatementDescriptor,
|
|
36
32
|
getSupportedPaymentCurrencies,
|
|
37
33
|
getSupportedPaymentMethods,
|
|
34
|
+
isDonationCheckoutSession,
|
|
38
35
|
isLineItemAligned,
|
|
39
36
|
} from '../libs/session';
|
|
40
37
|
import {
|
|
@@ -51,10 +48,11 @@ import {
|
|
|
51
48
|
getDataObjectFromQuery,
|
|
52
49
|
isUserInBlocklist,
|
|
53
50
|
} from '../libs/util';
|
|
54
|
-
import { invoiceQueue } from '../queues/invoice';
|
|
55
|
-
import { paymentQueue } from '../queues/payment';
|
|
56
51
|
import {
|
|
57
52
|
Invoice,
|
|
53
|
+
SetupIntent,
|
|
54
|
+
Subscription,
|
|
55
|
+
SubscriptionItem,
|
|
58
56
|
type LineItem,
|
|
59
57
|
type SubscriptionData,
|
|
60
58
|
type TPriceExpanded,
|
|
@@ -68,10 +66,13 @@ import { PaymentLink } from '../store/models/payment-link';
|
|
|
68
66
|
import { PaymentMethod } from '../store/models/payment-method';
|
|
69
67
|
import { Price } from '../store/models/price';
|
|
70
68
|
import { Product } from '../store/models/product';
|
|
71
|
-
import {
|
|
72
|
-
import {
|
|
73
|
-
import {
|
|
69
|
+
import { ensureStripePaymentIntent, ensureStripeSubscription } from '../integrations/stripe/resource';
|
|
70
|
+
import { handleStripePaymentSucceed } from '../integrations/stripe/handlers/payment-intent';
|
|
71
|
+
import { paymentQueue } from '../queues/payment';
|
|
72
|
+
import { invoiceQueue } from '../queues/invoice';
|
|
74
73
|
import { ensureInvoiceForCheckout } from './connect/shared';
|
|
74
|
+
import { isCreditSufficientForPayment, isDelegationSufficientForPayment } from '../libs/payment';
|
|
75
|
+
import { handleStripeSubscriptionSucceed } from '../integrations/stripe/handlers/subscription';
|
|
75
76
|
import { CHARGE_SUPPORTED_CHAIN_TYPES } from '../libs/constants';
|
|
76
77
|
|
|
77
78
|
const router = Router();
|
|
@@ -143,6 +144,188 @@ export async function validateInventory(line_items: LineItem[], includePendingQu
|
|
|
143
144
|
await Promise.all(checks);
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
export async function validatePaymentSettings(paymentMethodId: string, paymentCurrencyId: string) {
|
|
148
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentMethodId);
|
|
149
|
+
const paymentCurrency = await PaymentCurrency.findByPk(paymentCurrencyId);
|
|
150
|
+
|
|
151
|
+
if (!paymentMethod) {
|
|
152
|
+
throw new Error('Payment method not found');
|
|
153
|
+
}
|
|
154
|
+
if (!paymentCurrency) {
|
|
155
|
+
throw new Error('Payment currency not found');
|
|
156
|
+
}
|
|
157
|
+
if (paymentCurrency.payment_method_id !== paymentMethod.id) {
|
|
158
|
+
throw new Error('Payment currency not match with payment method');
|
|
159
|
+
}
|
|
160
|
+
return { paymentMethod, paymentCurrency };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 计算并更新支付金额
|
|
165
|
+
*/
|
|
166
|
+
export async function calculateAndUpdateAmount(
|
|
167
|
+
checkoutSession: CheckoutSession,
|
|
168
|
+
paymentCurrencyId: string,
|
|
169
|
+
useTrialSetting: boolean = false
|
|
170
|
+
) {
|
|
171
|
+
const now = dayjs().unix();
|
|
172
|
+
const lineItems = await Price.expand(checkoutSession.line_items, { product: true, upsell: true });
|
|
173
|
+
|
|
174
|
+
let trialInDays = 0;
|
|
175
|
+
let trialEnd = 0;
|
|
176
|
+
|
|
177
|
+
// only use trial setting for subscription
|
|
178
|
+
if (useTrialSetting) {
|
|
179
|
+
const trialSetup = getSubscriptionTrialSetup(checkoutSession.subscription_data as any, paymentCurrencyId);
|
|
180
|
+
trialInDays = trialSetup.trialInDays;
|
|
181
|
+
trialEnd = trialSetup.trialEnd;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const amount = getCheckoutAmount(lineItems, paymentCurrencyId, trialInDays > 0 || trialEnd > now);
|
|
185
|
+
|
|
186
|
+
await checkoutSession.update({
|
|
187
|
+
amount_subtotal: amount.subtotal,
|
|
188
|
+
amount_total: amount.total,
|
|
189
|
+
total_details: {
|
|
190
|
+
amount_discount: amount.discount,
|
|
191
|
+
amount_shipping: amount.shipping,
|
|
192
|
+
amount_tax: amount.tax,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (checkoutSession.mode === 'payment' && amount.total <= 0) {
|
|
197
|
+
throw new Error('Payment amount should be greater than 0');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { lineItems, amount, trialInDays, trialEnd, now };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 创建或更新支付意向
|
|
205
|
+
*/
|
|
206
|
+
async function createOrUpdatePaymentIntent(
|
|
207
|
+
checkoutSession: CheckoutSession,
|
|
208
|
+
paymentMethod: PaymentMethod,
|
|
209
|
+
paymentCurrency: PaymentCurrency,
|
|
210
|
+
lineItems: any[],
|
|
211
|
+
customerId?: string,
|
|
212
|
+
customerEmail?: string,
|
|
213
|
+
formData?: any
|
|
214
|
+
) {
|
|
215
|
+
let paymentIntent: PaymentIntent | null = null;
|
|
216
|
+
|
|
217
|
+
if (checkoutSession.mode !== 'payment') {
|
|
218
|
+
return { paymentIntent };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const paymentLink = checkoutSession.payment_link_id
|
|
222
|
+
? await PaymentLink.findByPk(checkoutSession.payment_link_id)
|
|
223
|
+
: null;
|
|
224
|
+
|
|
225
|
+
const beneficiaries =
|
|
226
|
+
paymentLink?.payment_intent_data?.beneficiaries || paymentLink?.donation_settings?.beneficiaries || [];
|
|
227
|
+
|
|
228
|
+
if (checkoutSession.payment_intent_id) {
|
|
229
|
+
paymentIntent = await PaymentIntent.findByPk(checkoutSession.payment_intent_id);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// check existing payment intent
|
|
233
|
+
if (paymentIntent) {
|
|
234
|
+
// Check payment intent, if we have a payment intent, we should not create a new one
|
|
235
|
+
if (paymentIntent.status === 'succeeded') {
|
|
236
|
+
throw new Error('PAYMENT_SUCCEEDED');
|
|
237
|
+
}
|
|
238
|
+
if (paymentIntent.status === 'canceled') {
|
|
239
|
+
throw new Error('PAYMENT_CANCELLED');
|
|
240
|
+
}
|
|
241
|
+
if (paymentIntent.status === 'processing') {
|
|
242
|
+
throw new Error('PAYMENT_PROCESSING');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const updateData: Partial<PaymentIntent> = {
|
|
246
|
+
status: 'requires_capture',
|
|
247
|
+
amount: checkoutSession.amount_total,
|
|
248
|
+
currency_id: paymentCurrency.id,
|
|
249
|
+
payment_method_id: paymentMethod.id,
|
|
250
|
+
last_payment_error: null,
|
|
251
|
+
beneficiaries: createPaymentBeneficiaries(checkoutSession.amount_total, beneficiaries),
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
if (customerId) {
|
|
255
|
+
updateData.customer_id = customerId;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (customerEmail) {
|
|
259
|
+
updateData.receipt_email = customerEmail;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
paymentIntent = await paymentIntent.update(updateData);
|
|
263
|
+
logger.info('payment intent for checkout session reset', {
|
|
264
|
+
session: checkoutSession.id,
|
|
265
|
+
intent: paymentIntent.id,
|
|
266
|
+
});
|
|
267
|
+
} else {
|
|
268
|
+
// 创建新的支付意向
|
|
269
|
+
const createData: any = {
|
|
270
|
+
livemode: !!checkoutSession.livemode,
|
|
271
|
+
amount: checkoutSession.amount_total,
|
|
272
|
+
amount_received: '0',
|
|
273
|
+
amount_capturable: checkoutSession.amount_total,
|
|
274
|
+
description: checkoutSession.payment_intent_data?.description || '',
|
|
275
|
+
currency_id: paymentCurrency.id,
|
|
276
|
+
payment_method_id: paymentMethod.id,
|
|
277
|
+
status: 'requires_payment_method',
|
|
278
|
+
capture_method: 'automatic',
|
|
279
|
+
confirmation_method: 'automatic',
|
|
280
|
+
payment_method_types: checkoutSession.payment_method_types,
|
|
281
|
+
statement_descriptor:
|
|
282
|
+
checkoutSession.payment_intent_data?.statement_descriptor || getStatementDescriptor(lineItems),
|
|
283
|
+
statement_descriptor_suffix: '',
|
|
284
|
+
setup_future_usage: 'on_session',
|
|
285
|
+
beneficiaries: createPaymentBeneficiaries(checkoutSession.amount_total, beneficiaries),
|
|
286
|
+
metadata: checkoutSession.payment_intent_data?.metadata || checkoutSession.metadata,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
if (customerId) {
|
|
290
|
+
createData.customer_id = customerId;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (customerEmail) {
|
|
294
|
+
createData.receipt_email = customerEmail;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (formData) {
|
|
298
|
+
createData.metadata = {
|
|
299
|
+
...createData.metadata,
|
|
300
|
+
is_donation: true,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
paymentIntent = await PaymentIntent.create(createData);
|
|
305
|
+
logger.info('paymentIntent created on checkout session submit', {
|
|
306
|
+
session: checkoutSession.id,
|
|
307
|
+
intent: paymentIntent.id,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// lock prices used by this payment
|
|
311
|
+
await Price.update({ locked: true }, { where: { id: lineItems.map((x) => x.price_id) } });
|
|
312
|
+
|
|
313
|
+
// persist payment intent id
|
|
314
|
+
const updateData: any = { payment_intent_id: paymentIntent.id };
|
|
315
|
+
|
|
316
|
+
if (formData) {
|
|
317
|
+
updateData.metadata = {
|
|
318
|
+
...checkoutSession.metadata,
|
|
319
|
+
is_donation: true,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
await checkoutSession.update(updateData);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { paymentIntent };
|
|
327
|
+
}
|
|
328
|
+
|
|
146
329
|
const SubscriptionDataSchema = Joi.object({
|
|
147
330
|
service_actions: Joi.array()
|
|
148
331
|
.items(
|
|
@@ -441,7 +624,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
441
624
|
payment: 'Thanks for your purchase',
|
|
442
625
|
subscription: 'Thanks for your subscribing',
|
|
443
626
|
setup: 'Thanks for your subscribing',
|
|
444
|
-
donate: 'Thanks for your
|
|
627
|
+
donate: 'Thanks for for your tip',
|
|
445
628
|
};
|
|
446
629
|
const mode = link.submit_type === 'donate' ? 'donate' : raw.mode;
|
|
447
630
|
raw.payment_intent_data = {
|
|
@@ -613,47 +796,19 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
613
796
|
}
|
|
614
797
|
}
|
|
615
798
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
return res.status(400).json({ error: 'Payment method not found' });
|
|
621
|
-
}
|
|
622
|
-
if (!paymentCurrency) {
|
|
623
|
-
return res.status(400).json({ error: 'Payment currency not found' });
|
|
624
|
-
}
|
|
625
|
-
if (paymentCurrency.payment_method_id !== paymentMethod.id) {
|
|
626
|
-
return res.status(400).json({ error: 'Payment currency not match with payment method' });
|
|
627
|
-
}
|
|
799
|
+
const { paymentMethod, paymentCurrency } = await validatePaymentSettings(
|
|
800
|
+
req.body.payment_method,
|
|
801
|
+
req.body.payment_currency
|
|
802
|
+
);
|
|
628
803
|
await checkoutSession.update({ currency_id: paymentCurrency.id });
|
|
629
804
|
|
|
630
|
-
//
|
|
631
|
-
const now =
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const { trialEnd, trialInDays } = getSubscriptionTrialSetup(
|
|
636
|
-
checkoutSession.subscription_data as any,
|
|
637
|
-
paymentCurrency.id
|
|
805
|
+
// calculate amount and update checkout session
|
|
806
|
+
const { lineItems, trialInDays, trialEnd, now } = await calculateAndUpdateAmount(
|
|
807
|
+
checkoutSession,
|
|
808
|
+
paymentCurrency.id,
|
|
809
|
+
true
|
|
638
810
|
);
|
|
639
811
|
|
|
640
|
-
const billingThreshold = Number(checkoutSession.subscription_data?.billing_threshold_amount || 0);
|
|
641
|
-
const minStakeAmount = Number(checkoutSession.subscription_data?.min_stake_amount || 0);
|
|
642
|
-
const amount = getCheckoutAmount(lineItems, paymentCurrency.id, trialInDays > 0 || trialEnd > now);
|
|
643
|
-
await checkoutSession.update({
|
|
644
|
-
amount_subtotal: amount.subtotal,
|
|
645
|
-
amount_total: amount.total,
|
|
646
|
-
total_details: {
|
|
647
|
-
amount_discount: amount.discount,
|
|
648
|
-
amount_shipping: amount.shipping,
|
|
649
|
-
amount_tax: amount.tax,
|
|
650
|
-
},
|
|
651
|
-
});
|
|
652
|
-
if (checkoutSession.mode === 'payment' && amount.total <= 0) {
|
|
653
|
-
return res.status(400).json({ error: 'Payment amount should be greater than 0' });
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// ensure customer created or updated
|
|
657
812
|
let customer = await Customer.findOne({ where: { did: req.user.did } });
|
|
658
813
|
if (!customer) {
|
|
659
814
|
customer = await Customer.create({
|
|
@@ -687,6 +842,8 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
687
842
|
|
|
688
843
|
await customer.update(updates);
|
|
689
844
|
}
|
|
845
|
+
|
|
846
|
+
// check if customer can make new purchase
|
|
690
847
|
const canMakeNewPurchase = await customer.canMakeNewPurchase(checkoutSession.invoice_id);
|
|
691
848
|
if (!canMakeNewPurchase) {
|
|
692
849
|
return res.status(403).json({
|
|
@@ -695,7 +852,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
695
852
|
});
|
|
696
853
|
}
|
|
697
854
|
|
|
698
|
-
// check if user in
|
|
855
|
+
// check if user is in blocklist
|
|
699
856
|
if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
700
857
|
const inBlock = await isUserInBlocklist(req.user.did, paymentMethod);
|
|
701
858
|
if (inBlock) {
|
|
@@ -707,80 +864,21 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
707
864
|
|
|
708
865
|
await checkoutSession.update({ customer_id: customer.id, customer_did: req.user.did });
|
|
709
866
|
|
|
710
|
-
//
|
|
867
|
+
// create or update payment intent
|
|
711
868
|
let paymentIntent: PaymentIntent | null = null;
|
|
712
869
|
if (checkoutSession.mode === 'payment') {
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
// check existing payment intent
|
|
724
|
-
if (paymentIntent) {
|
|
725
|
-
// Check payment intent, if we have a payment intent, we should not create a new one
|
|
726
|
-
if (paymentIntent.status === 'succeeded') {
|
|
727
|
-
return res.status(403).json({ code: 'PAYMENT_SUCCEEDED', error: 'Checkout session payment completed' });
|
|
728
|
-
}
|
|
729
|
-
if (paymentIntent.status === 'canceled') {
|
|
730
|
-
return res.status(403).json({ code: 'PAYMENT_CANCELLED', error: 'Checkout session payment canceled' });
|
|
731
|
-
}
|
|
732
|
-
if (paymentIntent.status === 'processing') {
|
|
733
|
-
return res.status(403).json({ code: 'PAYMENT_PROCESSING', error: 'Checkout session payment processing' });
|
|
734
|
-
}
|
|
735
|
-
paymentIntent = await paymentIntent.update({
|
|
736
|
-
status: 'requires_capture',
|
|
737
|
-
amount: checkoutSession.amount_total,
|
|
738
|
-
customer_id: customer.id,
|
|
739
|
-
currency_id: paymentCurrency.id,
|
|
740
|
-
payment_method_id: paymentMethod.id,
|
|
741
|
-
receipt_email: customer.email,
|
|
742
|
-
last_payment_error: null,
|
|
743
|
-
beneficiaries: createPaymentBeneficiaries(checkoutSession.amount_total, beneficiaries),
|
|
744
|
-
});
|
|
745
|
-
logger.info('payment intent for checkout session reset', {
|
|
746
|
-
session: checkoutSession.id,
|
|
747
|
-
intent: paymentIntent.id,
|
|
748
|
-
});
|
|
749
|
-
} else {
|
|
750
|
-
paymentIntent = await PaymentIntent.create({
|
|
751
|
-
livemode: !!checkoutSession.livemode,
|
|
752
|
-
amount: checkoutSession.amount_total,
|
|
753
|
-
amount_received: '0',
|
|
754
|
-
amount_capturable: checkoutSession.amount_total,
|
|
755
|
-
customer_id: customer.id,
|
|
756
|
-
description: checkoutSession.payment_intent_data?.description || '',
|
|
757
|
-
currency_id: paymentCurrency.id,
|
|
758
|
-
payment_method_id: paymentMethod.id,
|
|
759
|
-
status: 'requires_payment_method',
|
|
760
|
-
capture_method: 'automatic',
|
|
761
|
-
confirmation_method: 'automatic',
|
|
762
|
-
payment_method_types: checkoutSession.payment_method_types,
|
|
763
|
-
receipt_email: customer.email,
|
|
764
|
-
statement_descriptor:
|
|
765
|
-
checkoutSession.payment_intent_data?.statement_descriptor || getStatementDescriptor(lineItems),
|
|
766
|
-
statement_descriptor_suffix: '',
|
|
767
|
-
setup_future_usage: 'on_session',
|
|
768
|
-
beneficiaries: createPaymentBeneficiaries(checkoutSession.amount_total, beneficiaries),
|
|
769
|
-
metadata: checkoutSession.payment_intent_data?.metadata || checkoutSession.metadata,
|
|
770
|
-
});
|
|
771
|
-
logger.info('paymentIntent created on checkout session submit', {
|
|
772
|
-
session: checkoutSession.id,
|
|
773
|
-
intent: paymentIntent.id,
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
// lock prices used by this payment
|
|
777
|
-
await Price.update({ locked: true }, { where: { id: lineItems.map((x) => x.price_id) } });
|
|
778
|
-
|
|
779
|
-
// persist payment intent id
|
|
780
|
-
await checkoutSession.update({ payment_intent_id: paymentIntent.id });
|
|
781
|
-
}
|
|
870
|
+
const result = await createOrUpdatePaymentIntent(
|
|
871
|
+
checkoutSession,
|
|
872
|
+
paymentMethod,
|
|
873
|
+
paymentCurrency,
|
|
874
|
+
lineItems,
|
|
875
|
+
customer.id,
|
|
876
|
+
customer.email
|
|
877
|
+
);
|
|
878
|
+
paymentIntent = result.paymentIntent;
|
|
782
879
|
}
|
|
783
880
|
|
|
881
|
+
// SetupIntent processing
|
|
784
882
|
let setupIntent: SetupIntent | null = null;
|
|
785
883
|
if (checkoutSession.mode === 'setup' && paymentMethod.type !== 'stripe') {
|
|
786
884
|
if (checkoutSession.setup_intent_id) {
|
|
@@ -830,6 +928,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
830
928
|
}
|
|
831
929
|
}
|
|
832
930
|
|
|
931
|
+
// subscription processing
|
|
833
932
|
let subscription: Subscription | null = null;
|
|
834
933
|
if (checkoutSession.mode === 'subscription' || checkoutSession.mode === 'setup') {
|
|
835
934
|
if (checkoutSession.subscription_id) {
|
|
@@ -909,8 +1008,8 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
909
1008
|
},
|
|
910
1009
|
},
|
|
911
1010
|
billing_thresholds: {
|
|
912
|
-
amount_gte:
|
|
913
|
-
stake_gte:
|
|
1011
|
+
amount_gte: getBillingThreshold(checkoutSession.subscription_data as any),
|
|
1012
|
+
stake_gte: getMinStakeAmount(checkoutSession.subscription_data as any),
|
|
914
1013
|
reset_billing_cycle_anchor: false,
|
|
915
1014
|
},
|
|
916
1015
|
pending_invoice_item_interval: setup.recurring,
|
|
@@ -1105,6 +1204,73 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1105
1204
|
}
|
|
1106
1205
|
});
|
|
1107
1206
|
|
|
1207
|
+
// 打赏(不强制登录)
|
|
1208
|
+
router.put('/:id/donate-submit', ensureCheckoutSessionOpen, async (req, res) => {
|
|
1209
|
+
try {
|
|
1210
|
+
const checkoutSession = req.doc as CheckoutSession;
|
|
1211
|
+
if (!isDonationCheckoutSession(checkoutSession)) {
|
|
1212
|
+
return res.status(400).json({
|
|
1213
|
+
code: 'INVALID_DONATION',
|
|
1214
|
+
error: 'This endpoint is only for donations',
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
if (checkoutSession.mode !== 'payment') {
|
|
1219
|
+
return res.status(400).json({
|
|
1220
|
+
code: 'INVALID_MODE',
|
|
1221
|
+
error: 'This endpoint is only for payment mode donations',
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// validate inventory
|
|
1226
|
+
if (checkoutSession.line_items) {
|
|
1227
|
+
try {
|
|
1228
|
+
await validateInventory(checkoutSession.line_items);
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
logger.error('validateInventory failed', {
|
|
1231
|
+
error: err,
|
|
1232
|
+
line_items: checkoutSession.line_items,
|
|
1233
|
+
checkoutSessionId: checkoutSession.id,
|
|
1234
|
+
});
|
|
1235
|
+
return res.status(400).json({ error: err.message });
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// validate payment settings
|
|
1240
|
+
const { paymentMethod, paymentCurrency } = await validatePaymentSettings(
|
|
1241
|
+
req.body.payment_method,
|
|
1242
|
+
req.body.payment_currency
|
|
1243
|
+
);
|
|
1244
|
+
await checkoutSession.update({ currency_id: paymentCurrency.id });
|
|
1245
|
+
|
|
1246
|
+
// calculate amount and update checkout session
|
|
1247
|
+
const { lineItems } = await calculateAndUpdateAmount(checkoutSession, paymentCurrency.id, false);
|
|
1248
|
+
|
|
1249
|
+
const { paymentIntent } = await createOrUpdatePaymentIntent(
|
|
1250
|
+
checkoutSession,
|
|
1251
|
+
paymentMethod,
|
|
1252
|
+
paymentCurrency,
|
|
1253
|
+
lineItems
|
|
1254
|
+
);
|
|
1255
|
+
|
|
1256
|
+
// 返回支付信息
|
|
1257
|
+
return res.json({
|
|
1258
|
+
paymentIntent,
|
|
1259
|
+
checkoutSession,
|
|
1260
|
+
paymentMethod,
|
|
1261
|
+
paymentCurrency,
|
|
1262
|
+
formData: req.body,
|
|
1263
|
+
});
|
|
1264
|
+
} catch (err) {
|
|
1265
|
+
logger.error('Error processing donation submission', {
|
|
1266
|
+
sessionId: req.params.id,
|
|
1267
|
+
error: err.message,
|
|
1268
|
+
stack: err.stack,
|
|
1269
|
+
});
|
|
1270
|
+
res.status(400).json({ code: err.code, error: err.message });
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1108
1274
|
// upsell
|
|
1109
1275
|
router.put('/:id/upsell', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
1110
1276
|
try {
|
|
@@ -99,6 +99,9 @@ export default {
|
|
|
99
99
|
result.push({
|
|
100
100
|
step,
|
|
101
101
|
claim: claims?.[0],
|
|
102
|
+
stepRequest: {
|
|
103
|
+
headers: request?.headers,
|
|
104
|
+
},
|
|
102
105
|
});
|
|
103
106
|
|
|
104
107
|
// 判断是否为最后一步
|
|
@@ -164,12 +167,17 @@ export default {
|
|
|
164
167
|
|
|
165
168
|
if (paymentMethod.type === 'arcblock') {
|
|
166
169
|
await prepareTxExecution();
|
|
170
|
+
const requestArray = result
|
|
171
|
+
.map((item: { stepRequest?: Request }) => item.stepRequest)
|
|
172
|
+
.filter(Boolean) as Request[];
|
|
173
|
+
const requestSource = requestArray.length > 0 ? requestArray : request;
|
|
174
|
+
|
|
167
175
|
const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
|
|
168
176
|
userDid,
|
|
169
177
|
userPk,
|
|
170
178
|
claimsList,
|
|
171
179
|
paymentMethod,
|
|
172
|
-
|
|
180
|
+
requestSource,
|
|
173
181
|
subscription?.id,
|
|
174
182
|
paymentCurrency.contract
|
|
175
183
|
);
|
|
@@ -113,6 +113,9 @@ export default {
|
|
|
113
113
|
result.push({
|
|
114
114
|
step,
|
|
115
115
|
claim: claims?.[0],
|
|
116
|
+
stepRequest: {
|
|
117
|
+
headers: request?.headers,
|
|
118
|
+
},
|
|
116
119
|
});
|
|
117
120
|
|
|
118
121
|
// 判断是否为最后一步
|
|
@@ -171,12 +174,17 @@ export default {
|
|
|
171
174
|
if (paymentMethod.type === 'arcblock') {
|
|
172
175
|
await prepareTxExecution();
|
|
173
176
|
|
|
177
|
+
const requestArray = result
|
|
178
|
+
.map((item: { stepRequest?: Request }) => item.stepRequest)
|
|
179
|
+
.filter(Boolean) as Request[];
|
|
180
|
+
const requestSource = requestArray.length > 0 ? requestArray : request;
|
|
181
|
+
|
|
174
182
|
const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
|
|
175
183
|
userDid,
|
|
176
184
|
userPk,
|
|
177
185
|
claimsList,
|
|
178
186
|
paymentMethod,
|
|
179
|
-
|
|
187
|
+
requestSource,
|
|
180
188
|
subscription?.id,
|
|
181
189
|
paymentCurrency?.contract
|
|
182
190
|
);
|
|
@@ -23,16 +23,18 @@ export default {
|
|
|
23
23
|
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
24
24
|
const { paymentMethod } = await ensureSubscriptionForCollectBatch(
|
|
25
25
|
extraParams.subscriptionId,
|
|
26
|
-
extraParams.currencyId
|
|
26
|
+
extraParams.currencyId,
|
|
27
|
+
extraParams.customerId
|
|
27
28
|
);
|
|
28
29
|
return getAuthPrincipalClaim(paymentMethod, 'pay');
|
|
29
30
|
},
|
|
30
31
|
},
|
|
31
32
|
onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
|
|
32
|
-
const { subscriptionId, currencyId } = extraParams;
|
|
33
|
+
const { subscriptionId, currencyId, customerId } = extraParams;
|
|
33
34
|
const { amount, invoices, paymentCurrency, paymentMethod } = await ensureSubscriptionForCollectBatch(
|
|
34
35
|
subscriptionId,
|
|
35
|
-
currencyId
|
|
36
|
+
currencyId,
|
|
37
|
+
customerId
|
|
36
38
|
);
|
|
37
39
|
|
|
38
40
|
if (paymentMethod.type === 'arcblock') {
|
|
@@ -83,8 +85,8 @@ export default {
|
|
|
83
85
|
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
84
86
|
},
|
|
85
87
|
onAuth: async ({ request, userDid, claims, extraParams }: CallbackArgs) => {
|
|
86
|
-
const { subscriptionId, currencyId } = extraParams;
|
|
87
|
-
const { invoices, paymentMethod } = await ensureSubscriptionForCollectBatch(subscriptionId, currencyId);
|
|
88
|
+
const { subscriptionId, currencyId, customerId } = extraParams;
|
|
89
|
+
const { invoices, paymentMethod } = await ensureSubscriptionForCollectBatch(subscriptionId, currencyId, customerId);
|
|
88
90
|
|
|
89
91
|
const afterTxExecution = async (paymentDetails: any) => {
|
|
90
92
|
const paymentIntents = await PaymentIntent.findAll({
|
|
@@ -20,7 +20,7 @@ export default {
|
|
|
20
20
|
|
|
21
21
|
claims: {
|
|
22
22
|
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
23
|
-
const { paymentMethod } = await ensurePaymentIntent(extraParams.checkoutSessionId);
|
|
23
|
+
const { paymentMethod } = await ensurePaymentIntent(extraParams.checkoutSessionId, '', true);
|
|
24
24
|
return getAuthPrincipalClaim(paymentMethod, 'pay');
|
|
25
25
|
},
|
|
26
26
|
},
|