payment-kit 1.20.19 → 1.20.21

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.
Files changed (31) hide show
  1. package/api/src/integrations/stripe/resource.ts +1 -1
  2. package/api/src/libs/discount/coupon.ts +41 -73
  3. package/api/src/libs/invoice.ts +17 -0
  4. package/api/src/libs/notification/template/subscription-renew-failed.ts +22 -1
  5. package/api/src/libs/notification/template/subscription-will-renew.ts +22 -0
  6. package/api/src/locales/en.ts +1 -0
  7. package/api/src/locales/zh.ts +1 -0
  8. package/api/src/queues/checkout-session.ts +2 -2
  9. package/api/src/routes/checkout-sessions.ts +84 -0
  10. package/api/src/routes/connect/collect-batch.ts +2 -2
  11. package/api/src/routes/connect/pay.ts +1 -1
  12. package/api/src/routes/vendor.ts +3 -1
  13. package/api/src/store/migrations/20250926-change-customer-did-unique.ts +49 -0
  14. package/api/src/store/models/customer.ts +1 -0
  15. package/api/tests/libs/coupon.spec.ts +219 -0
  16. package/api/tests/libs/discount.spec.ts +250 -0
  17. package/blocklet.yml +1 -1
  18. package/package.json +7 -7
  19. package/src/components/discount/discount-info.tsx +0 -1
  20. package/src/components/invoice/action.tsx +26 -0
  21. package/src/components/invoice/table.tsx +2 -9
  22. package/src/components/invoice-pdf/styles.ts +2 -0
  23. package/src/components/invoice-pdf/template.tsx +44 -12
  24. package/src/components/metadata/list.tsx +1 -0
  25. package/src/components/subscription/metrics.tsx +7 -3
  26. package/src/locales/en.tsx +7 -0
  27. package/src/locales/zh.tsx +7 -0
  28. package/src/pages/admin/billing/subscriptions/detail.tsx +11 -3
  29. package/src/pages/admin/products/coupons/applicable-products.tsx +20 -37
  30. package/src/pages/customer/invoice/detail.tsx +1 -1
  31. package/src/pages/customer/subscription/detail.tsx +12 -3
