payment-kit 1.15.20 → 1.15.22

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 (52) hide show
  1. package/api/src/crons/base.ts +69 -7
  2. package/api/src/crons/subscription-trial-will-end.ts +20 -5
  3. package/api/src/crons/subscription-will-canceled.ts +22 -6
  4. package/api/src/crons/subscription-will-renew.ts +13 -4
  5. package/api/src/index.ts +4 -1
  6. package/api/src/integrations/arcblock/stake.ts +27 -0
  7. package/api/src/libs/audit.ts +4 -1
  8. package/api/src/libs/context.ts +48 -0
  9. package/api/src/libs/invoice.ts +2 -2
  10. package/api/src/libs/middleware.ts +39 -1
  11. package/api/src/libs/notification/template/subscription-canceled.ts +4 -0
  12. package/api/src/libs/notification/template/subscription-trial-will-end.ts +12 -34
  13. package/api/src/libs/notification/template/subscription-will-canceled.ts +82 -48
  14. package/api/src/libs/notification/template/subscription-will-renew.ts +16 -45
  15. package/api/src/libs/time.ts +13 -0
  16. package/api/src/libs/util.ts +17 -0
  17. package/api/src/locales/en.ts +12 -2
  18. package/api/src/locales/zh.ts +11 -2
  19. package/api/src/queues/checkout-session.ts +15 -0
  20. package/api/src/queues/event.ts +13 -4
  21. package/api/src/queues/invoice.ts +21 -3
  22. package/api/src/queues/payment.ts +3 -0
  23. package/api/src/queues/refund.ts +3 -0
  24. package/api/src/queues/subscription.ts +107 -2
  25. package/api/src/queues/usage-record.ts +4 -0
  26. package/api/src/queues/webhook.ts +9 -0
  27. package/api/src/routes/checkout-sessions.ts +40 -2
  28. package/api/src/routes/connect/recharge.ts +143 -0
  29. package/api/src/routes/connect/shared.ts +25 -0
  30. package/api/src/routes/customers.ts +2 -2
  31. package/api/src/routes/donations.ts +5 -1
  32. package/api/src/routes/events.ts +9 -4
  33. package/api/src/routes/payment-links.ts +40 -20
  34. package/api/src/routes/prices.ts +17 -4
  35. package/api/src/routes/products.ts +21 -2
  36. package/api/src/routes/refunds.ts +20 -3
  37. package/api/src/routes/subscription-items.ts +39 -2
  38. package/api/src/routes/subscriptions.ts +77 -40
  39. package/api/src/routes/usage-records.ts +29 -0
  40. package/api/src/store/models/event.ts +1 -0
  41. package/api/src/store/models/subscription.ts +2 -0
  42. package/api/tests/libs/time.spec.ts +54 -0
  43. package/blocklet.yml +1 -1
  44. package/package.json +19 -19
  45. package/src/app.tsx +10 -0
  46. package/src/components/subscription/actions/cancel.tsx +30 -9
  47. package/src/components/subscription/actions/index.tsx +11 -3
  48. package/src/components/webhook/attempts.tsx +122 -3
  49. package/src/locales/en.tsx +13 -0
  50. package/src/locales/zh.tsx +13 -0
  51. package/src/pages/customer/recharge.tsx +417 -0
  52. package/src/pages/customer/subscription/detail.tsx +38 -20
@@ -12,7 +12,7 @@ import { getCustomerInvoicePageUrl } from '../../invoice';
12
12
  import logger from '../../logger';
13
13
  import { getMainProductName } from '../../product';
14
14
  import { getCustomerSubscriptionPageUrl } from '../../subscription';
15
- import { formatTime } from '../../time';
15
+ import { formatTime, getSimplifyDuration } from '../../time';
16
16
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
17
17
  import { getSubscriptionNotificationCustomActions } from '../../util';
18
18
 
