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
@@ -34,13 +34,19 @@ export abstract class BaseSubscriptionScheduleNotification<Options extends any>
34
34
  const subscriptions = await this.getSubscriptions();
35
35
  logger.info(`${name}.run.${subscriptions.length}`, subscriptions.length);
36
36
 
37
- const subscriptionForWillRenew = this.getSubscriptionsForWillRenew(subscriptions);
37
+ const scheduleSubscriptions = this.getScheduleSubscriptions(subscriptions);
38
38
 
39
- await this.addTaskToQueue(subscriptionForWillRenew);
39
+ await this.addTaskToQueue(scheduleSubscriptions);
40
40
 
41
41
  logger.info(`${name}.run end`);
42
42
  }
43
43
 
44
+ async reScheduleSubscriptionTasks(subscriptions: Subscription[]): Promise<void> {
45
+ await this.deleteScheduleSubscriptionJobs(subscriptions);
46
+ const scheduleSubscriptions = this.getScheduleSubscriptions(subscriptions);
47
+ await this.addTaskToQueue(scheduleSubscriptions);
48
+ }
49
+
44
50
  static readonly DIFFS: Diff[] = [
45
51
  {
46
52
  value: 1,
@@ -67,6 +73,8 @@ export abstract class BaseSubscriptionScheduleNotification<Options extends any>
67
73
  unit: 'm',
68
74
  },
69
75
  ];
76
+
77
+ abstract getEndTime(subscription: Subscription): number;
70
78
  /**
71
79
  * @see https://github.com/blocklet/payment-kit/issues/236#issuecomment-1824129965
72
80
  * @description
@@ -74,13 +82,13 @@ export abstract class BaseSubscriptionScheduleNotification<Options extends any>
74
82
  * @return {*} {SubscriptionForWillRenew[]}
75
83
  * @memberof SubscriptionWillRenewSchedule
76
84
  */
