payment-kit 1.18.24 → 1.18.26

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 (41) hide show
  1. package/api/src/libs/event.ts +22 -2
  2. package/api/src/libs/invoice.ts +142 -0
  3. package/api/src/libs/notification/template/aggregated-subscription-renewed.ts +165 -0
  4. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +2 -5
  5. package/api/src/libs/notification/template/subscription-canceled.ts +2 -3
  6. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +7 -4
  7. package/api/src/libs/notification/template/subscription-renew-failed.ts +3 -5
  8. package/api/src/libs/notification/template/subscription-renewed.ts +2 -2
  9. package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +2 -3
  10. package/api/src/libs/notification/template/subscription-succeeded.ts +2 -2
  11. package/api/src/libs/notification/template/subscription-upgraded.ts +5 -5
  12. package/api/src/libs/notification/template/subscription-will-renew.ts +2 -2
  13. package/api/src/libs/queue/index.ts +6 -0
  14. package/api/src/libs/queue/store.ts +13 -1
  15. package/api/src/libs/util.ts +22 -1
  16. package/api/src/locales/en.ts +5 -0
  17. package/api/src/locales/zh.ts +5 -0
  18. package/api/src/queues/invoice.ts +21 -7
  19. package/api/src/queues/notification.ts +353 -11
  20. package/api/src/queues/payment.ts +26 -10
  21. package/api/src/queues/payout.ts +21 -7
  22. package/api/src/routes/checkout-sessions.ts +26 -12
  23. package/api/src/routes/connect/recharge-account.ts +13 -1
  24. package/api/src/routes/connect/recharge.ts +13 -1
  25. package/api/src/routes/connect/shared.ts +54 -36
  26. package/api/src/routes/customers.ts +61 -0
  27. package/api/src/routes/invoices.ts +51 -1
  28. package/api/src/routes/subscriptions.ts +1 -1
  29. package/api/src/store/migrations/20250328-notification-preference.ts +29 -0
  30. package/api/src/store/models/customer.ts +42 -1
  31. package/api/src/store/models/types.ts +17 -1
  32. package/blocklet.yml +1 -1
  33. package/package.json +24 -24
  34. package/src/components/customer/form.tsx +21 -2
  35. package/src/components/customer/notification-preference.tsx +428 -0
  36. package/src/components/layout/user.tsx +1 -1
  37. package/src/locales/en.tsx +30 -0
  38. package/src/locales/zh.tsx +30 -0
  39. package/src/pages/customer/index.tsx +27 -23
  40. package/src/pages/customer/recharge/account.tsx +19 -17
  41. package/src/pages/customer/subscription/embed.tsx +25 -9
@@ -212,5 +212,10 @@ export default flat({
212
212
  title: 'Insufficient Credit for SubGuard™',
213
213
  body: 'Your subscription to {productName} has insufficient staked credit for SubGuard™. Please increase your stake to maintain the service or disable it if no longer needed.',
214
214
  },
215
+
216
+ aggregatedSubscriptionRenewed: {
217
+ title: '{count} Subscriptions Renewed',
218
+ body: 'During {startTime} - {endTime}, {count} subscriptions were successfully renewed, total amount: {totalAmount}.\n\nSubscription List:\n{subscriptionList}',
219
+ },
215
220
  },
216
221
  });
@@ -206,5 +206,10 @@ export default flat({
206
206
  title: '订阅守护服务额度不足',
207
207
  body: '您订阅的 {productName} 订阅守护服务额度不足,为了避免影响您的使用,请及时充值。如不再需要订阅守护服务,可关闭该功能。',
208
208
  },
209
+
210
+ aggregatedSubscriptionRenewed: {
211
+ title: '{count} 个订阅续费成功',
212
+ body: '在 {startTime} - {endTime} 期间,共有 {count} 个订阅成功续费,总金额:{totalAmount}。\n\n订阅清单:\n{subscriptionList}',
213
+ },
209
214
  },
210
215
  });
@@ -289,16 +289,30 @@ events.on('invoice.queued', async (id, job, args = {}) => {
289
289
  job,
290
290
  ...extraArgs,
291
291
  });