@@ -579,7 +579,7 @@ async function createStripeSubscriptionCoupon(
579
579
  id: existingCouponId, // Use predictable ID
580
580
  name: `sub_coupon_${localCoupon.id}`,
581
581
  duration: localCoupon.duration as 'once' | 'forever' | 'repeating',
582
- max_redemptions: 1, // Limit to this subscription only
582
+ redeem_by: Math.floor((new Date().getTime() + 24 * 60 * 60 * 1000) / 1000),
583
583
  metadata: {
584
584
  appPid: env.appPid,
585
585
  local_coupon_id: localCoupon.id,
@@ -264,7 +264,7 @@ function buildBaseDiscountData({
264
264
  checkoutSession: CheckoutSession;
265
265
  customerId: string;
266
266
  coupon: Coupon;
267
- promotionCode: PromotionCode;
267
+ promotionCode?: PromotionCode;
268
268
  discountAmount: string;
269
269
  verificationData?: any;
270
270
  }) {
@@ -282,18 +282,18 @@ function buildBaseDiscountData({
282
282
  return {
283
283
  livemode: coupon.livemode,
284
284
  coupon_id: coupon.id,
285
- promotion_code_id: promotionCode.id,
285
+ promotion_code_id: promotionCode?.id,
286
286
  customer_id: customerId,
287
287
  checkout_session_id: checkoutSession.id,
288
288
  start: now,
289
289
  end: endTime,
290
- verification_method: promotionCode.verification_type,
290
+ verification_method: promotionCode?.verification_type,
291
291
  verification_data: verificationData || checkoutSession.metadata?.verification_data,
292
292
  metadata: {
293
- checkout_session_id: checkoutSession.id,
294
293
  discount_amount: discountAmount,
295
294
  original_amount: checkoutSession.amount_subtotal,
296
295
  final_amount: checkoutSession.amount_total,
296
+ currency_id: checkoutSession.currency_id,
297
297
  applied_at: new Date().toISOString(),
298
298
  },
299
299
  };
@@ -317,41 +317,23 @@ async function processSubscriptionDiscount({
317
317
  const existingDiscount = existingDiscountMap.get(subscriptionId);
318
318
 
319
319
  if (existingDiscount) {
320
- // Check if coupon or promotion code has changed
321
- if (
322
- existingDiscount.coupon_id !== baseDiscountData.coupon_id ||
323
- existingDiscount.promotion_code_id !== baseDiscountData.promotion_code_id
324
- ) {
325
- // Update existing discount record
326
- await existingDiscount.update({
327
- ...baseDiscountData,
328
- subscription_id: subscriptionId,
329
- metadata: {
330
- ...baseDiscountData.metadata,
331
- subscription_id: subscriptionId,
332
- updated_at: new Date().toISOString(),
333
- },
334
- });
335
-
336
- logger.info('Updated existing discount record for subscription', {
337
- checkoutSessionId,
338
- subscriptionId,
339
- discountId: existingDiscount.id,
340
- oldCouponId: existingDiscount.coupon_id,
341
- newCouponId: baseDiscountData.coupon_id,
342
- });
320
+ const couponChanged = existingDiscount.coupon_id !== baseDiscountData.coupon_id;
321
+ const promoChanged = existingDiscount.promotion_code_id !== baseDiscountData.promotion_code_id;
343
322
 
344
- return { discountRecord: existingDiscount, shouldUpdateUsage: true };
345
- }
323
+ const updated = await existingDiscount.update({
324
+ ...baseDiscountData,
325
+ subscription_id: subscriptionId,
326
+ });
346
327
 
347
- // Same coupon/promotion code, no update needed
348
- logger.debug('Discount record already exists with same coupon/promotion', {
328
+ logger.info('Upserted discount record for subscription', {
349
329
  checkoutSessionId,
350
330
  subscriptionId,
351
- discountId: existingDiscount.id,
331
+ discountId: updated.id,
332
+ couponChanged,
333
+ promoChanged,
352
334
  });
353
335
 
354
- return { discountRecord: existingDiscount, shouldUpdateUsage: false };
336
+ return { discountRecord: updated, shouldUpdateUsage: couponChanged || promoChanged };
355
337
  }
356
338
 
357
339
  // Create new discount record
@@ -397,35 +379,21 @@ async function processNonSubscriptionDiscount(
397
379
  const existingDiscount = existingDiscountMap.get('no_subscription');
398
380
 
399
381
  if (existingDiscount) {
400
- if (
401
- existingDiscount.coupon_id !== baseDiscountData.coupon_id ||
402
- existingDiscount.promotion_code_id !== baseDiscountData.promotion_code_id
403
- ) {
404
- // Update existing discount record
405
- await existingDiscount.update({
406
- ...baseDiscountData,
407
- metadata: {
408
- ...baseDiscountData.metadata,
409
- updated_at: new Date().toISOString(),
410
- },
411
- });
412
-
413
- logger.info('Updated existing discount record', {
414
- checkoutSessionId,
415
- discountId: existingDiscount.id,
416
- oldCouponId: existingDiscount.coupon_id,
417
- newCouponId: baseDiscountData.coupon_id,
418
- });
382
+ const couponChanged = existingDiscount.coupon_id !== baseDiscountData.coupon_id;
383
+ const promoChanged = existingDiscount.promotion_code_id !== baseDiscountData.promotion_code_id;
419
384
 
420
- return { discountRecord: existingDiscount, shouldUpdateUsage: true };
421
- }
385
+ const updated = await existingDiscount.update({
386
+ ...baseDiscountData,
387
+ });
422
388
 
423
- logger.debug('Discount record already exists with same coupon/promotion', {
389
+ logger.info('Upserted single discount record', {
424
390
  checkoutSessionId,
425
- discountId: existingDiscount.id,
391
+ discountId: updated.id,
392
+ couponChanged,
393
+ promoChanged,
426
394
  });
427
395
 
428
- return { discountRecord: existingDiscount, shouldUpdateUsage: false };
396
+ return { discountRecord: updated, shouldUpdateUsage: couponChanged || promoChanged };
429
397
  }
430
398
 
431
399
  // Create new discount record
@@ -464,17 +432,17 @@ async function updateUsageCounts({
464
432
  checkoutSessionId,
465
433
  }: {
466
434
  coupon: Coupon;
467
- promotionCode: PromotionCode;
435
+ promotionCode?: PromotionCode;
468
436
  updatedCoupons: Set<string>;
469
437
  updatedPromotionCodes: Set<string>;
470
438
  checkoutSessionId: string;
471
439
  }): Promise<void> {
472
440
  try {
473
- const updatePromises = [];
441
+ const updatePromises = [] as Array<Promise<any>>;
474
442
 
475
443
  // Create unique keys based on checkout session to prevent duplicate counting
476
444
  const couponSessionKey = `${coupon.id}-${checkoutSessionId}`;
477
- const promotionCodeSessionKey = `${promotionCode.id}-${checkoutSessionId}`;
445
+ const promotionCodeSessionKey = promotionCode ? `${promotionCode.id}-${checkoutSessionId}` : '';
478
446
 
479
447
  if (!updatedCoupons.has(couponSessionKey)) {
480
448
  updatedCoupons.add(couponSessionKey);
@@ -485,7 +453,7 @@ async function updateUsageCounts({
485
453
  );
486
454
  }
487
455
 
488
- if (!updatedPromotionCodes.has(promotionCodeSessionKey)) {
456
+ if (promotionCode && promotionCodeSessionKey && !updatedPromotionCodes.has(promotionCodeSessionKey)) {
489
457
  updatedPromotionCodes.add(promotionCodeSessionKey);
490
458
  updatePromises.push(
491
459
  promotionCode.update({
@@ -496,18 +464,20 @@ async function updateUsageCounts({
496
464
 
497
465
  await Promise.all(updatePromises);
498
466
  emitAsync('discount-status.queued', coupon, 'coupon', true);
499
- emitAsync('discount-status.queued', promotionCode, 'promotion-code', true);
467
+ if (promotionCode) {
468
+ emitAsync('discount-status.queued', promotionCode, 'promotion-code', true);
469
+ }
500
470
 
501
471
  logger.debug('Updated coupon and promotion code usage counts', {
502
472
  checkoutSessionId,
503
473
  couponId: coupon.id,
504
- promotionCodeId: promotionCode.id,
474
+ promotionCodeId: promotionCode?.id,
505
475
  });
506
476
  } catch (error) {
507
477
  logger.error('Error updating usage counts', {
508
478
  checkoutSessionId,
509
479
  couponId: coupon.id,
510
- promotionCodeId: promotionCode.id,
480
+ promotionCodeId: promotionCode?.id,
511
481
  error: error.message,
512
482
  });
513
483
  throw new Error(`Failed to update usage counts: ${error.message}`);
@@ -630,10 +600,9 @@ export async function createDiscountRecordsForCheckout({
630
600
  const discountProcessingPromises = checkoutSession.discounts.map(async (discount: any) => {
631
601
  const { promotion_code: promotionCodeId, coupon: couponId, discount_amount: discountAmount } = discount;
632
602
 
633
- if (!promotionCodeId || !couponId) {
603
+ if (!couponId) {
634
604
  logger.warn('Incomplete discount configuration, skipping', {
635
605
  checkoutSessionId: checkoutSession.id,
636
- hasPromotionCode: !!promotionCodeId,
637
606
  hasCoupon: !!couponId,
638
607
  });
639
608
  return null;
@@ -643,13 +612,12 @@ export async function createDiscountRecordsForCheckout({
643
612
  // Fetch coupon and promotion code
644
613
  const [coupon, promotionCode] = await Promise.all([
645
614
  Coupon.findByPk(couponId),
646
- PromotionCode.findByPk(promotionCodeId),
615
+ promotionCodeId ? PromotionCode.findByPk(promotionCodeId) : Promise.resolve(null),
647
616
  ]);
648
617
 
649
- if (!coupon || !promotionCode) {
650
- logger.warn('Coupon or promotion code not found, skipping discount', {
618
+ if (!coupon) {
619
+ logger.warn('Coupon not found, skipping discount', {
651
620
  couponId,
652
- promotionCodeId,
653
621
  checkoutSessionId: checkoutSession.id,
654
622
  });
655
623
  return null;
@@ -660,7 +628,7 @@ export async function createDiscountRecordsForCheckout({
660
628
  checkoutSession,
661
629
  customerId,
662
630
  coupon,
663
- promotionCode,
631
+ promotionCode: promotionCode || undefined,
664
632
  discountAmount,
665
633
  verificationData: discount.verification_data,
666
634
  });
@@ -725,7 +693,7 @@ export async function createDiscountRecordsForCheckout({
725
693
  try {
726
694
  await updateUsageCounts({
727
695
  coupon,
728
- promotionCode,
696
+ promotionCode: promotionCode || undefined,
729
697
  updatedCoupons,
730
698
  updatedPromotionCodes,
731
699
  checkoutSessionId: checkoutSession.id,
@@ -740,7 +708,7 @@ export async function createDiscountRecordsForCheckout({
740
708
  } catch (error) {
741
709
  logger.error('Failed to update usage counts, but continuing', {
742
710
  couponId: coupon.id,
743
- promotionCodeId: promotionCode.id,
711
+ promotionCodeId: promotionCode?.id,
744
712
  error: error.message,
745
713
  });
746
714
  }
@@ -1386,3 +1386,20 @@ export const migrateSubscriptionPaymentMethodInvoice = async (
1386
1386
  throw error;
1387
1387
  }
1388
1388
  };
1389
+
1390
+ export async function destroyExistingInvoice(invoice: Invoice) {
1391
+ const paymentMethod = await PaymentMethod.findByPk(invoice.default_payment_method_id);
1392
+ try {
1393
+ if (paymentMethod?.type === 'stripe' && invoice.metadata?.stripe_id) {
1394
+ const client = paymentMethod.getStripeClient();
1395
+ await client.invoices.del(invoice.metadata.stripe_id);
1396
+ }
1397
+ } catch (error) {
1398
+ logger.error('Failed to destroy invoice on stripe', {
1399
+ error: error.message,
1400
+ invoiceId: invoice.id,
1401
+ });
1402
+ }
1403
+ await InvoiceItem.destroy({ where: { invoice_id: invoice.id } });
1404
+ await invoice.destroy();
1405
+ }
@@ -20,7 +20,7 @@ import { PaymentCurrency } from '../../../store/models/payment-currency';
20
20
  import { getCustomerInvoicePageUrl } from '../../invoice';
21
21
  import { SufficientForPaymentResult, getPaymentDetail } from '../../payment';
22
22
  import { getMainProductName } from '../../product';
23
- import { getCustomerSubscriptionPageUrl } from '../../subscription';
23
+ import { getCustomerSubscriptionPageUrl, getSubscriptionPaymentAddress } from '../../subscription';
24
24
  import { formatTime, getPrettyMsI18nLocale } from '../../time';
25
25
  import { formatCurrencyInfo, getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
26
26
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
@@ -47,6 +47,7 @@ interface SubscriptionRenewFailedEmailTemplateContext {
47
47
  viewInvoiceLink: string;
48
48
  viewTxHashLink: string | undefined;
49
49
  customActions: any[];
50
+ payer: string;
50
51
  }
51
52
 
52
53
  export class SubscriptionRenewFailedEmailTemplate
@@ -166,6 +167,8 @@ export class SubscriptionRenewFailedEmailTemplate
166
167
  viewInvoiceLink,
167
168
  viewTxHashLink,
168
169
  customActions,
170
+ payer:
171
+ paymentMethod?.type !== 'stripe' && getSubscriptionPaymentAddress(subscription, paymentMethod?.type as string),
169
172
  };
170
173
  }
171
174
 
@@ -185,6 +188,7 @@ export class SubscriptionRenewFailedEmailTemplate
185
188
  viewInvoiceLink,
186
189
  viewTxHashLink,
187
190
  customActions,
191
+ payer,
188
192
  } = await this.getContext();
189
193
 
190
194
  const template: BaseEmailTemplateType = {
@@ -230,6 +234,23 @@ export class SubscriptionRenewFailedEmailTemplate
230
234
  text: productName,
231
235
  },
232
236
  },
237
+ ...(payer && [
238
+ {
239
+ type: 'text',
240
+ data: {
241
+ type: 'plain',
242
+ color: '#9397A1',
243
+ text: translate('notification.common.payer', locale),
244
+ },
245
+ },
246
+ {
247
+ type: 'text',
248
+ data: {
249
+ type: 'plain',
250
+ text: payer,
251
+ },
252
+ },
253
+ ]),
233
254
  {
234
255
  type: 'text',
235
256
  data: {
@@ -40,6 +40,7 @@ interface SubscriptionWillRenewEmailTemplateContext {
40
40
  isCreditSubscription: boolean;
41
41
  isStripe: boolean;
42
42
  isMetered: boolean;
43
+ payer: string;
43
44
  }
44
45
 
45
46
  export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTemplate<SubscriptionWillRenewEmailTemplateContext> {
@@ -159,6 +160,9 @@ export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTem
159
160
  isCreditSubscription,
160
161
  isStripe,
161
162
  isMetered: !isPrePaid,
163
+ payer:
164
+ paymentInfoResult.paymentMethod?.type !== 'stripe' &&
165
+ getSubscriptionPaymentAddress(subscription, paymentInfoResult.paymentMethod!.type),
162
166
  };
163
167
  }
164
168
 
@@ -210,6 +214,7 @@ export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTem
210
214
  isCreditSubscription,
211
215
  isStripe,
212
216
  isMetered,
217
+ payer,
213
218
  } = context;
214
219
 
215
220
  // 如果当前时间大于预计扣费时间,那么不发送通知
@@ -385,6 +390,23 @@ export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTem
385
390
  type: 'section',
386
391
  fields: [
387
392
  ...commonFields,
393
+ ...(payer && [
394
+ {
395
+ type: 'text',
396
+ data: {
397
+ type: 'plain',
398
+ color: '#9397A1',
399
+ text: translate('notification.common.payer', locale),
400
+ },
401
+ },
402
+ {
403
+ type: 'text',
404
+ data: {
405
+ type: 'plain',
406
+ text: payer,
407
+ },
408
+ },
409
+ ]),
388
410
  ...renewAmountFields,
389
411
  ...balanceFields,
390
412
  ...insufficientBalanceFields,
@@ -50,6 +50,7 @@ export default flat({
50
50
  billedAmount: 'Billed amount',
51
51
  viewCreditGrant: 'View Credit Balance',
52
52
  invoiceNumber: 'Invoice Number',
53
+ payer: 'Payer',
53
54
  },
54
55
 
55
56
  billingDiscrepancy: {
@@ -50,6 +50,7 @@ export default flat({
50
50
  billedAmount: '实缴金额',
51
51
  viewCreditGrant: '查看额度',
52
52
  invoiceNumber: '账单编号',
53
+ payer: '付款方',
53
54
  },
54
55
 
55
56
  sendTo: '发送给',
@@ -22,6 +22,7 @@ import {
22
22
  } from '../store/models';
23
23
  import { getCheckoutSessionSubscriptionIds } from '../libs/session';
24
24
  import { rollbackDiscountUsageForCheckoutSession } from '../libs/discount/discount';
25
+ import { destroyExistingInvoice } from '../libs/invoice';
25
26
 
26
27
  type CheckoutSessionJob = {
27
28
  id: string;
@@ -147,8 +148,7 @@ events.on('checkout.session.expired', async (checkoutSession: CheckoutSession) =
147
148
  // Do some reverse lookup if invoice is not related to checkout session
148
149
  const invoice = await Invoice.findOne({ where: { checkout_session_id: checkoutSession.id } });
149
150
  if (invoice) {
150
- await InvoiceItem.destroy({ where: { invoice_id: invoice.id } });
151
- await Invoice.destroy({ where: { id: invoice.id } });
151
+ await destroyExistingInvoice(invoice);
152
152
  logger.info('Invoice and InvoiceItem for checkout session deleted on expire', {
153
153
  checkoutSession: checkoutSession.id,
154
154
  invoice: invoice.id,
@@ -107,6 +107,7 @@ import {
107
107
  } from '../libs/discount/coupon';
108
108
  import { rollbackDiscountUsageForCheckoutSession, applyDiscountsToLineItems } from '../libs/discount/discount';
109
109
  import { formatToShortUrl } from '../libs/url';
110
+ import { destroyExistingInvoice } from '../libs/invoice';
110
111
 
111
112
  const router = Router();
112
113
 
@@ -1200,6 +1201,76 @@ router.get('/:id', auth, async (req, res) => {
1200
1201
  }
1201
1202
  });
1202
1203
 
1204
+ // abort stripe subscription(s) created during an incomplete checkout session
1205
+ router.post('/:id/abort-stripe', user, ensureCheckoutSessionOpen, async (req, res) => {
1206
+ try {
1207
+ const checkoutSession = req.doc as CheckoutSession;
1208
+
1209
+ if (checkoutSession.status === 'complete') {
1210
+ return res.status(400).json({ error: 'Checkout session already completed' });
1211
+ }
1212
+
1213
+ // cancel stripe subscriptions if any
1214
+ const canceledSubscriptions: string[] = [];
1215
+ if (['subscription', 'setup'].includes(checkoutSession.mode)) {
1216
+ const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);
1217
+ const subscriptions = await Subscription.findAll({ where: { id: subscriptionIds } });
1218
+
1219
+ const cancelOps = subscriptions.map(async (sub) => {
1220
+ const stripeSubId = sub.payment_details?.stripe?.subscription_id;
1221
+ if (!stripeSubId) {
1222
+ return null;
1223
+ }
1224
+ const method = await PaymentMethod.findByPk(sub.default_payment_method_id);
1225
+ if (!method || method.type !== 'stripe') {
1226
+ return null;
1227
+ }
1228
+ const client = method.getStripeClient();
1229
+ try {
1230
+ await client.subscriptions.cancel(stripeSubId);
1231
+ await sub.update({
1232
+ payment_details: omit(sub.payment_details || {}, 'stripe'),
1233
+ payment_settings: {
1234
+ payment_method_options: omit(sub.payment_settings?.payment_method_options || {}, 'stripe'),
1235
+ payment_method_types: sub.payment_settings?.payment_method_types || [],
1236
+ },
1237
+ });
1238
+ canceledSubscriptions.push(sub.id);
1239
+ } catch (err) {
1240
+ logger.error('Failed to cancel stripe subscription for checkout abort', {
1241
+ checkoutSessionId: checkoutSession.id,
1242
+ subscriptionId: sub.id,
1243
+ error: err.message,
1244
+ });
1245
+ }
1246
+ return sub.id;
1247
+ });
1248
+ await Promise.all(cancelOps);
1249
+ }
1250
+
1251
+ // remove related invoice if created
1252
+ try {
1253
+ const existInvoice = await Invoice.findOne({ where: { checkout_session_id: checkoutSession.id } });
1254
+ if (existInvoice && existInvoice.status === 'paid') {
1255
+ await destroyExistingInvoice(existInvoice);
1256
+ }
1257
+ } catch (error: any) {
1258
+ logger.error('Failed to destroy invoice on checkout abort', {
1259
+ checkoutSessionId: checkoutSession.id,
1260
+ error: error.message,
1261
+ });
1262
+ }
1263
+ res.json({ checkoutSessionId: checkoutSession.id, canceledSubscriptions });
1264
+ } catch (err: any) {
1265
+ logger.error('Error aborting stripe for checkout session', {
1266
+ sessionId: req.params.id,
1267
+ error: err.message,
1268
+ stack: err.stack,
1269
+ });
1270
+ res.status(500).json({ error: err.message });
1271
+ }
1272
+ });
1273
+
1203
1274
  // for checkout page
1204
1275
  router.get('/retrieve/:id', user, async (req, res) => {
1205
1276
  const doc = await CheckoutSession.findByPk(req.params.id);
@@ -1436,6 +1507,19 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
1436
1507
  amount: checkoutSession.amount_subtotal,
1437
1508
  });
1438
1509
 
1510
+ // destroy exist invoice if currency is not aligned
1511
+ try {
1512
+ const existInvoice = await Invoice.findOne({ where: { checkout_session_id: checkoutSession.id } });
1513
+ if (existInvoice && existInvoice.currency_id !== paymentCurrency.id) {
1514
+ await destroyExistingInvoice(existInvoice);
1515
+ }
1516
+ } catch (err) {
1517
+ logger.error('Failed to destroy exist invoice', {
1518
+ error: err.message,
1519
+ checkoutSessionId: checkoutSession.id,
1520
+ });
1521
+ }
1522
+
1439
1523
  // create or update payment intent
1440
1524
  let paymentIntent: PaymentIntent | null = null;
1441
1525
  if (checkoutSession.mode === 'payment') {
@@ -31,7 +31,7 @@ export default {
31
31
  },
32
32
  onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
33
33
  const { subscriptionId, currencyId, customerId } = extraParams;
34
- const { amount, invoices, paymentCurrency, paymentMethod } = await ensureSubscriptionForCollectBatch(
34
+ const { amount, invoices, paymentCurrency, paymentMethod, subscription } = await ensureSubscriptionForCollectBatch(
35
35
  subscriptionId,
36
36
  currencyId,
37
37
  customerId
@@ -52,7 +52,7 @@ export default {
52
52
  const claims: { [key: string]: object } = {
53
53
  prepareTx: {
54
54
  type: 'TransferV3Tx',
55
- description: `Pay all past due invoices for ${subscriptionId}`,
55
+ description: `Pay all past due invoices for subscription ${subscription?.description}`,
56
56
  partialTx: { from: userDid, pk: userPk, itx },
57
57
  requirement: { tokens },
58
58
  chainInfo: {
@@ -54,7 +54,7 @@ export default {
54
54
  return {
55
55
  prepareTx: {
56
56
  type: 'TransferV3Tx',
57
- description: `Approve the transfer to complete payment ${paymentIntent.id}`,
57
+ description: 'Approve the transfer to complete payment',
58
58
  partialTx: { from: userDid, pk: userPk, itx },
59
59
  requirement: { tokens },
60
60
  chainInfo: {
@@ -342,7 +342,9 @@ async function getVendorStatusById(vendorId: string, orderId: string) {
342
342
 
343
343
  const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
344
344
 
345
- return vendorAdapter.getOrderStatus(vendor, orderId);
345
+ const data = await vendorAdapter.getOrderStatus(vendor, orderId);
346
+
347
+ return { ...data, vendorType: vendor.vendor_type };
346
348
  }
347
349
 
348
350
  async function doRequestVendor(sessionId: string, func: (vendorId: string, orderId: string) => Promise<any>) {
@@ -0,0 +1,49 @@
1
+ /* eslint-disable no-console */
2
+ import { DataTypes, QueryTypes } from 'sequelize';
3
+ import { Migration } from '../migrate';
4
+
5
+ export const up: Migration = async ({ context }) => {
6
+ try {
7
+ await context.changeColumn('customers', 'did', {
8
+ type: DataTypes.STRING(40),
9
+ allowNull: false,
10
+ unique: true,
11
+ });
12
+
13
+ console.log('Successfully added unique constraint to customers.did');
14
+ } catch (error) {
15
+ try {
16
+ const duplicates = await context.sequelize.query(
17
+ `SELECT did, COUNT(*) as count
18
+ FROM customers
19
+ GROUP BY did
20
+ HAVING COUNT(*) > 1
21
+ LIMIT 5`,
22
+ { type: QueryTypes.SELECT }
23
+ );
24
+
25
+ if (duplicates.length > 0) {
26
+ console.warn('Found duplicate DIDs in customers table (showing first 5):', duplicates);
27
+ console.warn('Skipping unique constraint addition. Please clean up duplicate data manually if needed.');
28
+ } else {
29
+ console.error('Failed to add unique constraint for unknown reason:', error.message);
30
+ }
31
+ } catch (checkError) {
32
+ console.error('Failed to check for duplicates:', checkError.message);
33
+ }
34
+ }
35
+ };
36
+
37
+ export const down: Migration = async ({ context }) => {
38
+ try {
39
+ await context.changeColumn('customers', 'did', {
40
+ type: DataTypes.STRING(40),
41
+ allowNull: false,
42
+ unique: false,
43
+ });
44
+
45
+ console.log('Successfully removed unique constraint from customers.did');
46
+ } catch (error) {
47
+ console.warn('Failed to remove unique constraint from customers.did:', error.message);
48
+ }
49
+ };
@@ -81,6 +81,7 @@ export class Customer extends Model<InferAttributes<Customer>, InferCreationAttr
81
81
  did: {
82
82
  type: DataTypes.STRING(40),
83
83
  allowNull: false,
84
+ unique: true,
84
85
  },
85
86
  address: {
86
87
  type: DataTypes.JSON,