77
- getSubscriptionsForWillRenew(subscriptions: Subscription[]): BaseSubscriptionScheduleNotificationSubscription[] {
85
+ getScheduleSubscriptions(subscriptions: Subscription[]): BaseSubscriptionScheduleNotificationSubscription[] {
78
86
  return subscriptions.map((subscription: Subscription): BaseSubscriptionScheduleNotificationSubscription => {
79
87
  const s: BaseSubscriptionScheduleNotificationSubscription = clone(
80
88
  subscription
81
89
  ) as BaseSubscriptionScheduleNotificationSubscription;
82
90
  const currentPeriodStart: number = s.current_period_start * 1000;
83
- const currentPeriodEnd: number = s.current_period_end * 1000;
91
+ const currentPeriodEnd: number = this.getEndTime(s);
84
92
 
85
93
  if (
86
94
  dayjs(currentPeriodEnd).diff(this.start, 'M') >= 1 &&
@@ -151,13 +159,26 @@ export abstract class BaseSubscriptionScheduleNotification<Options extends any>
151
159
 
152
160
  async addTaskToQueue(subscriptions: BaseSubscriptionScheduleNotificationSubscription[]): Promise<void> {
153
161
  const tasks: Task<Options>[] = [];
162
+ const deleteTasks = [];
154
163
  for (const subscription of subscriptions) {
155
164
  for (const diff of subscription.diffs) {
156
- const task: Task<Options> = this.getTask({ subscription, diff });
157
- tasks.push(task);
165
+ try {
166
+ const task: Task<Options> = this.getTask({ subscription, diff });
167
+ tasks.push(task);
168
+ } catch (error) {
169
+ logger.error('getTask error', { error, subscriptionId: subscription.id, diff });
170
+ deleteTasks.push(() => this.deleteScheduleSubscriptionJobs([subscription]));
171
+ }
158
172
  }
159
173
  }
160
174
 
175
+ await pAll(
176
+ deleteTasks.map((task) => () => task()),
177
+ {
178
+ concurrency: notificationCronConcurrency,
179
+ stopOnError: false,
180
+ }
181
+ );
161
182
  await pAll(
162
183
  tasks.map((x) => {
163
184
  return async () => {
@@ -170,7 +191,48 @@ export abstract class BaseSubscriptionScheduleNotification<Options extends any>
170
191
  notificationQueue.push(x);
171
192
  };
172
193
  }),
173
- { concurrency: notificationCronConcurrency }
194
+ {
195
+ concurrency: notificationCronConcurrency,
196
+ stopOnError: false,
197
+ }
174
198
  );
175
199
  }
200
+
201
+ async deleteScheduleSubscriptionJobs(subscriptions: Subscription[]): Promise<void> {
202
+ const jobsToDelete = await Promise.all(
203
+ subscriptions.flatMap((subscription) =>
204
+ BaseSubscriptionScheduleNotification.DIFFS.map(async (diff) => {
205
+ const jobId = `${subscription.id}.${this.eventType}.${diff.value}.${diff.unit}`;
206
+ const job = await notificationQueue.get(jobId);
207
+ return job ? { jobId, subscriptionId: subscription.id } : null;
208
+ })
209
+ )
210
+ );
211
+
212
+ const existingJobs = jobsToDelete.filter((x): x is { jobId: string; subscriptionId: string } => x !== null);
213
+
214
+ const deletePromises = existingJobs.map(
215
+ ({ jobId, subscriptionId }: { jobId: string; subscriptionId: string }) =>
216
+ async () => {
217
+ try {
218
+ const deleted = await notificationQueue.delete(jobId);
219
+ if (deleted) {
220
+ logger.info('Deleted subscription job', { jobId, subscriptionId, eventType: this.eventType });
221
+ }
222
+ } catch (error) {
223
+ logger.error('Failed to delete subscription job', {
224
+ jobId,
225
+ subscriptionId,
226
+ eventType: this.eventType,
227
+ error: error instanceof Error ? error.message : String(error),
228
+ });
229
+ }
230
+ }
231
+ );
232
+
233
+ await pAll(deletePromises, {
234
+ concurrency: notificationCronConcurrency,
235
+ stopOnError: false,
236
+ });
237
+ }
176
238
  }
@@ -17,7 +17,7 @@ export class SubscriptionTrialWillEndSchedule extends BaseSubscriptionScheduleNo
17
17
  current_period_start: {
18
18
  [Op.lte]: this.start / 1000,
19
19
  },
20
- current_period_end: {
20
+ trial_end: {
21
21
  [Op.lt]: this.end / 1000,
22
22
  },
23
23
  trial_start: {
@@ -25,7 +25,7 @@ export class SubscriptionTrialWillEndSchedule extends BaseSubscriptionScheduleNo
25
25
  },
26
26
  status: 'trialing',
27
27
  },
28
- attributes: ['id', 'current_period_start', 'current_period_end'],
28
+ attributes: ['id', 'current_period_start', 'current_period_end', 'trial_end'],
29
29
  raw: true,
30
30
  });
31
31
 
@@ -40,6 +40,19 @@ export class SubscriptionTrialWillEndSchedule extends BaseSubscriptionScheduleNo
40
40
  diff: Diff;
41
41
  }): Task<SubscriptionTrialWillEndEmailTemplateOptions> {
42
42
  const type = this.eventType;
43
+ if (subscription.status !== 'trialing') {
44
+ throw new Error(`Subscription(${subscription.id}) is not trialing, no need to send notification`);
45
+ }
46
+
47
+ const trialEnd = this.getEndTime(subscription);
48
+ if (!subscription.trial_end) {
49
+ throw new Error(`Subscription(${subscription.id}) has no trial_end, no need to send notification`);
50
+ }
51
+
52
+ if (trialEnd <= this.start) {
53
+ throw new Error(`Subscription(${subscription.id}) is already expired, no need to send notification`);
54
+ }
55
+
43
56
  return {
44
57
  id: `${subscription.id}.${type}.${diff.value}.${diff.unit}`,
45
58
  job: {
@@ -51,9 +64,11 @@ export class SubscriptionTrialWillEndSchedule extends BaseSubscriptionScheduleNo
51
64
  required: !!diff.required,
52
65
  },
53
66
  },
54
- delay: dayjs(subscription.current_period_end * 1000)
55
- .subtract(diff.value, diff.unit)
56
- .diff(this.start, 's'),
67
+ delay: dayjs(trialEnd).subtract(diff.value, diff.unit).diff(this.start, 's'),
57
68
  };
