payment-kit 1.18.16 → 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.
@@ -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
- const customer = await Customer.findByPk(cs.customer_id);
81
- if (!customer) {
82
- throw new Error(`Customer not found: ${cs.customer_id}`);
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 = getCustomerInvoicePageUrl({
145
- invoiceId: checkoutSession.invoice_id!,
146
- userDid,
147
- locale,
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
  }
@@ -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
+ }
@@ -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: joinURL(process.env.BLOCKLET_APP_URL!, '.well-known/service/blocklet/logo'),
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: user?.fullName,
257
- avatar: joinURL(process.env.BLOCKLET_APP_URL!, user?.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 { SetupIntent } from '../store/models/setup-intent';
72
- import { Subscription } from '../store/models/subscription';
73
- import { SubscriptionItem } from '../store/models/subscription-item';
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 support',
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
- // validate payment settings
617
- const paymentMethod = await PaymentMethod.findByPk(req.body.payment_method);
618
- const paymentCurrency = await PaymentCurrency.findByPk(req.body.payment_currency);
619
- if (!paymentMethod) {
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
- // always update payment amount in case currency has changed
631
- const now = dayjs().unix();
632
- const lineItems = await Price.expand(checkoutSession.line_items, { product: true, upsell: true });
633
-
634
- // trialing can be customized with currency_id list
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 block list
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
- // payment intent is only created when checkout session is in payment mode
867
+ // create or update payment intent
711
868
  let paymentIntent: PaymentIntent | null = null;
712
869
  if (checkoutSession.mode === 'payment') {
713
- const paymentLink = checkoutSession.payment_link_id
714
- ? await PaymentLink.findByPk(checkoutSession.payment_link_id)
715
- : null;
716
- const beneficiaries =
717
- paymentLink?.payment_intent_data?.beneficiaries || paymentLink?.donation_settings?.beneficiaries || [];
718
-
719
- if (checkoutSession.payment_intent_id) {
720
- paymentIntent = await PaymentIntent.findByPk(checkoutSession.payment_intent_id);
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: billingThreshold,
913
- stake_gte: minStakeAmount,
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
  },