292
- events.emit('invoice.queued.done');
292
+ events.emit('invoice.queued.done', { id });
293
293
  } catch (error) {
294
294
  logger.error('Error in invoice.queued', { id, job, error });
295
- events.emit('invoice.queued.error', error);
295
+ events.emit('invoice.queued.error', { id, job, error });
296
296
  }
297
297
  return;
298
298
  }
299
- invoiceQueue.push({
300
- id,
301
- job,
302
- ...extraArgs,
303
- });
299
+ try {
300
+ const existJob = await invoiceQueue.get(id);
301
+ if (existJob) {
302
+ await invoiceQueue.delete(id);
303
+ logger.info('Removed existing invoice job for immediate execution', {
304
+ id,
305
+ originalRunAt: existJob.runAt,
306
+ });
307
+ }
308
+ invoiceQueue.push({
309
+ id,
310
+ job,
311
+ ...extraArgs,
312
+ });
313
+ events.emit('invoice.queued.done', { id });
314
+ } catch (error) {
315
+ logger.error('Error in invoice.queued', { id, job, error });
316
+ events.emit('invoice.queued.error', { id, job, error });
317
+ }
304
318
  });
@@ -1,6 +1,8 @@
1
1
  /* eslint-disable @typescript-eslint/indent */
2
+ import { Op } from 'sequelize';
2
3
  import { events } from '../libs/event';
3
4
  import logger from '../libs/logger';
5
+ import dayjs from '../libs/dayjs';
4
6
  import { Notification } from '../libs/notification';
5
7
  import type { BaseEmailTemplate } from '../libs/notification/template/base';