58
69
  }
70
+
71
+ override getEndTime(subscription: Subscription): number {
72
+ return subscription.trial_end! * 1000;
73
+ }
59
74
  }
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable no-console */
2
2
  import dayjs from 'dayjs';
3
3
 
4
+ import { Op } from 'sequelize';
4
5
  import type { SubscriptionWillCanceledEmailTemplateOptions } from '../libs/notification/template/subscription-will-canceled';
5
6
  import { Subscription } from '../store/models';
6
7
  import { BaseSubscriptionScheduleNotification, BaseSubscriptionScheduleNotificationEventType } from './base';
@@ -12,14 +13,20 @@ export class SubscriptionWillCanceledSchedule extends BaseSubscriptionScheduleNo
12
13
  async getSubscriptions(): Promise<Subscription[]> {
13
14
  const subscriptions = await Subscription.findAll({
14
15
  where: {
15
- status: 'past_due',
16
+ status: ['active', 'past_due'],
17
+ [Op.or]: [
18
+ { cancel_at: { [Op.gt]: this.start } },
19
+ { cancel_at_period_end: true, current_period_end: { [Op.gt]: this.start } },
20
+ ],
16
21
  },
17
- attributes: ['id', 'current_period_start', 'current_period_end'],
22
+ attributes: ['id', 'current_period_start', 'current_period_end', 'cancel_at', 'status'],
18
23
  raw: true,
19
24
  });
20
25
 
21
26
  subscriptions.forEach((subscription) => {
22
- subscription.current_period_end = subscription.cancel_at!;
27
+ if (subscription.cancel_at) {
28
+ subscription.current_period_end = subscription.cancel_at;
29
+ }
23
30
  });
24
31
 
25
32
  return subscriptions;
@@ -33,6 +40,13 @@ export class SubscriptionWillCanceledSchedule extends BaseSubscriptionScheduleNo
33
40
  diff: Diff;
34
41
  }): Task<SubscriptionWillCanceledEmailTemplateOptions> {
35
42
  const type = this.eventType;
43
+ const cancelAt = this.getEndTime(subscription);
44
+ if (!cancelAt) {
45
+ throw new Error(`Subscription(${subscription.id}) has no cancel_at, no need to send notification`);
46
+ }
47
+ if (cancelAt <= this.start) {
48
+ throw new Error(`Subscription(${subscription.id}) is already canceled, no need to send notification`);
49
+ }
36
50
  return {
37
51
  id: `${subscription.id}.${type}.${diff.value}.${diff.unit}`,
38
52
  job: {
@@ -44,9 +58,11 @@ export class SubscriptionWillCanceledSchedule extends BaseSubscriptionScheduleNo
44
58
  required: true,
45
59
  },
46
60
  },
47
- delay: dayjs(subscription.current_period_end * 1000)
48
- .subtract(diff.value, diff.unit)
49
- .diff(this.start, 's'),
61
+ delay: dayjs(cancelAt).subtract(diff.value, diff.unit).diff(this.start, 's'),
50
62
  };
51
63
  }
64
+
65
+ override getEndTime(subscription: Subscription): number {
66
+ return (subscription.cancel_at || subscription.current_period_end) * 1000;
67
+ }
52
68
  }
@@ -21,7 +21,7 @@ export class SubscriptionWillRenewSchedule extends BaseSubscriptionScheduleNotif
21
21
  },
22
22
  status: 'active',
23
23
  },
24
- attributes: ['id', 'current_period_start', 'current_period_end'],
24
+ attributes: ['id', 'current_period_start', 'current_period_end', 'status'],
25
25
  raw: true,
26
26
  });
27
27
 
@@ -36,6 +36,13 @@ export class SubscriptionWillRenewSchedule extends BaseSubscriptionScheduleNotif
36
36
  diff: Diff;
