payment-kit 1.15.16 → 1.15.18
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/integrations/stripe/handlers/invoice.ts +20 -0
- package/api/src/integrations/stripe/resource.ts +2 -2
- package/api/src/libs/audit.ts +1 -1
- package/api/src/libs/invoice.ts +81 -1
- package/api/src/libs/notification/template/billing-discrepancy.ts +223 -0
- package/api/src/libs/notification/template/subscription-canceled.ts +11 -0
- package/api/src/libs/notification/template/subscription-refund-succeeded.ts +10 -2
- package/api/src/libs/notification/template/subscription-renew-failed.ts +10 -2
- package/api/src/libs/notification/template/subscription-renewed.ts +11 -3
- package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +11 -1
- package/api/src/libs/notification/template/subscription-succeeded.ts +11 -1
- package/api/src/libs/notification/template/subscription-trial-start.ts +11 -0
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +17 -0
- package/api/src/libs/notification/template/subscription-upgraded.ts +51 -26
- package/api/src/libs/notification/template/subscription-will-canceled.ts +16 -0
- package/api/src/libs/notification/template/subscription-will-renew.ts +15 -3
- package/api/src/libs/notification/template/usage-report-empty.ts +158 -0
- package/api/src/libs/queue/index.ts +69 -19
- package/api/src/libs/queue/store.ts +28 -5
- package/api/src/libs/subscription.ts +129 -19
- package/api/src/libs/util.ts +30 -0
- package/api/src/locales/en.ts +13 -0
- package/api/src/locales/zh.ts +13 -0
- package/api/src/queues/invoice.ts +58 -20
- package/api/src/queues/notification.ts +43 -1
- package/api/src/queues/payment.ts +5 -1
- package/api/src/queues/subscription.ts +64 -15
- package/api/src/routes/checkout-sessions.ts +26 -0
- package/api/src/routes/invoices.ts +11 -31
- package/api/src/routes/subscriptions.ts +43 -7
- package/api/src/store/models/checkout-session.ts +2 -0
- package/api/src/store/models/job.ts +4 -0
- package/api/src/store/models/types.ts +22 -4
- package/api/src/store/models/usage-record.ts +5 -1
- package/api/tests/libs/subscription.spec.ts +154 -0
- package/api/tests/libs/util.spec.ts +135 -0
- package/blocklet.yml +1 -1
- package/package.json +10 -10
- package/scripts/sdk.js +37 -3
- package/src/components/invoice/list.tsx +0 -1
- package/src/components/invoice/table.tsx +7 -2
- package/src/components/subscription/items/index.tsx +26 -7
- package/src/components/subscription/items/usage-records.tsx +21 -10
- package/src/components/subscription/portal/actions.tsx +16 -14
- package/src/libs/util.ts +51 -0
- package/src/locales/en.tsx +2 -0
- package/src/locales/zh.tsx +2 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +1 -1
- package/src/pages/customer/subscription/change-plan.tsx +1 -1
- package/src/pages/customer/subscription/embed.tsx +16 -14
- package/vite-server.config.ts +8 -0
|
@@ -4,6 +4,8 @@ import pick from 'lodash/pick';
|
|
|
4
4
|
import pWaitFor from 'p-wait-for';
|
|
5
5
|
import type Stripe from 'stripe';
|
|
6
6
|
|
|
7
|
+
import { checkUsageReportEmpty } from '@api/libs/subscription';
|
|
8
|
+
import { createEvent } from '@api/libs/audit';
|
|
7
9
|
import { getLock } from '../../../libs/lock';
|
|
8
10
|
import logger from '../../../libs/logger';
|
|
9
11
|
import {
|
|
@@ -256,6 +258,24 @@ export async function handleStripeInvoiceCreated(event: TEventExpanded, client:
|
|
|
256
258
|
const customer = await Customer.findByPk(subscription.customer_id);
|
|
257
259
|
const checkoutSession = await CheckoutSession.findOne({ where: { subscription_id: subscription.id } });
|
|
258
260
|
|
|
261
|
+
if (stripeInvoice.billing_reason === 'subscription_cycle') {
|
|
262
|
+
// check if usage report is empty
|
|
263
|
+
const usageReportStart = stripeInvoice.period_start;
|
|
264
|
+
const usageReportEnd = stripeInvoice.period_end;
|
|
265
|
+
const usageReportEmpty = await checkUsageReportEmpty(subscription, usageReportStart, usageReportEnd);
|
|
266
|
+
if (usageReportEmpty) {
|
|
267
|
+
createEvent('Subscription', 'usage.report.empty', subscription, {
|
|
268
|
+
usageReportStart,
|
|
269
|
+
usageReportEnd,
|
|
270
|
+
}).catch(console.error);
|
|
271
|
+
logger.info('create usage report empty event', {
|
|
272
|
+
subscriptionId: subscription.id,
|
|
273
|
+
usageReportStart,
|
|
274
|
+
usageReportEnd,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
259
279
|
// create stripe invoice
|
|
260
280
|
const invoice = await ensureStripeInvoice(stripeInvoice, subscription, client);
|
|
261
281
|
|
|
@@ -44,7 +44,7 @@ export async function ensureStripeProduct(internal: Product, method: PaymentMeth
|
|
|
44
44
|
attrs.unit_label = internal.unit_label;
|
|
45
45
|
}
|
|
46
46
|
if (internal.statement_descriptor) {
|
|
47
|
-
attrs.
|
|
47
|
+
attrs.statement_descriptor = '';
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const product = await client.products.create(attrs);
|
|
@@ -176,7 +176,7 @@ export async function ensureStripePaymentIntent(
|
|
|
176
176
|
enabled: true,
|
|
177
177
|
allow_redirects: 'never',
|
|
178
178
|
},
|
|
179
|
-
|
|
179
|
+
statement_descriptor: '',
|
|
180
180
|
metadata: {
|
|
181
181
|
appPid: env.appPid,
|
|
182
182
|
id: internal.id,
|
package/api/src/libs/audit.ts
CHANGED
|
@@ -33,7 +33,7 @@ export async function createEvent(scope: string, type: LiteralUnion<EventType, s
|
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
events.emit('event.created', { id: event.id });
|
|
36
|
-
events.emit(event.type, data.object);
|
|
36
|
+
events.emit(event.type, data.object, options);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export async function createStatusEvent(
|
package/api/src/libs/invoice.ts
CHANGED
|
@@ -3,9 +3,20 @@ import type { LiteralUnion } from 'type-fest';
|
|
|
3
3
|
import { withQuery } from 'ufo';
|
|
4
4
|
|
|
5
5
|
import { fromUnitToToken } from '@ocap/util';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Invoice,
|
|
8
|
+
InvoiceItem,
|
|
9
|
+
PaymentCurrency,
|
|
10
|
+
PaymentMethod,
|
|
11
|
+
Price,
|
|
12
|
+
Product,
|
|
13
|
+
Subscription,
|
|
14
|
+
SubscriptionItem,
|
|
15
|
+
UsageRecord,
|
|
16
|
+
} from '../store/models';
|
|
7
17
|
import { getConnectQueryParam } from './util';
|
|
8
18
|
import { expandLineItems } from './session';
|
|
19
|
+
import { getSubscriptionCycleAmount, getSubscriptionCycleSetup } from './subscription';
|
|
9
20
|
|
|
10
21
|
export function getCustomerInvoicePageUrl({
|
|
11
22
|
invoiceId,
|
|
@@ -64,3 +75,72 @@ export async function getOneTimeProductInfo(invoiceId: string, paymentCurrency:
|
|
|
64
75
|
return [];
|
|
65
76
|
}
|
|
66
77
|
}
|
|
78
|
+
|
|
79
|
+
export async function getInvoiceShouldPayTotal(invoice: Invoice) {
|
|
80
|
+
try {
|
|
81
|
+
const subscription = await Subscription.findByPk(invoice.subscription_id);
|
|
82
|
+
if (!subscription) {
|
|
83
|
+
throw new Error(`Subscription not found: ${invoice.subscription_id}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const paymentCurrency = await PaymentCurrency.findByPk(invoice.currency_id);
|
|
87
|
+
if (!paymentCurrency) {
|
|
88
|
+
throw new Error(`Payment currency not found: ${invoice.currency_id}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
92
|
+
if (!paymentMethod) {
|
|
93
|
+
throw new Error(`Payment method not found: ${paymentCurrency.payment_method_id}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const subscriptionItems = await SubscriptionItem.findAll({
|
|
97
|
+
where: { subscription_id: subscription.id },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
let expandedItems = await Price.expand(
|
|
101
|
+
subscriptionItems.map((x) => ({ id: x.id, price_id: x.price_id, quantity: x.quantity })),
|
|
102
|
+
{ product: true }
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const setup = getSubscriptionCycleSetup(subscription.pending_invoice_item_interval, invoice.period_start);
|
|
106
|
+
let offset = 0;
|
|
107
|
+
let filterFunc = (item: any) => item;
|
|
108
|
+
if (['arcblock', 'ethereum'].includes(paymentMethod.type)) {
|
|
109
|
+
switch (invoice.billing_reason) {
|
|
110
|
+
case 'subscription_cancel':
|
|
111
|
+
filterFunc = (item: any) => item.price?.recurring?.usage_type === 'metered';
|
|
112
|
+
break;
|
|
113
|
+
case 'subscription_cycle':
|
|
114
|
+
filterFunc = (item: any) => item.price?.type === 'recurring';
|
|
115
|
+
offset = setup.cycle / 1000;
|
|
116
|
+
break;
|
|
117
|
+
default:
|
|
118
|
+
filterFunc = () => true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const previousPeriodStart = setup.period.start - offset;
|
|
122
|
+
const previousPeriodEnd = setup.period.end - offset;
|
|
123
|
+
expandedItems = await Promise.all(
|
|
124
|
+
expandedItems.filter(filterFunc).map(async (item: any) => {
|
|
125
|
+
const { price } = item;
|
|
126
|
+
if (price?.recurring?.usage_type === 'metered') {
|
|
127
|
+
const rawQuantity = await UsageRecord.getSummary({
|
|
128
|
+
id: item.id,
|
|
129
|
+
start: previousPeriodStart,
|
|
130
|
+
end: previousPeriodEnd,
|
|
131
|
+
method: price.recurring?.aggregate_usage,
|
|
132
|
+
dryRun: true,
|
|
133
|
+
searchBilled: false,
|
|
134
|
+
});
|
|
135
|
+
item.quantity = price.transformQuantity(rawQuantity);
|
|
136
|
+
}
|
|
137
|
+
return item;
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
const amount = getSubscriptionCycleAmount(expandedItems, subscription.currency_id);
|
|
141
|
+
return amount?.total || invoice.total;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error(err);
|
|
144
|
+
return invoice.total;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { fromUnitToToken } from '@ocap/util';
|
|
2
|
+
import prettyMsI18n from 'pretty-ms-i18n';
|
|
3
|
+
import { getOwnerDid } from '../../util';
|
|
4
|
+
import { translate } from '../../../locales';
|
|
5
|
+
import { Invoice, PaymentCurrency, Subscription } from '../../../store/models';
|
|
6
|
+
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
7
|
+
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
8
|
+
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
9
|
+
import { getAdminSubscriptionPageUrl, getAdminInvoicePageUrl } from '../../subscription';
|
|
10
|
+
import { getMainProductName } from '../../product';
|
|
11
|
+
import { getInvoiceShouldPayTotal } from '../../invoice';
|
|
12
|
+
|
|
13
|
+
export interface BillingDiscrepancyEmailTemplateOptions {
|
|
14
|
+
invoiceId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface BillingDiscrepancyEmailTemplateContext {
|
|
18
|
+
locale: string;
|
|
19
|
+
userDid: string;
|
|
20
|
+
productName: string;
|
|
21
|
+
subscriptionId: string;
|
|
22
|
+
billingAmount: string;
|
|
23
|
+
shouldPayAmount: string;
|
|
24
|
+
invoiceId: string;
|
|
25
|
+
currentPeriodStart: string;
|
|
26
|
+
currentPeriodEnd: string;
|
|
27
|
+
duration: string;
|
|
28
|
+
viewSubscriptionLink: string;
|
|
29
|
+
viewInvoiceLink: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class BillingDiscrepancyEmailTemplate implements BaseEmailTemplate<BillingDiscrepancyEmailTemplateContext> {
|
|
33
|
+
options: BillingDiscrepancyEmailTemplateOptions;
|
|
34
|
+
|
|
35
|
+
constructor(options: BillingDiscrepancyEmailTemplateOptions) {
|
|
36
|
+
this.options = options;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async getContext(): Promise<BillingDiscrepancyEmailTemplateContext> {
|
|
40
|
+
const { invoiceId } = this.options;
|
|
41
|
+
const invoice = await Invoice.findByPk(invoiceId);
|
|
42
|
+
if (!invoice) {
|
|
43
|
+
throw new Error(`Invoice not found: ${invoiceId}`);
|
|
44
|
+
}
|
|
45
|
+
const subscriptionId = invoice.subscription_id;
|
|
46
|
+
const subscription: Subscription | null = await Subscription.findByPk(subscriptionId);
|
|
47
|
+
if (!subscription) {
|
|
48
|
+
throw new Error(`Subscription not found: ${subscriptionId}`);
|
|
49
|
+
}
|
|
50
|
+
const paymentCurrency = (await PaymentCurrency.findOne({
|
|
51
|
+
where: {
|
|
52
|
+
id: subscription.currency_id,
|
|
53
|
+
},
|
|
54
|
+
})) as PaymentCurrency;
|
|
55
|
+
|
|
56
|
+
const userDid = await getOwnerDid();
|
|
57
|
+
if (!userDid) {
|
|
58
|
+
throw new Error('get owner did failed');
|
|
59
|
+
}
|
|
60
|
+
const locale = await getUserLocale(userDid);
|
|
61
|
+
const productName = await getMainProductName(subscription.id);
|
|
62
|
+
const currentPeriodStart = formatTime(invoice.period_start * 1000);
|
|
63
|
+
const currentPeriodEnd = formatTime(invoice.period_end * 1000);
|
|
64
|
+
const duration: string = prettyMsI18n(
|
|
65
|
+
new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
|
|
66
|
+
{
|
|
67
|
+
locale: getPrettyMsI18nLocale(locale),
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
const viewSubscriptionLink = getAdminSubscriptionPageUrl({
|
|
71
|
+
subscriptionId: subscription.id,
|
|
72
|
+
locale,
|
|
73
|
+
userDid,
|
|
74
|
+
});
|
|
75
|
+
const viewInvoiceLink = getAdminInvoicePageUrl({
|
|
76
|
+
invoiceId,
|
|
77
|
+
userDid,
|
|
78
|
+
locale,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const billingAmount = `${fromUnitToToken(invoice.total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
82
|
+
|
|
83
|
+
const shouldPayTotal = await getInvoiceShouldPayTotal(invoice);
|
|
84
|
+
if (shouldPayTotal === invoice.total) {
|
|
85
|
+
throw new Error('should pay total is equal to invoice total, no need to send billing discrepancy notification');
|
|
86
|
+
}
|
|
87
|
+
const shouldPayAmount = `${fromUnitToToken(shouldPayTotal, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
88
|
+
return {
|
|
89
|
+
userDid,
|
|
90
|
+
locale,
|
|
91
|
+
productName,
|
|
92
|
+
subscriptionId: subscription.id,
|
|
93
|
+
billingAmount,
|
|
94
|
+
shouldPayAmount,
|
|
95
|
+
invoiceId,
|
|
96
|
+
currentPeriodStart,
|
|
97
|
+
currentPeriodEnd,
|
|
98
|
+
duration,
|
|
99
|
+
viewSubscriptionLink,
|
|
100
|
+
viewInvoiceLink,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async getTemplate(): Promise<BaseEmailTemplateType> {
|
|
105
|
+
const {
|
|
106
|
+
locale,
|
|
107
|
+
productName,
|
|
108
|
+
subscriptionId,
|
|
109
|
+
billingAmount,
|
|
110
|
+
currentPeriodStart,
|
|
111
|
+
currentPeriodEnd,
|
|
112
|
+
duration,
|
|
113
|
+
viewSubscriptionLink,
|
|
114
|
+
viewInvoiceLink,
|
|
115
|
+
shouldPayAmount,
|
|
116
|
+
} = await this.getContext();
|
|
117
|
+
|
|
118
|
+
const template: BaseEmailTemplateType = {
|
|
119
|
+
title: translate('notification.billingDiscrepancy.title', locale, {
|
|
120
|
+
productName,
|
|
121
|
+
}),
|
|
122
|
+
body: translate('notification.billingDiscrepancy.body', locale, {
|
|
123
|
+
productName,
|
|
124
|
+
}),
|
|
125
|
+
attachments: [
|
|
126
|
+
{
|
|
127
|
+
type: 'section',
|
|
128
|
+
fields: [
|
|
129
|
+
{
|
|
130
|
+
type: 'text',
|
|
131
|
+
data: {
|
|
132
|
+
type: 'plain',
|
|
133
|
+
color: '#9397A1',
|
|
134
|
+
text: translate('notification.common.product', locale),
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
type: 'text',
|
|
139
|
+
data: {
|
|
140
|
+
type: 'plain',
|
|
141
|
+
text: productName,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: 'text',
|
|
146
|
+
data: {
|
|
147
|
+
type: 'plain',
|
|
148
|
+
color: '#9397A1',
|
|
149
|
+
text: translate('notification.common.subscriptionId', locale),
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
type: 'text',
|
|
154
|
+
data: {
|
|
155
|
+
type: 'plain',
|
|
156
|
+
text: subscriptionId,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
type: 'text',
|
|
161
|
+
data: {
|
|
162
|
+
type: 'plain',
|
|
163
|
+
color: '#9397A1',
|
|
164
|
+
text: translate('notification.common.shouldPayAmount', locale),
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
type: 'text',
|
|
169
|
+
data: {
|
|
170
|
+
type: 'plain',
|
|
171
|
+
text: shouldPayAmount,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
type: 'text',
|
|
176
|
+
data: {
|
|
177
|
+
type: 'plain',
|
|
178
|
+
color: '#9397A1',
|
|
179
|
+
text: translate('notification.common.billedAmount', locale),
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
type: 'text',
|
|
184
|
+
data: {
|
|
185
|
+
type: 'plain',
|
|
186
|
+
text: billingAmount,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
type: 'text',
|
|
191
|
+
data: {
|
|
192
|
+
type: 'plain',
|
|
193
|
+
color: '#9397A1',
|
|
194
|
+
text: translate('notification.common.validityPeriod', locale),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: 'text',
|
|
199
|
+
data: {
|
|
200
|
+
type: 'plain',
|
|
201
|
+
text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
].filter(Boolean),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
actions: [
|
|
208
|
+
{
|
|
209
|
+
name: translate('notification.common.viewSubscription', locale),
|
|
210
|
+
title: translate('notification.common.viewSubscription', locale),
|
|
211
|
+
link: viewSubscriptionLink,
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: translate('notification.common.viewInvoice', locale),
|
|
215
|
+
title: translate('notification.common.viewInvoice', locale),
|
|
216
|
+
link: viewInvoiceLink,
|
|
217
|
+
},
|
|
218
|
+
].filter(Boolean),
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return template;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -12,6 +12,7 @@ import { getMainProductName } from '../../product';
|
|
|
12
12
|
import { getCustomerSubscriptionPageUrl, getSubscriptionStakeCancellation } from '../../subscription';
|
|
13
13
|
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
14
14
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
15
|
+
import { getSubscriptionNotificationCustomActions } from '../../util';
|
|
15
16
|
|
|
16
17
|
export interface SubscriptionCanceledEmailTemplateOptions {
|
|
17
18
|
subscriptionId: string;
|
|
@@ -31,6 +32,7 @@ interface SubscriptionCanceledEmailTemplateContext {
|
|
|
31
32
|
cancellationReason: string;
|
|
32
33
|
|
|
33
34
|
viewSubscriptionLink: string;
|
|
35
|
+
customActions: any[];
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<SubscriptionCanceledEmailTemplateContext> {
|
|
@@ -128,6 +130,12 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
128
130
|
userDid,
|
|
129
131
|
});
|
|
130
132
|
|
|
133
|
+
const customActions = getSubscriptionNotificationCustomActions(
|
|
134
|
+
subscription,
|
|
135
|
+
'customer.subscription.deleted',
|
|
136
|
+
locale
|
|
137
|
+
);
|
|
138
|
+
|
|
131
139
|
return {
|
|
132
140
|
locale,
|
|
133
141
|
productName,
|
|
@@ -142,6 +150,7 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
142
150
|
cancellationReason,
|
|
143
151
|
|
|
144
152
|
viewSubscriptionLink,
|
|
153
|
+
customActions,
|
|
145
154
|
};
|
|
146
155
|
}
|
|
147
156
|
|
|
@@ -159,6 +168,7 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
159
168
|
cancellationReason,
|
|
160
169
|
|
|
161
170
|
viewSubscriptionLink,
|
|
171
|
+
customActions,
|
|
162
172
|
} = await this.getContext();
|
|
163
173
|
|
|
164
174
|
const template: BaseEmailTemplateType = {
|
|
@@ -259,6 +269,7 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
259
269
|
title: translate('notification.common.viewSubscription', locale),
|
|
260
270
|
link: viewSubscriptionLink,
|
|
261
271
|
},
|
|
272
|
+
...customActions,
|
|
262
273
|
].filter(Boolean),
|
|
263
274
|
};
|
|
264
275
|
|
|
@@ -5,13 +5,13 @@ import prettyMsI18n from 'pretty-ms-i18n';
|
|
|
5
5
|
|
|
6
6
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
7
7
|
import { translate } from '../../../locales';
|
|
8
|
-
import { Customer, PaymentIntent, PaymentMethod, Refund } from '../../../store/models';
|
|
8
|
+
import { Customer, PaymentIntent, PaymentMethod, Refund, Subscription } from '../../../store/models';
|
|
9
9
|
import { Invoice } from '../../../store/models/invoice';
|
|
10
10
|
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
11
11
|
import { getMainProductName } from '../../product';
|
|
12
12
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
13
13
|
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
14
|
-
import { getExplorerLink } from '../../util';
|
|
14
|
+
import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
|
|
15
15
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
16
16
|
|
|
17
17
|
export interface SubscriptionRefundSucceededEmailTemplateOptions {
|
|
@@ -37,6 +37,7 @@ interface SubscriptionRefundSucceededEmailTemplateContext {
|
|
|
37
37
|
viewSubscriptionLink: string;
|
|
38
38
|
viewTxHashLink: string | undefined;
|
|
39
39
|
refund: Refund;
|
|
40
|
+
customActions: any[];
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export class SubscriptionRefundSucceededEmailTemplate
|
|
@@ -122,6 +123,12 @@ export class SubscriptionRefundSucceededEmailTemplate
|
|
|
122
123
|
chainHost,
|
|
123
124
|
});
|
|
124
125
|
|
|
126
|
+
let customActions: any[] = [];
|
|
127
|
+
if (refund?.subscription_id) {
|
|
128
|
+
const subscription = await Subscription.findByPk(refund.subscription_id);
|
|
129
|
+
customActions = getSubscriptionNotificationCustomActions(subscription!, 'refund.succeeded', locale);
|
|
130
|
+
}
|
|
131
|
+
|
|
125
132
|
return {
|
|
126
133
|
locale,
|
|
127
134
|
productName,
|
|
@@ -141,6 +148,7 @@ export class SubscriptionRefundSucceededEmailTemplate
|
|
|
141
148
|
viewSubscriptionLink,
|
|
142
149
|
viewTxHashLink,
|
|
143
150
|
refund,
|
|
151
|
+
customActions,
|
|
144
152
|
};
|
|
145
153
|
}
|
|
146
154
|
|
|
@@ -21,7 +21,7 @@ import { SufficientForPaymentResult, getPaymentDetail } from '../../payment';
|
|
|
21
21
|
import { getMainProductName } from '../../product';
|
|
22
22
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
23
23
|
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
24
|
-
import { getExplorerLink } from '../../util';
|
|
24
|
+
import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
|
|
25
25
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
26
26
|
|
|
27
27
|
export interface SubscriptionRenewFailedEmailTemplateOptions {
|
|
@@ -45,6 +45,7 @@ interface SubscriptionRenewFailedEmailTemplateContext {
|
|
|
45
45
|
viewSubscriptionLink: string;
|
|
46
46
|
viewInvoiceLink: string;
|
|
47
47
|
viewTxHashLink: string | undefined;
|
|
48
|
+
customActions: any[];
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export class SubscriptionRenewFailedEmailTemplate
|
|
@@ -147,7 +148,11 @@ export class SubscriptionRenewFailedEmailTemplate
|
|
|
147
148
|
chainHost,
|
|
148
149
|
})
|
|
149
150
|
: undefined;
|
|
150
|
-
|
|
151
|
+
const customActions = getSubscriptionNotificationCustomActions(
|
|
152
|
+
subscription,
|
|
153
|
+
'customer.subscription.renew_failed',
|
|
154
|
+
locale
|
|
155
|
+
);
|
|
151
156
|
return {
|
|
152
157
|
locale,
|
|
153
158
|
productName,
|
|
@@ -164,6 +169,7 @@ export class SubscriptionRenewFailedEmailTemplate
|
|
|
164
169
|
viewSubscriptionLink,
|
|
165
170
|
viewInvoiceLink,
|
|
166
171
|
viewTxHashLink,
|
|
172
|
+
customActions,
|
|
167
173
|
};
|
|
168
174
|
}
|
|
169
175
|
|
|
@@ -182,6 +188,7 @@ export class SubscriptionRenewFailedEmailTemplate
|
|
|
182
188
|
viewSubscriptionLink,
|
|
183
189
|
viewInvoiceLink,
|
|
184
190
|
viewTxHashLink,
|
|
191
|
+
customActions,
|
|
185
192
|
} = await this.getContext();
|
|
186
193
|
|
|
187
194
|
const template: BaseEmailTemplateType = {
|
|
@@ -308,6 +315,7 @@ export class SubscriptionRenewFailedEmailTemplate
|
|
|
308
315
|
title: translate('notification.common.viewTxHash', locale),
|
|
309
316
|
link: viewTxHashLink as string,
|
|
310
317
|
},
|
|
318
|
+
...customActions,
|
|
311
319
|
].filter(Boolean),
|
|
312
320
|
};
|
|
313
321
|
|
|
@@ -19,7 +19,7 @@ import { getCustomerInvoicePageUrl } from '../../invoice';
|
|
|
19
19
|
import { getMainProductName } from '../../product';
|
|
20
20
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
21
21
|
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
22
|
-
import { getExplorerLink } from '../../util';
|
|
22
|
+
import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
|
|
23
23
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
24
24
|
|
|
25
25
|
export interface SubscriptionRenewedEmailTemplateOptions {
|
|
@@ -49,6 +49,7 @@ interface SubscriptionRenewedEmailTemplateContext {
|
|
|
49
49
|
viewInvoiceLink: string;
|
|
50
50
|
viewTxHashLink: string | undefined;
|
|
51
51
|
invoice: Invoice;
|
|
52
|
+
customActions: any[];
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<SubscriptionRenewedEmailTemplateContext> {
|
|
@@ -93,7 +94,7 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
93
94
|
const userDid: string = customer.did;
|
|
94
95
|
const locale = await getUserLocale(userDid);
|
|
95
96
|
const productName = await getMainProductName(subscription.id);
|
|
96
|
-
const at: string = formatTime(Date.now());
|
|
97
|
+
const at: string = formatTime((invoice?.status_transitions?.paid_at ?? Math.floor(Date.now() / 1000)) * 1000);
|
|
97
98
|
|
|
98
99
|
const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
|
|
99
100
|
const nftMintItem: NftMintItem | undefined = hasNft
|
|
@@ -133,7 +134,11 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
133
134
|
chainHost,
|
|
134
135
|
})
|
|
135
136
|
: undefined;
|
|
136
|
-
|
|
137
|
+
const customActions = getSubscriptionNotificationCustomActions(
|
|
138
|
+
subscription,
|
|
139
|
+
'customer.subscription.renewed',
|
|
140
|
+
locale
|
|
141
|
+
);
|
|
137
142
|
return {
|
|
138
143
|
locale,
|
|
139
144
|
productName,
|
|
@@ -151,6 +156,7 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
151
156
|
viewInvoiceLink,
|
|
152
157
|
viewTxHashLink,
|
|
153
158
|
invoice,
|
|
159
|
+
customActions,
|
|
154
160
|
};
|
|
155
161
|
}
|
|
156
162
|
|
|
@@ -170,6 +176,7 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
170
176
|
viewInvoiceLink,
|
|
171
177
|
viewTxHashLink,
|
|
172
178
|
invoice,
|
|
179
|
+
customActions,
|
|
173
180
|
} = await this.getContext();
|
|
174
181
|
|
|
175
182
|
if (invoice.total === '0') {
|
|
@@ -288,6 +295,7 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
288
295
|
title: translate('notification.common.viewTxHash', locale),
|
|
289
296
|
link: viewTxHashLink as string,
|
|
290
297
|
},
|
|
298
|
+
...customActions,
|
|
291
299
|
].filter(Boolean),
|
|
292
300
|
};
|
|
293
301
|
|
|
@@ -8,7 +8,7 @@ import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
|
8
8
|
import { getMainProductName } from '../../product';
|
|
9
9
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
10
10
|
import { formatTime } from '../../time';
|
|
11
|
-
import { getExplorerLink } from '../../util';
|
|
11
|
+
import { getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
|
|
12
12
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
13
13
|
import logger from '../../logger';
|
|
14
14
|
|
|
@@ -29,6 +29,7 @@ interface SubscriptionStakeSlashSucceededEmailTemplateContext {
|
|
|
29
29
|
|
|
30
30
|
viewSubscriptionLink: string;
|
|
31
31
|
viewTxHashLink: string | undefined;
|
|
32
|
+
customActions: any[];
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export class SubscriptionStakeSlashSucceededEmailTemplate
|
|
@@ -105,6 +106,12 @@ export class SubscriptionStakeSlashSucceededEmailTemplate
|
|
|
105
106
|
|
|
106
107
|
const slashReason = subscription?.cancelation_details?.slash_reason || 'admin slash';
|
|
107
108
|
|
|
109
|
+
const customActions = getSubscriptionNotificationCustomActions(
|
|
110
|
+
subscription,
|
|
111
|
+
'subscription.stake.slash.succeeded',
|
|
112
|
+
locale
|
|
113
|
+
);
|
|
114
|
+
|
|
108
115
|
return {
|
|
109
116
|
locale,
|
|
110
117
|
productName,
|
|
@@ -115,6 +122,7 @@ export class SubscriptionStakeSlashSucceededEmailTemplate
|
|
|
115
122
|
slashReason,
|
|
116
123
|
viewSubscriptionLink,
|
|
117
124
|
viewTxHashLink,
|
|
125
|
+
customActions,
|
|
118
126
|
};
|
|
119
127
|
}
|
|
120
128
|
|
|
@@ -129,6 +137,7 @@ export class SubscriptionStakeSlashSucceededEmailTemplate
|
|
|
129
137
|
slashReason,
|
|
130
138
|
viewSubscriptionLink,
|
|
131
139
|
viewTxHashLink,
|
|
140
|
+
customActions,
|
|
132
141
|
} = await this.getContext();
|
|
133
142
|
|
|
134
143
|
logger.info('SubscriptionStakeSlashSucceededEmailTemplate getTemplate', { productName, at, userDid, slashInfo, viewSubscriptionLink, viewTxHashLink });
|
|
@@ -220,6 +229,7 @@ export class SubscriptionStakeSlashSucceededEmailTemplate
|
|
|
220
229
|
title: translate('notification.common.viewTxHash', locale),
|
|
221
230
|
link: viewTxHashLink as string,
|
|
222
231
|
},
|
|
232
|
+
...customActions,
|
|
223
233
|
].filter(Boolean),
|
|
224
234
|
};
|
|
225
235
|
|