6
8
  import {
@@ -56,7 +58,16 @@ import {
56
58
  SubscriptionStakeSlashSucceededEmailTemplateOptions,
57
59
  } from '../libs/notification/template/subscription-stake-slash-succeeded';
58
60
  import createQueue from '../libs/queue';
59
- import { CheckoutSession, EventType, Invoice, PaymentLink, Payout, Refund, Subscription } from '../store/models';
61
+ import {
62
+ CheckoutSession,
63
+ EventType,
64
+ Invoice,
65
+ PaymentLink,
66
+ Payout,
67
+ Refund,
68
+ Subscription,
69
+ Customer,
70
+ } from '../store/models';
60
71
  import {
61
72
  UsageReportEmptyEmailTemplate,
62
73
  UsageReportEmptyEmailTemplateOptions,
@@ -73,9 +84,22 @@ import {
73
84
  CustomerRevenueSucceededEmailTemplate,
74
85
  CustomerRevenueSucceededEmailTemplateOptions,
75
86
  } from '../libs/notification/template/customer-revenue-succeeded';
87
+ import {
88
+ AggregatedSubscriptionRenewedEmailTemplate,
89
+ AggregatedSubscriptionRenewedEmailTemplateOptions,
90
+ } from '../libs/notification/template/aggregated-subscription-renewed';
91
+ import type { TJob } from '../store/models/job';
76
92
 
77
93
  export type NotificationQueueJobOptions = any;
78
94
 
95
+ interface NotificationPreference {
96
+ frequency: 'default' | 'daily' | 'weekly' | 'monthly';
97
+ schedule?: {
98
+ time: string; // HH:mm 格式
99
+ date?: number; // weekly: 0-6, monthly: 1-31
100
+ };
101
+ }
102
+
79
103
  export type NotificationQueueJobType =
80
104
  | EventType
81
105
  | 'customer.subscription.will_renew'
@@ -91,6 +115,62 @@ export type NotificationQueueJob = {
91
115
  options: NotificationQueueJobOptions;
92
116
  };
93
117
 
118
+ export interface AggregatedNotificationJob {
119
+ type: NotificationQueueJobType;
120
+ options: {
121
+ customer_id: string;
122
+ items: NotificationItem[];
123
+ time_range: {
124
+ start: number;
125
+ end: number;
126
+ };
127
+ };
128
+ }
129
+
130
+ function calculateNextNotificationTime(preference: NotificationPreference): number {
131
+ if (preference.frequency === 'default' || !preference.schedule) {
132
+ return Math.floor(Date.now() / 1000);
133
+ }
134
+
135
+ const now = dayjs();
136
+ const [hour = 10, minute = 0] = preference.schedule?.time ? preference.schedule.time.split(':').map(Number) : [10, 0];
137
+ let nextTime = dayjs().hour(hour).minute(minute).second(0);
138
+ const targetDate = preference.schedule?.date ?? 1;
139
+
140
+ switch (preference.frequency) {
141
+ case 'daily':
142
+ if (nextTime.isBefore(now)) {
143
+ nextTime = nextTime.add(1, 'day');
144
+ }
145
+ break;
146
+
147
+ case 'weekly':
148
+ nextTime = nextTime.day(targetDate);
149
+ if (nextTime.isBefore(now)) {
150
+ nextTime = nextTime.add(1, 'week');
151
+ }
152
+ break;
153
+
154
+ case 'monthly':
155
+ nextTime = nextTime.date(targetDate);
156
+ // if the date is not the same, set to the end of the month
157
+ if (nextTime.date() !== targetDate) {
158
+ nextTime = nextTime.endOf('month').hour(hour).minute(minute);
159
+ }
160
+ if (nextTime.isBefore(now)) {
161
+ nextTime = nextTime.add(1, 'month');
162
+ if (nextTime.date() !== targetDate) {
163
+ nextTime = nextTime.endOf('month').hour(hour).minute(minute);
164
+ }
165
+ }
166
+ break;
167
+ default:
168
+ throw new Error(`Unknown frequency: ${preference.frequency}`);
169
+ }
170
+
171
+ return Math.floor(nextTime.valueOf() / 1000);
172
+ }
173
+
94
174
  function getNotificationTemplate(job: NotificationQueueJob): BaseEmailTemplate {
95
175
  if (job.type === 'usage.report.empty') {
96
176
  return new UsageReportEmptyEmailTemplate(job.options as UsageReportEmptyEmailTemplateOptions);
@@ -156,10 +236,158 @@ async function handleNotificationJob(job: NotificationQueueJob): Promise<void> {
156
236
  try {
157
237
  const template = getNotificationTemplate(job);
158
238
  await new Notification(template).send();
159
- logger.info('handleNotificationJob.success', { job });
239
+ logger.info('handleImmediateNotificationJob.success', { job });
160
240
  } catch (error) {
161
- logger.error('handleNotificationJob.error.$job', job);
162
- logger.error('handleNotificationJob.error', error);
241
+ logger.error('handleImmediateNotificationJob.error', error);
242
+ throw error;
243
+ }
244
+ }
245
+
246
+ const MAX_AGGREGATED_ITEMS = 1000;
247
+
248
+ interface NotificationItem<T = Record<string, any>> {
249
+ event_id: string;
250
+ occurred_at: number;
251
+ data: T;
252
+ }
253
+
254
+ /**
255
+ * Handles immediate notifications by pushing them directly to the notification queue
256
+ */
257
+ function handleImmediateNotification(type: string, data: Record<string, any>, extraIds: string[] = []) {
258
+ const idParts = [type];
259
+ if (extraIds.length) {
260
+ idParts.push(...extraIds);
261
+ } else {
262
+ idParts.push(Date.now().toString());
263
+ }
264
+
265
+ return notificationQueue.push({
266
+ id: idParts.join('.'),
267
+ job: {
268
+ type,
269
+ options: data,
270
+ },
271
+ });
272
+ }
273
+
274
+ /**
275
+ * Handles aggregated notifications with support for:
276
+ * - Automatic fallback to immediate notification on error
277
+ * - Item limit enforcement
278
+ * - Time range tracking
279
+ */
280
+ async function handleAggregatedNotification<T extends Record<string, any>>({
281
+ type,
282
+ customerId,
283
+ preference,
284
+ newItem,
285
+ extraIds,
286
+ }: {
287
+ type: NotificationQueueJobType;
288
+ customerId: string;
289
+ preference: NotificationPreference;
290
+ newItem: NotificationItem<T>;
291
+ extraIds: string[];
292
+ }) {
293
+ try {
294
+ const nextNotificationTime = calculateNextNotificationTime(preference);
295
+ const jobId = `${type}.${customerId}.${nextNotificationTime}`;
296
+ const existingJob = await aggregatedNotificationQueue.get(jobId);
297
+
298
+ if (existingJob) {
299
+ const updatedItems = [...(existingJob.options.items || []), newItem];
300
+
301
+ if (updatedItems.length >= MAX_AGGREGATED_ITEMS) {
302
+ await aggregatedNotificationQueue.push({
303
+ id: `${jobId}.immediate`,
304
+ job: {
305
+ type,
306
+ options: {
307
+ customer_id: customerId,
308
+ items: existingJob.options.items,
309
+ time_range: {
310
+ start: existingJob.options.time_range.start,
311
+ end: Math.floor(Date.now() / 1000),
312
+ },
313
+ },
314
+ },
315
+ });
316
+ logger.info('immediate aggregated notification job', { jobId });
317
+
318
+ await aggregatedNotificationQueue.update(jobId, {
319
+ job: {
320
+ type,
321
+ options: {
322
+ customer_id: customerId,
323
+ items: [newItem],
324
+ time_range: {
325
+ start: Math.floor(Date.now() / 1000),
326
+ end: nextNotificationTime,
327
+ },
328
+ },
329
+ },
330
+ runAt: nextNotificationTime,
331
+ });
332
+ logger.info('update aggregated notification job', { jobId });
333
+ } else {
334
+ await aggregatedNotificationQueue.update(jobId, {
335
+ job: {
336
+ type,
337
+ options: {
338
+ customer_id: customerId,
339
+ items: updatedItems,
340
+ time_range: {
341
+ start: existingJob.options.time_range.start,
342
+ end: nextNotificationTime,
343
+ },
344
+ },
345
+ },
346
+ runAt: nextNotificationTime,
347
+ });
348
+ }
349
+ logger.info('updated aggregated notification job', { jobId });
350
+ } else {
351
+ // Create new aggregation task
352
+ await aggregatedNotificationQueue.push({
353
+ id: jobId,
354
+ job: {
355
+ type,
356
+ options: {
357
+ customer_id: customerId,
358
+ items: [newItem],
359
+ time_range: {
360
+ start: Math.floor(Date.now() / 1000),
361
+ end: nextNotificationTime,
362
+ },
363
+ },
364
+ },
365
+ runAt: nextNotificationTime,
366
+ });
367
+ }
368
+ logger.info('created aggregated notification job', { jobId });
369
+ } catch (error) {
370
+ logger.error('Failed to handle aggregated notification', { error });
371
+ await handleImmediateNotification(type, newItem.data, extraIds);
372
+ }
373
+ }
374
+
375
+ function getAggregatedNotificationTemplate(job: AggregatedNotificationJob): BaseEmailTemplate {
376
+ if (job.type === 'customer.subscription.renewed') {
377
+ return new AggregatedSubscriptionRenewedEmailTemplate(
378
+ job.options as AggregatedSubscriptionRenewedEmailTemplateOptions
379
+ );
380
+ }
381
+ throw new Error(`Unknown aggregated notification type: ${job.type}`);
382
+ }
383
+
384
+ async function handleAggregatedNotificationJob(job: AggregatedNotificationJob): Promise<void> {
385
+ try {
386
+ const template = await getAggregatedNotificationTemplate(job);
387
+ await new Notification(template).send();
388
+ logger.info('handleAggregatedNotificationJob.success', { job });
389
+ } catch (error) {
390
+ logger.error('handleAggregatedNotificationJob.error', error);
163
391
  throw error;
164
392
  }
165
393
  }
@@ -174,6 +402,16 @@ export const notificationQueue = createQueue<NotificationQueueJob>({
174
402
  },
175
403
  });
176
404
 
405
+ export const aggregatedNotificationQueue = createQueue<AggregatedNotificationJob>({
406
+ name: 'aggregated_notification',
407
+ onJob: handleAggregatedNotificationJob,
408
+ options: {
409
+ concurrency: 10,
410
+ maxRetries: 3,
411
+ enableScheduledJob: true,
412
+ },
413
+ });
414
+
177
415
  /**
178
416
  *
179
417
  * @see https://team.arcblock.io/comment/discussions/0fba06ff-75b4-47d2-8c5c-f0379540ce03
@@ -239,16 +477,39 @@ export async function startNotificationQueue() {
239
477
  }
240
478
  });
241
479
 
242
- events.on('customer.subscription.renewed', (subscription: Subscription) => {
243
- notificationQueue.push({
244
- id: `customer.subscription.renewed.${subscription.id}.${subscription.latest_invoice_id}`,
245
- job: {
246
- type: 'customer.subscription.renewed',
247
- options: {
480
+ events.on('customer.subscription.renewed', async (subscription: Subscription) => {
481
+ const customer = await Customer.findByPk(subscription.customer_id);
482
+ const preference = customer?.preference?.notification;
483
+ if (!subscription.latest_invoice_id) {
484
+ logger.warn('Missing latest_invoice_id for subscription', { subscriptionId: subscription.id });
485
+ return;
486
+ }
487
+
488
+ if (!preference || preference.frequency === 'default') {
489
+ handleImmediateNotification(
490
+ 'customer.subscription.renewed',
491
+ {
492
+ subscriptionId: subscription.id,
493
+ invoiceId: subscription.latest_invoice_id,
494
+ },
495
+ [subscription.id, subscription.latest_invoice_id]
496
+ );
497
+ return;
498
+ }
499
+
500
+ await handleAggregatedNotification({
501
+ type: 'customer.subscription.renewed',
502
+ customerId: customer.id,
503
+ preference,
504
+ newItem: {
505
+ event_id: `${subscription.id}.${subscription.latest_invoice_id}`,
506
+ occurred_at: Math.floor(Date.now() / 1000),
507
+ data: {
248
508
  subscriptionId: subscription.id,
249
509
  invoiceId: subscription.latest_invoice_id,
250
- } as SubscriptionRenewedEmailTemplateOptions,
510
+ },
251
511
  },
512
+ extraIds: [subscription.id, subscription.latest_invoice_id],
252
513
  });
253
514
  });
254
515
 
@@ -362,3 +623,84 @@ export async function startNotificationQueue() {
362
623
  }
363
624
  });
364
625
  }
626
+
627
+ export async function handleNotificationPreferenceChange(
628
+ customerId: string,
629
+ newPreference?: NotificationPreference
630
+ ): Promise<void> {
631
+ // 1. get pending scheduled jobs for this customer
632
+ const pendingJobs = await aggregatedNotificationQueue.store.findJobs({
633
+ delay: { [Op.not]: -1 },
634
+ will_run_at: { [Op.gt]: Date.now() },
635
+ });
636
+
637
+ const customerJobs = pendingJobs.filter((job: TJob) => job.job?.options?.customer_id === customerId);
638
+
639
+ if (!customerJobs.length) {
640
+ return;
641
+ }
642
+
643
+ // 2. if changed to immediate notification, delete the old job and create a new one immediately
644
+ if (newPreference?.frequency === 'default') {
645
+ /* eslint-disable no-await-in-loop */
646
+ for (const job of customerJobs) {
647
+ try {
648
+ await aggregatedNotificationQueue.delete(job.id);
649
+ aggregatedNotificationQueue.push({
650
+ id: job.id,
651
+ job: {
652
+ ...job.job,
653
+ options: {
654
+ ...job.job.options,
655
+ time_range: {
656
+ start: job.job.options.time_range.start,
657
+ end: Math.floor(Date.now() / 1000),
658
+ },
659
+ },
660
+ },
661
+ });
662
+ } catch (error) {
663
+ logger.error('Failed to process immediate notification job', { error, jobId: job.id });
664
+ }
665
+ }
666
+ /* eslint-enable no-await-in-loop */
667
+ return;
668
+ }
669
+
670
+ // 3. for other frequency changes, recalculate the send time
671
+ if (newPreference) {
672
+ /* eslint-disable no-await-in-loop */
673
+ for (const job of customerJobs) {
674
+ try {
675
+ const nextTime = calculateNextNotificationTime(newPreference);
676
+ const oldScheduleTime = job.will_run_at ? job.will_run_at / 1000 : 0;
677
+
678
+ // if next notification time is different from the old schedule time, update the job
679
+ if (nextTime !== oldScheduleTime) {
680
+ // Keep the original job id format but update the timestamp
681
+ const jobIdParts = job.id.split('.');
682
+ const jobId = `${jobIdParts.slice(0, -1).join('.')}.${nextTime}`;
683
+
684
+ await aggregatedNotificationQueue.delete(job.id);
685
+ aggregatedNotificationQueue.push({
686
+ id: jobId,
687
+ job: {
688
+ ...job.job,
689
+ options: {
690
+ ...job.job.options,
691
+ time_range: {
692
+ start: job.job.options.time_range.start,
693
+ end: nextTime,
694
+ },
695
+ },
696
+ },
697
+ runAt: nextTime,
698
+ });
699
+ }
700
+ } catch (error) {
701
+ logger.error('Failed to process notification job', { error, jobId: job.id });
702
+ }
703
+ }
704
+ /* eslint-enable no-await-in-loop */
705
+ }
706
+ }
@@ -45,6 +45,8 @@ type PaymentJob = {
45
45
  paymentIntentId: string;
46
46
  paymentSettings?: PaymentSettings;
47
47
  retryOnError?: boolean;
48
+ ignoreMaxRetryCheck?: boolean;
49
+ immediateRetry?: boolean;
48
50
  };
49
51
 
50
52
  type DepositVaultJob = {
@@ -721,7 +723,7 @@ export const handlePayment = async (job: PaymentJob) => {
721
723
  }
722
724
 
723
725
  // check max retry before doing any hard work
724
- if (invoice && invoice.attempt_count >= MAX_RETRY_COUNT) {
726
+ if (invoice && invoice.attempt_count >= MAX_RETRY_COUNT && !job.ignoreMaxRetryCheck) {
725
727
  logger.info('PaymentIntent capture aborted since max retry exceeded', { id: paymentIntent.id });
726
728
  const updates = await handlePaymentFailed(
727
729
  paymentIntent,
@@ -882,7 +884,7 @@ export const handlePayment = async (job: PaymentJob) => {
882
884
  };
883
885
 
884
886
  if (!job.retryOnError) {
885
- // To a final state without any retry
887
+ // To a final state without any retry
886
888
  await paymentIntent.update({ status: 'requires_action', last_payment_error: error });
887
889
  if (invoice) {
888
890
  await invoice.update({
@@ -924,7 +926,6 @@ export const handlePayment = async (job: PaymentJob) => {
924
926
  }
925
927
  }
926
928
  }
927
-
928
929
  // reschedule next attempt
929
930
  const retryAt = updates.invoice.next_payment_attempt;
930
931
  if (retryAt) {
@@ -993,16 +994,31 @@ events.on('payment.queued', async (id, job, args = {}) => {
993
994
  job,
994
995
  ...extraArgs,
995
996
  });
996
- events.emit('payment.queued.done');
997
+ events.emit('payment.queued.done', { id, job });
997
998
  } catch (error) {
998
999
  logger.error('Error in payment.queued', { id, job, error });
999
- events.emit('payment.queued.error', error);
1000
+ events.emit('payment.queued.error', { id, job, error });
1000
1001
  }
1001
1002
  return;
1002
1003
  }
1003
- paymentQueue.push({
1004
- id,
1005
- job,
1006
- ...extraArgs,
1007
- });
1004
+
1005
+ try {
1006
+ const existingJob = await paymentQueue.get(id);
1007
+ if (existingJob) {
1008
+ await paymentQueue.delete(id);
1009
+ logger.info('Removed existing payment job for immediate execution', {
1010
+ id,
1011
+ originalRunAt: existingJob.runAt,
1012
+ });
1013
+ }
1014
+ paymentQueue.push({
1015
+ id,
1016
+ job,
1017
+ ...extraArgs,
1018
+ });
1019
+ events.emit('payment.queued.done', { id, job });
1020
+ } catch (error) {
1021
+ logger.error('Error in payment.queued', { id, job, error });
1022
+ events.emit('payment.queued.error', { id, job, error });
1023
+ }
1008
1024
  });
@@ -282,16 +282,30 @@ events.on('payout.queued', async (id, job, args = {}) => {
282
282
  job,
283
283
  ...extraArgs,
284
284
  });
285
- events.emit('payout.queued.done');
285
+ events.emit('payout.queued.done', { id, job });
286
286
  } catch (error) {
287
287
  logger.error('Error in payout.queued', { id, job, error });
288
- events.emit('payout.queued.error', error);
288
+ events.emit('payout.queued.error', { id, job, error });
289
289
  }
290
290
  return;
291
291
  }
292
- payoutQueue.push({
293
- id,
294
- job,
295
- ...extraArgs,
296
- });
292
+ try {
293
+ const existJob = await payoutQueue.get(id);
294
+ if (existJob) {
295
+ await payoutQueue.delete(id);
296
+ logger.info('Removed existing payout job for immediate execution', {
297
+ id,
298
+ originalRunAt: existJob.runAt,
299
+ });
300
+ }
301
+ payoutQueue.push({
302
+ id,
303
+ job,
304
+ ...extraArgs,
305
+ });
306
+ events.emit('payout.queued.done', { id, job });
307
+ } catch (error) {
308
+ logger.error('Error in payout.queued', { id, job, error });
309
+ events.emit('payout.queued.error', { id, job, error });
310
+ }
297
311
  });
