payment-kit 1.18.16 → 1.18.18
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/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/pay.ts +1 -1
- package/api/src/routes/connect/shared.ts +65 -6
- package/api/src/routes/customers.ts +1 -1
- package/api/src/routes/donations.ts +1 -1
- package/api/src/routes/invoices.ts +1 -1
- package/blocklet.yml +2 -1
- package/package.json +16 -16
- package/src/components/info-card.tsx +3 -1
- package/src/locales/en.tsx +1 -1
- package/src/pages/admin/payments/payouts/detail.tsx +16 -5
- package/src/pages/customer/index.tsx +71 -24
- package/src/pages/customer/invoice/past-due.tsx +1 -0
- package/src/pages/customer/payout/detail.tsx +16 -5
- package/src/pages/customer/recharge/account.tsx +204 -206
- package/src/pages/customer/recharge/subscription.tsx +11 -8
|
@@ -77,9 +77,12 @@ export class CustomerRewardSucceededEmailTemplate
|
|
|
77
77
|
throw new Error(`CheckoutSession(${this.options.checkoutSessionId}) mode must be payment`);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
|
|
80
|
+
let userDid = '';
|
|
81
|
+
if (cs.customer_id) {
|
|
82
|
+
const customer = await Customer.findByPk(cs.customer_id);
|
|
83
|
+
if (customer) {
|
|
84
|
+
userDid = customer.did;
|
|
85
|
+
}
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
await pWaitFor(
|
|
@@ -114,17 +117,31 @@ export class CustomerRewardSucceededEmailTemplate
|
|
|
114
117
|
},
|
|
115
118
|
})) as PaymentCurrency;
|
|
116
119
|
|
|
117
|
-
const userDid: string = customer.did;
|
|
118
|
-
const locale = await getUserLocale(userDid);
|
|
119
|
-
const at: string = formatTime(checkoutSession.created_at);
|
|
120
|
-
|
|
121
|
-
const paymentInfo: string = `${fromUnitToToken(checkoutSession?.amount_total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
122
120
|
const paymentIntent = await PaymentIntent.findByPk(checkoutSession!.payment_intent_id);
|
|
123
121
|
if (!paymentIntent) {
|
|
124
122
|
throw new Error(
|
|
125
123
|
`Payment intent cannot be found for checkoutSession.payment_intent_id${checkoutSession!.payment_intent_id}`
|
|
126
124
|
);
|
|
127
125
|
}
|
|
126
|
+
|
|
127
|
+
// 如果没有从customer获取到userDid,尝试从支付详情中获取
|
|
128
|
+
if (!userDid) {
|
|
129
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentIntent.payment_method_id);
|
|
130
|
+
if (paymentMethod) {
|
|
131
|
+
// @ts-expect-error
|
|
132
|
+
userDid = paymentIntent.payment_details?.[paymentMethod.type]?.payer || '';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!userDid) {
|
|
137
|
+
throw new Error('User DID not found for reward notification');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const locale = await getUserLocale(userDid);
|
|
141
|
+
const at: string = formatTime(checkoutSession.created_at);
|
|
142
|
+
|
|
143
|
+
const paymentInfo: string = `${fromUnitToToken(checkoutSession?.amount_total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
144
|
+
|
|
128
145
|
const rewardDetail = await this.getRewardDetail({
|
|
129
146
|
paymentIntent,
|
|
130
147
|
paymentCurrency,
|
|
@@ -141,11 +158,13 @@ export class CustomerRewardSucceededEmailTemplate
|
|
|
141
158
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(paymentIntent!.payment_method_id);
|
|
142
159
|
// @ts-expect-error
|
|
143
160
|
const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
|
|
144
|
-
const viewInvoiceLink =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
161
|
+
const viewInvoiceLink = checkoutSession.invoice_id
|
|
162
|
+
? getCustomerInvoicePageUrl({
|
|
163
|
+
invoiceId: checkoutSession.invoice_id,
|
|
164
|
+
userDid,
|
|
165
|
+
locale,
|
|
166
|
+
})
|
|
167
|
+
: '';
|
|
149
168
|
|
|
150
169
|
// @ts-expect-error
|
|
151
170
|
const txHash: string | undefined = paymentIntent?.payment_details?.[paymentMethod.type]?.tx_hash;
|
|
@@ -330,7 +349,6 @@ export class CustomerRewardSucceededEmailTemplate
|
|
|
330
349
|
},
|
|
331
350
|
].filter(Boolean),
|
|
332
351
|
};
|
|
333
|
-
|
|
334
352
|
return template;
|
|
335
353
|
}
|
|
336
354
|
}
|
package/api/src/libs/session.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { BN } from '@ocap/util';
|
|
|
4
4
|
import cloneDeep from 'lodash/cloneDeep';
|
|
5
5
|
import isEqual from 'lodash/isEqual';
|
|
6
6
|
|
|
7
|
-
import type { TLineItemExpanded, TPaymentCurrency, TPaymentMethodExpanded } from '../store/models';
|
|
7
|
+
import type { CheckoutSession, TLineItemExpanded, TPaymentCurrency, TPaymentMethodExpanded } from '../store/models';
|
|
8
8
|
import type { Price, TPrice } from '../store/models/price';
|
|
9
9
|
import type { Product } from '../store/models/product';
|
|
10
10
|
import type { PaymentBeneficiary, PriceCurrency, PriceRecurring } from '../store/models/types';
|
|
@@ -375,3 +375,11 @@ export function createPaymentOutput(
|
|
|
375
375
|
},
|
|
376
376
|
];
|
|
377
377
|
}
|
|
378
|
+
|
|
379
|
+
export function isDonationCheckoutSession(checkoutSession: CheckoutSession): boolean {
|
|
380
|
+
return (
|
|
381
|
+
checkoutSession.submit_type === 'donate' ||
|
|
382
|
+
checkoutSession.metadata?.type === 'donation' ||
|
|
383
|
+
!!checkoutSession.metadata?.is_donation
|
|
384
|
+
);
|
|
385
|
+
}
|
package/api/src/libs/util.ts
CHANGED
|
@@ -220,7 +220,7 @@ export async function getBlockletJson(url?: string) {
|
|
|
220
220
|
componentMountPoints: BLOCKLET_MOUNT_POINTS,
|
|
221
221
|
appId: process.env.BLOCKLET_APP_ID,
|
|
222
222
|
appName: process.env.BLOCKLET_APP_NAME,
|
|
223
|
-
appLogo:
|
|
223
|
+
appLogo: '/.well-known/service/blocklet/logo',
|
|
224
224
|
appUrl: process.env.BLOCKLET_APP_URL,
|
|
225
225
|
};
|
|
226
226
|
}
|
|
@@ -236,7 +236,7 @@ export async function getUserOrAppInfo(
|
|
|
236
236
|
if (blockletJson?.appId === address) {
|
|
237
237
|
return {
|
|
238
238
|
name: blockletJson?.appName,
|
|
239
|
-
avatar: blockletJson?.appLogo
|
|
239
|
+
avatar: `${blockletJson?.appUrl}${blockletJson?.appLogo}`,
|
|
240
240
|
type: 'dapp',
|
|
241
241
|
url: blockletJson?.appUrl,
|
|
242
242
|
};
|
|
@@ -252,9 +252,17 @@ export async function getUserOrAppInfo(
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
const { user } = await blocklet.getUser(address);
|
|
255
|
+
if (user) {
|
|
256
|
+
return {
|
|
257
|
+
name: user?.fullName,
|
|
258
|
+
avatar: joinURL(process.env.BLOCKLET_APP_URL!, user?.avatar),
|
|
259
|
+
type: 'user',
|
|
260
|
+
url: getCustomerProfileUrl({ userDid: address, locale: 'en' }),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
255
263
|
return {
|
|
256
|
-
name:
|
|
257
|
-
avatar:
|
|
264
|
+
name: 'anonymous',
|
|
265
|
+
avatar: getUrl('/methods/default.png'),
|
|
258
266
|
type: 'user',
|
|
259
267
|
url: getCustomerProfileUrl({ userDid: address, locale: 'en' }),
|
|
260
268
|
};
|
|
@@ -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 {
|
|
@@ -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
|
},
|