37
37
  }): Task<SubscriptionWillRenewEmailTemplateOptions> {
38
38
  const type = this.eventType;
39
+ const periodEnd = this.getEndTime(subscription);
40
+ if (periodEnd <= this.start) {
41
+ throw new Error(`Subscription(${subscription.id}) is already expired, no need to send notification`);
42
+ }
43
+ if (subscription.status !== 'active') {
44
+ throw new Error(`Subscription(${subscription.id}) is not active, no need to send notification`);
45
+ }
39
46
  return {
40
47
  id: `${subscription.id}.${type}.${diff.value}.${diff.unit}`,
41
48
  job: {
@@ -47,9 +54,11 @@ export class SubscriptionWillRenewSchedule extends BaseSubscriptionScheduleNotif
47
54
  required: !!diff.required,
48
55
  },
49
56
  },
50
- delay: dayjs(subscription.current_period_end * 1000)
51
- .subtract(diff.value, diff.unit)
52
- .diff(this.start, 's'),
57
+ delay: dayjs(periodEnd).subtract(diff.value, diff.unit).diff(this.start, 's'),
53
58
  };
54
59
  }
60
+
61
+ override getEndTime(subscription: Subscription): number {
62
+ return subscription.current_period_end * 1000;
63
+ }
55
64
  }
package/api/src/index.ts CHANGED
@@ -19,7 +19,7 @@ import { initResourceHandler } from './integrations/blocklet/resource';
19
19
  import { ensureWebhookRegistered } from './integrations/stripe/setup';
20
20
  import { handlers } from './libs/auth';
21
21
  import logger, { accessLogStream } from './libs/logger';
22
- import { ensureI18n } from './libs/middleware';
22
+ import { contextMiddleware, ensureI18n } from './libs/middleware';
23
23
  import { initEventBroadcast } from './libs/ws';
24
24
  import { startCheckoutSessionQueue } from './queues/checkout-session';
25
25
  import { startEventQueue } from './queues/event';
@@ -33,6 +33,7 @@ import changePaymentHandlers from './routes/connect/change-payment';
33
33
  import changePlanHandlers from './routes/connect/change-plan';
34
34
  import collectHandlers from './routes/connect/collect';
35
35
  import collectBatchHandlers from './routes/connect/collect-batch';
36
+ import rechargeHandlers from './routes/connect/recharge';
36
37
  import payHandlers from './routes/connect/pay';
37
38
  import setupHandlers from './routes/connect/setup';
38
39
  import subscribeHandlers from './routes/connect/subscribe';
@@ -69,6 +70,7 @@ handlers.attach(Object.assign({ app: router }, setupHandlers));
69
70
  handlers.attach(Object.assign({ app: router }, subscribeHandlers));
70
71
  handlers.attach(Object.assign({ app: router }, changePaymentHandlers));
71
72
  handlers.attach(Object.assign({ app: router }, changePlanHandlers));
73
+ handlers.attach(Object.assign({ app: router }, rechargeHandlers));
72
74
 
73
75
  router.use('/api', routes);
74
76
 
@@ -91,6 +93,7 @@ if (isProduction) {
91
93
  });
92
94
  }
93
95
 
96
+ app.use(contextMiddleware);
94
97
  app.use(router);
95
98
 
