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.
- package/api/src/crons/base.ts +69 -7
- package/api/src/crons/subscription-trial-will-end.ts +20 -5
- package/api/src/crons/subscription-will-canceled.ts +22 -6
- package/api/src/crons/subscription-will-renew.ts +13 -4
- package/api/src/index.ts +4 -1
- package/api/src/integrations/arcblock/stake.ts +27 -0
- package/api/src/libs/audit.ts +4 -1
- package/api/src/libs/context.ts +48 -0
- package/api/src/libs/invoice.ts +2 -2
- package/api/src/libs/middleware.ts +39 -1
- package/api/src/libs/notification/template/subscription-canceled.ts +4 -0
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +12 -34
- package/api/src/libs/notification/template/subscription-will-canceled.ts +82 -48
- package/api/src/libs/notification/template/subscription-will-renew.ts +16 -45
- package/api/src/libs/time.ts +13 -0
- package/api/src/libs/util.ts +17 -0
- package/api/src/locales/en.ts +12 -2
- package/api/src/locales/zh.ts +11 -2
- package/api/src/queues/checkout-session.ts +15 -0
- package/api/src/queues/event.ts +13 -4
- package/api/src/queues/invoice.ts +21 -3
- package/api/src/queues/payment.ts +3 -0
- package/api/src/queues/refund.ts +3 -0
- package/api/src/queues/subscription.ts +107 -2
- package/api/src/queues/usage-record.ts +4 -0
- package/api/src/queues/webhook.ts +9 -0
- package/api/src/routes/checkout-sessions.ts +40 -2
- package/api/src/routes/connect/recharge.ts +143 -0
- package/api/src/routes/connect/shared.ts +25 -0
- package/api/src/routes/customers.ts +2 -2
- package/api/src/routes/donations.ts +5 -1
- package/api/src/routes/events.ts +9 -4
- package/api/src/routes/payment-links.ts +40 -20
- package/api/src/routes/prices.ts +17 -4
- package/api/src/routes/products.ts +21 -2
- package/api/src/routes/refunds.ts +20 -3
- package/api/src/routes/subscription-items.ts +39 -2
- package/api/src/routes/subscriptions.ts +77 -40
- package/api/src/routes/usage-records.ts +29 -0
- package/api/src/store/models/event.ts +1 -0
- package/api/src/store/models/subscription.ts +2 -0
- package/api/tests/libs/time.spec.ts +54 -0
- package/blocklet.yml +1 -1
- package/package.json +19 -19
- package/src/app.tsx +10 -0
- package/src/components/subscription/actions/cancel.tsx +30 -9
- package/src/components/subscription/actions/index.tsx +11 -3
- package/src/components/webhook/attempts.tsx +122 -3
- package/src/locales/en.tsx +13 -0
- package/src/locales/zh.tsx +13 -0
- package/src/pages/customer/recharge.tsx +417 -0
- package/src/pages/customer/subscription/detail.tsx +38 -20
package/api/src/crons/base.ts
CHANGED
|
@@ -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
|
|
37
|
+
const scheduleSubscriptions = this.getScheduleSubscriptions(subscriptions);
|
|
38
38
|
|
|
39
|
-
await this.addTaskToQueue(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
+
}
|
package/api/src/libs/audit.ts
CHANGED
|
@@ -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();
|
package/api/src/libs/invoice.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
+
willEndDuration,
|
|
202
180
|
})}`
|
|
203
181
|
: `${translate('notification.subscriptionTrialWillEnd.unableToPayBody', locale, {
|
|
204
182
|
at,
|
|
205
183
|
productName,
|
|
206
|
-
|
|
184
|
+
willEndDuration,
|
|
207
185
|
balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
|
|
208
186
|
price: `${paymentDetail.price} ${paymentDetail.symbol}`,
|
|
209
187
|
})}`,
|