@@ -27,7 +27,8 @@ interface SubscriptionWillCanceledEmailTemplateContext {
27
27
  locale: string;
28
28
  productName: string;
29
29
  at: string;
30
- willCancelDuration: string;
30
+ cancelReason: string;
31
+ body: string;
31
32
 
32
33
  userDid: string;
33
34
  paymentInfo: string;
@@ -35,6 +36,7 @@ interface SubscriptionWillCanceledEmailTemplateContext {
35
36
  viewSubscriptionLink: string;
36
37
  viewInvoiceLink: string;
37
38
  customActions: any[];
39
+ needRenew: boolean;
38
40
  }
39
41
 
40
42
  export class SubscriptionWillCanceledEmailTemplate
@@ -56,8 +58,19 @@ export class SubscriptionWillCanceledEmailTemplate
56
58
  if (!subscription) {
57
59
  throw new Error(`Subscription(${this.options.subscriptionId}) not found`);
58
60
  }
59
- if (subscription.status !== 'past_due') {
60
- throw new Error(`Subscription(${this.options.subscriptionId}) status(${subscription.status}) must be past_due`);
61
+ if (subscription.isImmutable()) {
62
+ throw new Error(`Subscription(${this.options.subscriptionId}) is immutable, no need to send notification`);
63
+ }
64
+
65
+ const now = dayjs().unix();
66
+ const cancelAt = subscription.cancel_at || subscription.current_period_end;
67
+ if (!subscription.cancel_at && !subscription.cancel_at_period_end) {
68
+ throw new Error(
69
+ `Subscription(${this.options.subscriptionId}) is not scheduled to cancel, no need to send notification`
70
+ );
71
+ }
72
+ if (cancelAt <= now) {
73
+ throw new Error(`Subscription(${this.options.subscriptionId}) is already canceled, no need to send notification`);
61
74
  }
62
75
 
63
76
  const customer = await Customer.findByPk(subscription.customer_id);
@@ -75,12 +88,39 @@ export class SubscriptionWillCanceledEmailTemplate
75
88
  const userDid = customer.did;
76
89
  const locale = await getUserLocale(userDid);
77
90
  const productName = await getMainProductName(subscription.id);
78
- const at: string = formatTime(invoice.period_end * 1000);
79
- const willCancelDuration: string =
80
- locale === 'en' ? this.getWillCancelDuration(locale) : this.getWillCancelDuration(locale).split(' ').join('');
81
-
91
+ const at: string = formatTime(cancelAt * 1000);
92
+ const willCancelDuration: string = getSimplifyDuration((cancelAt - now) * 1000, locale);
82
93
  const paymentInfo: string = `${fromUnitToToken(+invoice.total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
83
94
 
95
+ let body: string = translate('notification.subscriptWillCanceled.body', locale, {
96
+ productName,
97
+ willCancelDuration,
98
+ at,
99
+ });
100
+
101
+ let needRenew = false;
102
+ const reasonMap = {
103
+ cancellation_requested: 'customerCanceled',
104
+ payment_failed: 'paymentFailed',
105
+ stake_revoked: 'stakeRevoked',
106
+ } as const;
107
+
108
+ const cancelReason = translate(
109
+ `notification.subscriptWillCanceled.${reasonMap[subscription.cancelation_details?.reason as keyof typeof reasonMap] || 'adminCanceled'}`,
110
+ locale,
111
+ {
112
+ canceled_at: formatTime(subscription.canceled_at ? subscription.canceled_at * 1000 : dayjs().unix()),
113
+ }
114
+ );
115
+ if (subscription.status === 'past_due' || subscription.cancelation_details?.reason === 'payment_failed') {
116
+ body = translate('notification.subscriptWillCanceled.pastDue', locale, {
117
+ productName,
118
+ willCancelDuration,
119
+ at,
120
+ });
121
+ needRenew = true;
122
+ }
123
+
84
124
  const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
85
125
  subscriptionId: subscription.id,
86
126
  locale,
@@ -102,7 +142,8 @@ export class SubscriptionWillCanceledEmailTemplate
102
142
  locale,
103
143
  productName,
104
144
  at,
105
- willCancelDuration,
145
+ body,
146
+ cancelReason,
106
147
 
107
148
  userDid,
108
149
  paymentInfo,
@@ -110,41 +151,18 @@ export class SubscriptionWillCanceledEmailTemplate
110
151
  viewSubscriptionLink,
111
152
  viewInvoiceLink,
112
153
  customActions,
154
+ needRenew,
113
155
  };
114
156
  }
115
157
 
116
- getWillCancelDuration(locale: string): string {
117
- if (this.options.willCancelUnit === 'M') {
118
- if (this.options.willCancelValue > 1) {
119
- return `${this.options.willCancelValue} ${translate('notification.common.months', locale)}`;
120
- }
121
-
122
- return `${this.options.willCancelValue} ${translate('notification.common.month', locale)}`;
123
- }
124
- if (this.options.willCancelUnit === 'd') {
125
- if (this.options.willCancelValue > 1) {
126
- return `${this.options.willCancelValue} ${translate('notification.common.days', locale)}`;
127
- }
128
- return `${this.options.willCancelValue} ${translate('notification.common.day', locale)}`;
129
- }
130
-
131
- if (this.options.willCancelUnit === 'm') {
132
- if (this.options.willCancelValue > 1) {
133
- return `${this.options.willCancelValue} ${translate('notification.common.minutes', locale)}`;
134
- }
135
- return `${this.options.willCancelValue} ${translate('notification.common.minute', locale)}`;
136
- }
137
-
138
- return `${this.options.willCancelValue} ${this.options.willCancelUnit}`;
139
- }
140
-
141
158
  async getTemplate(): Promise<BaseEmailTemplateType | null> {
142
159
  const {
143
160
  locale,
144
161
  productName,
145
162
  at,
146
- willCancelDuration,
147
-
163
+ body,
164
+ cancelReason,
165
+ needRenew,
148
166
  userDid,
149
167
  paymentInfo,
150
168
 
@@ -162,11 +180,7 @@ export class SubscriptionWillCanceledEmailTemplate
162
180
  title: `${translate('notification.subscriptWillCanceled.title', locale, {
163
181
  productName,
164
182
  })}`,
165
- body: translate('notification.subscriptWillCanceled.body', locale, {
166
- productName,
167
- willCancelDuration,
168
- at,
169
- }),
183
+ body,
170
184
  // @ts-expect-error
171
185
  attachments: [
172
186
  {
@@ -202,19 +216,38 @@ export class SubscriptionWillCanceledEmailTemplate
202
216
  text: productName,
203
217
  },
204
218
  },
219
+ ...(needRenew
220
+ ? [
221
+ {
222
+ type: 'text',
223
+ data: {
224
+ type: 'plain',
225
+ color: '#9397A1',
226
+ text: translate('notification.common.paymentAmount', locale),
227
+ },
228
+ },
229
+ {
230
+ type: 'text',
231
+ data: {
232
+ type: 'plain',
233
+ text: paymentInfo,
234
+ },
235
+ },
236
+ ]
237
+ : []),
205
238
  {
206
239
  type: 'text',
207
240
  data: {
208
241
  type: 'plain',
209
242
  color: '#9397A1',
210
- text: translate('notification.common.paymentAmount', locale),
243
+ text: translate('notification.subscriptWillCanceled.cancelReason', locale),
211
244
  },
212
245
  },
213
246
  {
214
247
  type: 'text',
215
248
  data: {
216
249
  type: 'plain',
217
- text: paymentInfo,
250
+ text: cancelReason,
218
251
  },
219
252
  },
220
253
  ].filter(Boolean),
@@ -227,11 +260,12 @@ export class SubscriptionWillCanceledEmailTemplate
227
260
  title: translate('notification.common.viewSubscription', locale),
228
261
  link: viewSubscriptionLink,
229
262
  },
230
- viewInvoiceLink && {
231
- name: translate('notification.common.renewNow', locale),
232
- title: translate('notification.common.renewNow', locale),
233
- link: viewInvoiceLink,
234
- },
263
+ viewInvoiceLink &&
264
+ needRenew && {
265
+ name: translate('notification.common.renewNow', locale),
266
+ title: translate('notification.common.renewNow', locale),
267
+ link: viewInvoiceLink,
268
+ },
235
269
  ...customActions,
236
270
  ].filter(Boolean),
237
271
  };
@@ -21,8 +21,8 @@ import {
21
21
  import { getPaymentAmountForCycleSubscription, type PaymentDetail } from '../../payment';
22
22
  import { getMainProductName } from '../../product';
23
23
  import { getCustomerSubscriptionPageUrl } from '../../subscription';
24
- import { formatTime, getPrettyMsI18nLocale } from '../../time';
25
- import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
24
+ import { formatTime, getPrettyMsI18nLocale, getSimplifyDuration } from '../../time';
25
+ import { getCustomerRechargeLink, getSubscriptionNotificationCustomActions } from '../../util';
26
26
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
27
27
 
28
28
  export interface SubscriptionWillRenewEmailTemplateOptions {
@@ -92,9 +92,7 @@ export class SubscriptionWillRenewEmailTemplate
92
92
  const locale = await getUserLocale(userDid);
93
93
  const productName = await getMainProductName(subscription.id);
94
94
  const at: string = formatTime(invoice.period_end * 1000);
95
- const willRenewDuration: string =
96
- locale === 'en' ? this.getWillRenewDuration(locale) : this.getWillRenewDuration(locale).split(' ').join('');
97
-
95
+ const willRenewDuration = getSimplifyDuration((invoice.period_end - dayjs().unix()) * 1000, locale);
98
96
  // const upcomingInvoiceAmount = await getUpcomingInvoiceAmount(subscription.id);
99
97
  // const amount: string = fromUnitToToken(+upcomingInvoiceAmount.amount, upcomingInvoiceAmount.currency?.decimal);
100
98
  // const paymentDetail: PaymentDetail = await getPaymentDetail(userDid, invoice, amount);
@@ -135,16 +133,12 @@ export class SubscriptionWillRenewEmailTemplate
135
133
  userDid,
136
134
  });
137
135
  const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
138
- // @ts-ignore
139
- const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
140
- const addFundsLink: string = getExplorerLink({
141
- type: 'account',
142
- did: userDid,
143
- chainHost,
144
- queryParams: {
145
- action: 'recharge',
146
- },
147
- })!;
136
+
137
+ const addFundsLink: string = getCustomerRechargeLink({
138
+ locale,
139
+ userDid,
140
+ subscriptionId: subscription.id,
141
+ });
148
142
 
149
143
  const customActions = getSubscriptionNotificationCustomActions(
150
144
  subscription,
@@ -213,31 +207,6 @@ export class SubscriptionWillRenewEmailTemplate
213
207
  return currentPeriodEnd > expectedCurrentPeriodEnd;
214
208
  }
215
209
 
216
- getWillRenewDuration(locale: string): string {
217
- if (this.options.willRenewUnit === 'M') {
218
- if (this.options.willRenewValue > 1) {
219
- return `${this.options.willRenewValue} ${translate('notification.common.months', locale)}`;
220
- }
221
-
222
- return `${this.options.willRenewValue} ${translate('notification.common.month', locale)}`;
223
- }
224
- if (this.options.willRenewUnit === 'd') {
225
- if (this.options.willRenewValue > 1) {
226
- return `${this.options.willRenewValue} ${translate('notification.common.days', locale)}`;
227
- }
228
- return `${this.options.willRenewValue} ${translate('notification.common.day', locale)}`;
229
- }
230
-
231
- if (this.options.willRenewUnit === 'm') {
232
- if (this.options.willRenewValue > 1) {
233
- return `${this.options.willRenewValue} ${translate('notification.common.minutes', locale)}`;
234
- }
235
- return `${this.options.willRenewValue} ${translate('notification.common.minute', locale)}`;
236
- }
237
-
238
- return `${this.options.willRenewValue} ${this.options.willRenewUnit}`;
239
- }
240
-
241
210
  async getTemplate(): Promise<BaseEmailTemplateType | null> {
242
211
  const {
243
212
  locale,
@@ -421,11 +390,13 @@ export class SubscriptionWillRenewEmailTemplate
421
390
  ].filter(Boolean),
422
391
  // @ts-ignore
423
392
  actions: [
424
- !canPay && {
425
- name: translate('notification.common.addFunds', locale),
426
- title: translate('notification.common.addFunds', locale),
427
- link: addFundsLink,
428
- },
393
+ !canPay &&
394
+ !isStripe &&
395
+ addFundsLink && {
396
+ name: translate('notification.common.addFunds', locale),
397
+ title: translate('notification.common.addFunds', locale),
398
+ link: addFundsLink,
399
+ },
429
400
  {
430
401
  name: translate('notification.common.viewSubscription', locale),
431
402
  title: translate('notification.common.viewSubscription', locale),
@@ -1,5 +1,6 @@
1
1
  import type { LiteralUnion } from 'type-fest';
2
2
 
3
+ import prettyMsI18n from 'pretty-ms-i18n';
3
4
  import dayjs from './dayjs';
4
5
 
5
6
  export function formatTime(time: dayjs.ConfigType): string {
@@ -15,3 +16,15 @@ export function getPrettyMsI18nLocale(
15
16
 
16
17
  return 'en';
17
18
  }
19
+
20
+ export function getSimplifyDuration(ms: number, locale: string): string {
21
+ const options = {
22
+ locale: getPrettyMsI18nLocale(locale),
23
+ compact: true,
24
+ verbose: true,
25
+ };
26
+ if (ms < 1000 && ms >= 0) {
27
+ options.verbose = false;
28
+ }
29
+ return prettyMsI18n(ms, options);
30
+ }
@@ -292,6 +292,23 @@ export function getCustomerProfileUrl({
292
292
  );
293
293
  }
294
294
 
295
+ export function getCustomerRechargeLink({
296
+ locale = 'en',
297
+ userDid,
298
+ subscriptionId,
299
+ }: {
300
+ locale: LiteralUnion<'en' | 'zh', string>;
301
+ userDid: string;
302
+ subscriptionId: string;
303
+ }) {
304
+ return getUrl(
305
+ withQuery(`customer/subscription/${subscriptionId}/recharge`, {
306
+ locale,
307
+ ...getConnectQueryParam({ userDid }),
308
+ })
309
+ );
310
+ }
311
+
295
312
  export async function getOwnerDid() {
296
313
  try {
297
314
  const { user } = await blocklet.getOwner();
@@ -153,9 +153,18 @@ export default flat({
153
153
  },
154
154
 
155
155
  subscriptWillCanceled: {
156
- title: '{productName} subscription is about to be automatically cancelled ',
157
- body: 'Your subscription {productName} will be automatically unsubscribed by the system after {at} (after {willCancelDuration}) due to a long period of failure to automatically complete the automatic payment. Please handle the problem of automatic payment manually in time, so as not to affect the use. If you have any questions, please feel free to contact us.',
156
+ title: '{productName} subscription is about to be cancelled ',
157
+ pastDue:
158
+ 'Your subscription {productName} will be automatically unsubscribed by the system after {at} (after {willCancelDuration}) due to a long period of failure to automatically complete the automatic payment. Please handle the problem of automatic payment manually in time, so as not to affect the use. If you have any questions, please feel free to contact us.',
159
+ body: 'Your subscription to {productName} will be automatically canceled on {at} ({willCancelDuration} later). If you have any questions, please feel free to contact us.',
158
160
  renewAmount: 'deduction amount ',
161
+ cancelReason: 'Cancel reason',
162
+ revokeStake: 'Revoke stake',
163
+ adminCanceled: 'Admin canceled',
164
+ customerCanceled: 'User-initiated cancellation',
165
+ paymentDisputed: 'Payment disputed',
166
+ paymentFailed: 'Payment failed',
167
+ stakeRevoked: 'Stake revoked',
159
168
  },
160
169
 
161
170
  subscriptionCanceled: {
@@ -173,6 +182,7 @@ export default flat({
173
182
  customerCanceledAndStakeReturned:
174
183
  'User-initiated cancellation, the stake will be returned later, please check for the stake return email',
175
184
  paymentFailed: 'Payment failed',
185
+ stakeRevoked: 'Stake revoked',
176
186
  },
177
187
  },
178
188
  });
@@ -150,9 +150,17 @@ export default flat({
150
150
  },
151
151
 
152
152
  subscriptWillCanceled: {
153
- title: '{productName} 订阅即将自动取消',
154
- body: '由于长时间未能自动完成扣费,您订阅的 {productName} 将于 {at} ({willCancelDuration}后) 被系统自动取消订阅。请您及时手动处理扣费问题,以免影响使用。如有任何疑问,请随时与我们联系。',
153
+ title: '{productName} 订阅即将取消',
154
+ pastDue:
155
+ '由于长时间未能自动完成扣费,您订阅的 {productName} 将于 {at} ({willCancelDuration}后) 被系统自动取消订阅。请您及时手动处理扣费问题,以免影响使用。如有任何疑问,请随时与我们联系。',
156
+ body: '您订阅的 {productName} 将于 {at} ({willCancelDuration}后) 被系统取消订阅,如有疑问,请随时与我们联系。',
155
157
  renewAmount: '扣费金额',
158
+ cancelReason: '取消原因',
159
+ adminCanceled: '管理员取消',
160
+ customerCanceled: '用户于 {canceled_at} 主动取消',
161
+ paymentDisputed: '扣费争议',
162
+ paymentFailed: '扣费失败',
163
+ stakeRevoked: '撤销质押',
156
164
  },
157
165
 
158
166
  subscriptionCanceled: {
@@ -168,6 +176,7 @@ export default flat({
168
176
  customerCanceled: '用户主动取消',
169
177
  customerCanceledAndStakeReturned: '用户主动取消, 押金会在稍后退还, 请留意后续的质押退还邮件',
170
178
  paymentFailed: '扣费失败',
179
+ stakeRevoked: '撤销质押',
171
180
  },
172
181
  },
173
182
  });
@@ -39,10 +39,15 @@ export const checkoutSessionQueue = createQueue<CheckoutSessionJob>({
39
39
  export async function handleCheckoutSessionJob(job: CheckoutSessionJob): Promise<void> {
40
40
  const checkoutSession = await CheckoutSession.findByPk(job.id);
41
41
  if (!checkoutSession) {
42
+ logger.warn('CheckoutSession not found', { id: job.id });
42
43
  return;
43
44
  }
44
45
  if (job.action === 'expire') {
45
46
  if (checkoutSession.status !== 'open') {
47
+ logger.info('Skip expire CheckoutSession since status is not open', {
48
+ checkoutSession: checkoutSession.id,
49
+ status: checkoutSession.status,
50
+ });
46
51
  return;
47
52
  }
48
53
  if (checkoutSession.payment_status === 'paid') {
@@ -257,15 +262,25 @@ events.on(
257
262
  async ({ checkoutSessionId, paymentIntentId }: { checkoutSessionId: string; paymentIntentId: string }) => {
258
263
  const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
259
264
  if (!checkoutSession) {
265
+ logger.warn('CheckoutSession not found for pending invoice', { checkoutSessionId });
260
266
  return;
261
267
  }
262
268
  if (checkoutSession.invoice_id) {
269
+ logger.info('Invoice already exists for checkout session', {
270
+ checkoutSessionId,
271
+ invoiceId: checkoutSession.invoice_id,
272
+ });
263
273
  return;
264
274
  }
265
275
  if (checkoutSession.mode !== 'payment') {
276
+ logger.info('Skipping invoice creation for non-payment mode', {
277
+ checkoutSessionId,
278
+ mode: checkoutSession.mode,
279
+ });
266
280
  return;
267
281
  }
268
282
  if (!checkoutSession.invoice_creation?.enabled) {
283
+ logger.info('Invoice creation not enabled for checkout session', { checkoutSessionId });
269
284
  return;
270
285
  }
271
286
 
@@ -14,16 +14,16 @@ type EventJob = {
14
14
  };
15
15
 
16
16
  export const handleEvent = async (job: EventJob) => {
17
- logger.info('handle event', job);
17
+ logger.info('Starting to handle event', job);
18
18
 
19
19
  const event = await Event.findByPk(job.eventId);
20
20
  if (!event) {
21
- logger.warn('event not found', job);
21
+ logger.warn('Event not found', job);
22
22
  return;
23
23
  }
24
24
 
25
25
  if (!event.pending_webhooks) {
26
- logger.warn('event already processed', job);
26
+ logger.warn('Event already processed', job);
27
27
  return;
28
28
  }
29
29
 
@@ -36,6 +36,8 @@ export const handleEvent = async (job: EventJob) => {
36
36
  }
37
37
 
38
38
  await event.update({ pending_webhooks: eventWebhooks.length });
39
+ logger.info(`Updated event ${event.id} with ${eventWebhooks.length} pending webhooks`);
40
+
39
41
  eventWebhooks.forEach(async (webhook) => {
40
42
  const attemptCount = await WebhookAttempt.count({
41
43
  where: {
@@ -53,7 +55,7 @@ export const handleEvent = async (job: EventJob) => {
53
55
  const jobId = getWebhookJobId(event.id, webhook.id);
54
56
  const exist = await webhookQueue.get(jobId);
55
57
  if (!exist) {
56
- logger.info('schedule attempt for event', job);
58
+ logger.info(`Scheduling attempt for event ${event.id} and webhook ${webhook.id}`, job);
57
59
  webhookQueue.push({
58
60
  id: jobId,
59
61
  job: { eventId: event.id, webhookId: webhook.id },
@@ -61,6 +63,8 @@ export const handleEvent = async (job: EventJob) => {
61
63
  }
62
64
  }
63
65
  });
66
+
67
+ logger.info(`Finished handling event ${job.eventId}`);
64
68
  };
65
69
 
66
70
  export const eventQueue = createQueue<EventJob>({
@@ -80,12 +84,17 @@ export const startEventQueue = async () => {
80
84
  attributes: ['id'],
81
85
  });
82
86
 
87
+ logger.info(`Found ${docs.length} events with pending webhooks`);
88
+
83
89
  docs.forEach(async (x) => {
84
90
  const exist = await eventQueue.get(x.id);
85
91
  if (!exist) {
92
+ logger.info(`Pushing event ${x.id} to queue`);
86
93
  eventQueue.push({ id: x.id, job: { eventId: x.id } });
87
94
  }
88
95
  });
96
+
97
+ logger.info('Finished starting event queue');
89
98
  };
90
99
 
91
100
  eventQueue.on('failed', ({ id, job, error }) => {
@@ -57,6 +57,7 @@ export const handleInvoice = async (job: InvoiceJob) => {
57
57
  attempted: true,
58
58
  status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
59
59
  });
60
+ logger.info('Invoice updated to paid status', { invoiceId: invoice.id });
60
61
 
61
62
  if (invoice.subscription_id) {
62
63
  const subscription = await Subscription.findByPk(invoice.subscription_id);
@@ -118,6 +119,11 @@ export const handleInvoice = async (job: InvoiceJob) => {
118
119
  paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
119
120
  if (paymentIntent && paymentIntent.isImmutable() === false) {
120
121
  await paymentIntent.update({ status: 'requires_capture', customer_id: invoice.customer_id });
122
+ logger.info('PaymentIntent updated for invoice', {
123
+ invoiceId: invoice.id,
124
+ paymentIntentId: paymentIntent.id,
125
+ newStatus: 'requires_capture',
126
+ });
121
127
  }
122
128
  } else {
123
129
  const descriptionMap: any = {
@@ -149,7 +155,11 @@ export const handleInvoice = async (job: InvoiceJob) => {
149
155
  metadata: {},
150
156
  });
151
157
  await invoice.update({ payment_intent_id: paymentIntent.id });
152
- logger.info('PaymentIntent created for invoice', { invoice: invoice.id, paymentIntent: paymentIntent.id });
158
+ logger.info('PaymentIntent created for invoice', {
159
+ invoiceId: invoice.id,
160
+ paymentIntentId: paymentIntent.id,
161
+ amount: paymentIntent.amount,
162
+ });
153
163
 
154
164
  if (invoice.checkout_session_id) {
155
165
  const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
@@ -246,15 +256,23 @@ invoiceQueue.on('failed', ({ id, job, error }) => {
246
256
  events.on('invoice.paid', async ({ id: invoiceId }) => {
247
257
  const invoice = await Invoice.findByPk(invoiceId);
248
258
  if (!invoice) {
249
- logger.error('Invoice not found', { invoiceId });
259
+ logger.error('Invoice not found for paid event', { invoiceId });
250
260
  return;
251
261
  }
262
+ logger.info('Processing paid invoice', { invoiceId, billingReason: invoice.billing_reason });
263
+
252
264
  const checkBillingReason = ['subscription_cycle', 'subscription_cancel'];
253
265
  if (checkBillingReason.includes(invoice.billing_reason)) {
254
266
  const shouldPayTotal = await getInvoiceShouldPayTotal(invoice);
255
267
  if (shouldPayTotal !== invoice.total) {
256
268
  createEvent('Invoice', 'billing.discrepancy', invoice);
257
- logger.info('create billing discrepancy event', { invoiceId, shouldPayTotal, invoiceTotal: invoice.total });
269
+ logger.warn('Billing discrepancy detected', {
270
+ invoiceId,
271
+ shouldPayTotal,
272
+ invoiceTotal: invoice.total,
273
+ });
274
+ } else {
275
+ logger.info('Invoice paid successfully with correct amount', { invoiceId, total: invoice.total });
258
276
  }
259
277
  }
260
278
  });
@@ -577,6 +577,9 @@ export const handlePayment = async (job: PaymentJob) => {
577
577
  let result;
578
578
  try {
579
579
  await paymentIntent.update({ status: 'processing', last_payment_error: null });
580
+ logger.info('PaymentIntent status updated to processing', {
581
+ paymentIntentId: paymentIntent.id,
582
+ });
580
583
  if (paymentMethod.type === 'arcblock') {
581
584
  if (invoice?.billing_reason === 'slash_stake') {
582
585
  await handleStakeSlash(invoice, paymentIntent, paymentMethod, customer, paymentCurrency);
@@ -175,6 +175,7 @@ const handleRefundJob = async (
175
175
  },
176
176
  },
177
177
  });
178
+ logger.info('Refund status updated to succeeded', { id: refund.id, txHash });
178
179
  }
179
180
 
180
181
  if (paymentMethod.type === 'ethereum') {
@@ -376,6 +377,7 @@ const handleStakeReturnJob = async (
376
377
  },
377
378
  },
378
379
  });
380
+ logger.info('Stake return refund status updated to succeeded', { id: refund.id, txHash });
379
381
  }
380
382
  } catch (err: any) {
381
383
  logger.error('stake return failed', { error: err, id: refund.id });
@@ -423,6 +425,7 @@ export const startRefundQueue = async () => {
423
425
  const exist = await refundQueue.get(x.id);
424
426
  if (!exist) {
425
427
  refundQueue.push({ id: x.id, job: { refundId: x.id } });
428
+ logger.info('Re-queued pending refund', { id: x.id });
426
429
  }
427
430
  });
428
431
  };