@@ -856,10 +856,17 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
856
856
  });
857
857
  logger.info('customer created on checkout session submit', { did: req.user.did, id: customer.id });
858
858
  try {
859
- await blocklet.updateUserAddress({
860
- did: customer.did,
861
- address: Customer.formatAddressFromCustomer(customer),
862
- });
859
+ await blocklet.updateUserAddress(
860
+ {
861
+ did: customer.did,
862
+ address: Customer.formatAddressFromCustomer(customer),
863
+ },
864
+ {
865
+ headers: {
866
+ cookie: req.headers.cookie || '',
867
+ },
868
+ }
869
+ );
863
870
  logger.info('updateUserAddress success', {
864
871
  did: customer.did,
865
872
  });
@@ -870,14 +877,14 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
870
877
  });
871
878
  }
872
879
  } else {
873
- const updates: Record<string, string> = {};
880
+ const updates: Record<string, any> = {};
874
881
  if (checkoutSession.customer_update?.name) {
875
882
  updates.name = req.body.customer_name;
876
883
  updates.email = req.body.customer_email;
877
884
  updates.phone = req.body.customer_phone;
878
885
  }
879
886
  if (checkoutSession.customer_update?.address) {
880
- updates.address = req.body.billing_address;
887
+ updates.address = Customer.formatUpdateAddress(req.body.billing_address, customer);
881
888
  }
882
889
  if (!customer.invoice_prefix) {
883
890
  updates.invoice_prefix = Customer.getInvoicePrefix();
@@ -885,12 +892,19 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
885
892
 
886
893
  await customer.update(updates);
887
894
  try {
888
- await blocklet.updateUserAddress({
889
- did: customer.did,
890
- address: Customer.formatAddressFromCustomer(customer),
891
- // @ts-ignore
892
- phone: customer.phone,
893
- });
895
+ await blocklet.updateUserAddress(
896
+ {
897
+ did: customer.did,
898
+ address: Customer.formatAddressFromCustomer(customer),
899
+ // @ts-ignore
900
+ phone: customer.phone,
901
+ },
902
+ {
903
+ headers: {
904
+ cookie: req.headers.cookie || '',
905
+ },
906
+ }
907
+ );
894
908
  logger.info('updateUserAddress success', {
895
909
  did: customer.did,
896
910
  });
@@ -6,7 +6,7 @@ import { getGasPayerExtra } from '../../libs/payment';
6
6
  import { getTxMetadata } from '../../libs/util';
7
7
  import { ensureAccountRecharge, getAuthPrincipalClaim } from './shared';
8
8
  import logger from '../../libs/logger';
9
- import { ensureRechargeInvoice } from '../../libs/invoice';
9
+ import { ensureRechargeInvoice, retryUncollectibleInvoices } from '../../libs/invoice';
10
10
 
11
11
  export default {
12
12
  action: 'recharge-account',
@@ -86,6 +86,18 @@ export default {
86
86
  paymentMethod,
87
87
  customer!
88
88
  );
89
+ try {
90
+ retryUncollectibleInvoices({
91
+ customerId: customer.id,
92
+ currencyId: paymentCurrency.id,
93
+ });
94
+ } catch (err) {
95
+ logger.error('Failed to retry uncollectible invoices', {
96
+ error: err,
97
+ customerId: customer.id,
98
+ currencyId: paymentCurrency.id,
99
+ });
100
+ }
89
101
  };
90
102
  if (paymentMethod.type === 'arcblock') {
91
103
  try {