96
99
  if (isProduction) {
@@ -261,3 +261,30 @@ export async function getTokenSummaryByDid(
261
261
 
262
262
  return results;
263
263
  }
264
+
265
+ export async function getTokenByAddress(
266
+ address: string,
267
+ paymentMethod: PaymentMethod,
268
+ paymentCurrency: PaymentCurrency
269
+ ): Promise<string | undefined> {
270
+ if (paymentMethod.type === 'arcblock') {
271
+ const client = paymentMethod.getOcapClient();
272
+ const { tokens } = await client.getAccountTokens({ address });
273
+ return tokens.find((t: any) => t.address === paymentCurrency.contract)?.balance;
274
+ }
275
+ if (paymentMethod.type === 'ethereum') {
276
+ const client = paymentMethod.getEvmClient();
277
+ if (paymentCurrency.contract) {
278
+ const token = await fetchErc20Balance(client, paymentCurrency.contract, address);
279
+ return token;
280
+ }
281
+ const token = await fetchEtherBalance(client, address);
282
+ return token;
283
+ }
284
+ logger.info(`unsupported payment method type: ${paymentMethod.type}`, {
285
+ paymentCurrencyId: paymentCurrency.id,
286
+ paymentMethodId: paymentMethod.id,
287
+ address,
288
+ });
289
+ return '0';
290
+ }
@@ -4,6 +4,7 @@ import type { LiteralUnion } from 'type-fest';
4
4
  import type { EventType } from '../store/models';
5
5
  import { Event } from '../store/models/event';
6
6
  import { events } from './event';
7
+ import { context } from './context';
7
8
 
8
9
  const API_VERSION = '2023-09-05';
9
10
 
@@ -15,7 +16,6 @@ export async function createEvent(scope: string, type: LiteralUnion<EventType, s
15
16
  data.previous_attributes = pick(model._previousDataValues, options.fields);
16
17
  }
17
18
  // console.log('createEvent', scope, type, data, options);
18
-
19
19
  const event = await Event.create({
20
20
  type,
21
21
  api_version: API_VERSION,
@@ -27,6 +27,7 @@ export async function createEvent(scope: string, type: LiteralUnion<EventType, s
27
27
  // FIXME:
28
28
  id: '',
29
29
  idempotency_key: '',
30
+ requested_by: options.requestedBy || context.getRequestedBy() || 'system',
30
31
  },
31
32
  metadata: {},
32
33
  pending_webhooks: 99, // force all events goto the event queue
@@ -69,6 +70,7 @@ export async function createStatusEvent(
69
70
  // FIXME:
70
71
  id: '',
71
72
  idempotency_key: '',
73
+ requested_by: options.requestedBy || context.getRequestedBy() || 'system',
72
74
  },
73
75
  metadata: {},
74
76
  pending_webhooks: 99, // force all events goto the event queue
@@ -107,6 +109,7 @@ export async function createCustomEvent(
107
109
  // FIXME:
108
110
  id: '',
109
111
  idempotency_key: '',
112
+ requested_by: options.requestedBy || context.getRequestedBy() || 'system',
110
113
  },
111
114
  metadata: {},
112
115
  pending_webhooks: 99, // force all events goto the event queue
@@ -0,0 +1,48 @@
1
+ import { AsyncLocalStorage, AsyncResource } from 'async_hooks';
2
+
3
+ interface RequestContext {
4
+ requestedBy?: string;
5
+ requestId?: string;
6
+ }
7
+
8
+ class RequestContextManager {
9
+ private storage = new AsyncLocalStorage<RequestContext>();
10
+ private contexts = new Map<string, RequestContext>();
11
+
12
+ getContext(requestId?: string): RequestContext {
13
+ if (requestId && this.contexts.has(requestId)) {
14
+ return this.contexts.get(requestId)!;
15
+ }
16
+ return this.storage.getStore() || {};
17
+ }
18
+
19
+ getRequestedBy(requestId?: string): string | undefined {
20
+ return this.getContext(requestId).requestedBy;
21
+ }
22
+
23
+ run<T>(context: RequestContext, fn: () => Promise<T> | T): Promise<T> {
24
+ const requestId = context.requestId || `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
25
+
26
+ this.contexts.set(requestId, {
27
+ ...context,
28
+ requestId,
29
+ });
30
+
31
+ return new Promise((resolve, reject) => {
32
+ this.storage.run({ ...context, requestId }, async () => {
33
+ const resource = new AsyncResource('RequestContext');
34
+ try {
35
+ const result = await resource.runInAsyncScope(fn);
36
+ resolve(result);
37
+ } catch (err) {
38
+ reject(err);
39
+ } finally {
40
+ // 清理上下文
41
+ this.contexts.delete(requestId);
42
+ }
43
+ });
44
+ });
45
+ }
46
+ }
47
+
48
+ export const context = new RequestContextManager();
@@ -71,7 +71,7 @@ export async function getOneTimeProductInfo(invoiceId: string, paymentCurrency:
71
71
  });
72
72
  return oneTimePaymentInfo;
73
73
  } catch (err) {
74
- console.error(err);
74
+ console.error(`Error in getOneTimeProductInfo for invoice ${invoiceId}:`, err);
75
75
  return [];
76
76
  }
77
77
  }
@@ -140,7 +140,7 @@ export async function getInvoiceShouldPayTotal(invoice: Invoice) {
140
140
  const amount = getSubscriptionCycleAmount(expandedItems, subscription.currency_id);
141
141
  return amount?.total || invoice.total;
142
142
  } catch (err) {
143
- console.error(err);
143
+ console.error(`Error in getInvoiceShouldPayTotal for invoice ${invoice.id}:`, err);
144
144
  return invoice.total;
145
145
  }
146
146
  }
@@ -1,7 +1,8 @@
1
1
  /* eslint-disable import/prefer-default-export */
2
2
  import type { NextFunction, Request, Response } from 'express';
3
-
3
+ import { verify } from '@blocklet/sdk/lib/util/verify-sign';
4
4
  import { translate } from '../locales';
5
+ import { context } from './context';
5
6
 
6
7
  export function ensureI18n() {
7
8
  return (req: Request, _: Response, next: NextFunction) => {
@@ -10,3 +11,40 @@ export function ensureI18n() {
10
11
  next();
11
12
  };
12
13
  }
14
+
15
+ export function contextMiddleware(req: Request, _res: Response, next: NextFunction) {
16
+ const requestId =
17
+ (req.headers['x-request-id'] as string) || `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
18
+ let requestedBy = 'system';
19
+
20
+ // Check component signature
21
+ const sig = req.get('x-component-sig');
22
+ const componentDid = req.get('x-component-did');
23
+ if (sig && componentDid) {
24
+ const data = typeof req.body === 'undefined' ? {} : req.body;
25
+ const verified = verify(data, sig);
26
+ if (verified) {
27
+ requestedBy = componentDid;
28
+ }
29
+ }
30
+
31
+ // Check user DID from headers
32
+ if (req.headers['x-user-did']) {
33
+ requestedBy = req.headers['x-user-did'] as string;
34
+ }
35
+
36
+ // Check authenticated user
37
+ if (req.user?.did) {
38
+ requestedBy = req.user.did;
39
+ }
40
+
41
+ return context.run(
42
+ {
43
+ requestId,
44
+ requestedBy,
45
+ },
46
+ async () => {
47
+ await next();
48
+ }
49
+ );
50
+ }
@@ -122,6 +122,10 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
122
122
  cancellationReason = translate('notification.subscriptionCanceled.paymentFailed', locale);
123
123
  }
124
124
 
125
+ if (subscription.cancelation_details?.reason === 'stake_revoked') {
126
+ cancellationReason = translate('notification.subscriptionCanceled.stakeRevoked', locale);
127
+ }
128
+
125
129
  // @ts-expect-error
126
130
  const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
127
131
  const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
@@ -11,7 +11,7 @@ import { Customer, PaymentMethod, Subscription, PaymentCurrency } from '../../..
11
11
  import { PaymentDetail, getPaymentAmountForCycleSubscription } from '../../payment';
12
12
  import { getMainProductName } from '../../product';
13
13
  import { getCustomerSubscriptionPageUrl } from '../../subscription';
14
- import { formatTime, getPrettyMsI18nLocale } from '../../time';
14
+ import { formatTime, getPrettyMsI18nLocale, getSimplifyDuration } from '../../time';
15
15
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
16
16
  import dayjs from '../../dayjs';
17
17
  import { getSubscriptionNotificationCustomActions } from '../../util';
@@ -27,7 +27,7 @@ interface SubscriptionTrialWilEndEmailTemplateContext {
27
27
  locale: string;
28
28
  productName: string;
29
29
  at: string;
30
- willRenewDuration: string;
30
+ willEndDuration: string;
31
31
  paymentDetail: PaymentDetail;
32
32
 
33
33
  userDid: string;
@@ -79,10 +79,13 @@ export class SubscriptionTrialWilEndEmailTemplate
79
79
  const userDid = customer.did;
80
80
  const locale = await getUserLocale(userDid);
81
81
  const productName = await getMainProductName(subscription.id);
82
+ if (!subscription.trial_end) {
83
+ throw new Error(`Subscription has no trial end: ${subscription.id}`);
84
+ }
85
+
82
86
  const at: string = formatTime((subscription.trial_end as number) * 1000);
83
- const willRenewDuration: string =
84
- locale === 'en' ? this.getWillRenewDuration(locale) : this.getWillRenewDuration(locale).split(' ').join('');
85
87
 
88
+ const willEndDuration: string = getSimplifyDuration((subscription.trial_end! - dayjs().unix()) * 1000, locale);
86
89
  // const paymentDetail: PaymentDetail = await getPaymentDetail(userDid, invoice);
87
90
 
88
91
  const paymentAmount = await getPaymentAmountForCycleSubscription(subscription, paymentCurrency);
@@ -117,7 +120,7 @@ export class SubscriptionTrialWilEndEmailTemplate
117
120
  locale,
118
121
  productName,
119
122
  at,
120
- willRenewDuration,
123
+ willEndDuration,
121
124
  paymentDetail,
122
125
 
123
126
  userDid,
@@ -132,37 +135,12 @@ export class SubscriptionTrialWilEndEmailTemplate
132
135
  };
133
136
  }
134
137
 
135
- getWillRenewDuration(locale: string): string {
136
- if (this.options.willRenewUnit === 'M') {
137
- if (this.options.willRenewValue > 1) {
138
- return `${this.options.willRenewValue} ${translate('notification.common.months', locale)}`;
139
- }
140
-
141
- return `${this.options.willRenewValue} ${translate('notification.common.month', locale)}`;
142
- }
143
- if (this.options.willRenewUnit === 'd') {
144
- if (this.options.willRenewValue > 1) {
145
- return `${this.options.willRenewValue} ${translate('notification.common.days', locale)}`;
146
- }
147
- return `${this.options.willRenewValue} ${translate('notification.common.day', locale)}`;
148
- }
149
-
150
- if (this.options.willRenewUnit === 'm') {
151
- if (this.options.willRenewValue > 1) {
152
- return `${this.options.willRenewValue} ${translate('notification.common.minutes', locale)}`;
153
- }
154
- return `${this.options.willRenewValue} ${translate('notification.common.minute', locale)}`;
155
- }
156
-
157
- return `${this.options.willRenewValue} ${this.options.willRenewUnit}`;
158
- }
159
-
160
138
  async getTemplate(): Promise<BaseEmailTemplateType | null> {
161
139
  const {
162
140
  locale,
163
141
  productName,
164
142
  at,
165
- willRenewDuration,
143
+ willEndDuration,
166
144
  paymentDetail,
167
145
 
168
146
  userDid,
@@ -191,19 +169,19 @@ export class SubscriptionTrialWilEndEmailTemplate
191
169
  const template: BaseEmailTemplateType = {
192
170
  title: `${translate('notification.subscriptionTrialWillEnd.title', locale, {
193
171
  productName,
194
- willRenewDuration,
172
+ willEndDuration,
195
173
  })}`,
196
174
  body:
197
175
  canPay || isStripe
198
176
  ? `${translate('notification.subscriptionTrialWillEnd.body', locale, {
199
177
  at,
200
178
  productName,
201
- willRenewDuration,
179
+ willEndDuration,
202
180
  })}`
203
181
  : `${translate('notification.subscriptionTrialWillEnd.unableToPayBody', locale, {
204
182
  at,
205
183
  productName,
206
- willRenewDuration,
184
+ willEndDuration,
207
185
  balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
208
186
  price: `${paymentDetail.price} ${paymentDetail.symbol}`,
209
187
  })}`,