payment-kit 1.15.2 → 1.15.4
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/payment-stat.ts +1 -0
- package/api/src/index.ts +2 -2
- package/api/src/integrations/arcblock/stake.ts +17 -10
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +15 -8
- package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -30
- package/api/src/libs/notification/template/subscription-canceled.ts +45 -23
- package/api/src/libs/notification/template/subscription-refund-succeeded.ts +130 -47
- package/api/src/libs/notification/template/subscription-renewed.ts +10 -2
- package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +228 -0
- package/api/src/libs/notification/template/subscription-succeeded.ts +2 -2
- package/api/src/libs/notification/template/subscription-trial-start.ts +7 -10
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +13 -5
- package/api/src/libs/notification/template/subscription-will-renew.ts +41 -29
- package/api/src/libs/payment.ts +53 -1
- package/api/src/libs/subscription.ts +43 -0
- package/api/src/locales/en.ts +24 -0
- package/api/src/locales/zh.ts +22 -0
- package/api/src/queues/invoice.ts +1 -1
- package/api/src/queues/notification.ts +9 -0
- package/api/src/queues/payment.ts +17 -0
- package/api/src/routes/checkout-sessions.ts +13 -1
- package/api/src/routes/payment-stats.ts +3 -3
- package/api/src/routes/subscriptions.ts +26 -6
- package/api/src/store/migrations/20240905-index.ts +100 -0
- package/api/src/store/models/subscription.ts +1 -0
- package/api/tests/libs/payment.spec.ts +168 -0
- package/blocklet.yml +1 -1
- package/package.json +9 -9
- package/src/components/balance-list.tsx +2 -2
- package/src/components/invoice/list.tsx +2 -2
- package/src/components/invoice/table.tsx +1 -1
- package/src/components/payment-intent/list.tsx +1 -1
- package/src/components/payouts/list.tsx +1 -1
- package/src/components/refund/list.tsx +2 -2
- package/src/components/subscription/actions/cancel.tsx +41 -13
- package/src/components/subscription/actions/index.tsx +11 -8
- package/src/components/subscription/actions/slash-stake.tsx +52 -0
- package/src/locales/en.tsx +1 -0
- package/src/locales/zh.tsx +1 -0
- package/src/pages/admin/billing/invoices/detail.tsx +2 -2
- package/src/pages/customer/refund/list.tsx +1 -1
- package/src/pages/customer/subscription/detail.tsx +1 -1
package/api/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import express, { ErrorRequestHandler, Request, Response } from 'express';
|
|
|
10
10
|
import morgan from 'morgan';
|
|
11
11
|
|
|
12
12
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
13
|
-
import { xss } from '
|
|
13
|
+
import { xss } from '@blocklet/xss';
|
|
14
14
|
|
|
15
15
|
import crons from './crons/index';
|
|
16
16
|
import { ensureStakedForGas } from './integrations/arcblock/stake';
|
|
@@ -55,7 +55,7 @@ app.use((req, res, next) => {
|
|
|
55
55
|
});
|
|
56
56
|
app.use(express.urlencoded({ extended: true, limit: '1 mb' }));
|
|
57
57
|
app.use(cors());
|
|
58
|
-
app.use(xss());
|
|
58
|
+
app.use(xss({ allowedKeys: [] }));
|
|
59
59
|
app.use(ensureI18n());
|
|
60
60
|
|
|
61
61
|
const router = express.Router();
|
|
@@ -5,7 +5,7 @@ import assert from 'assert';
|
|
|
5
5
|
import { isEthereumDid } from '@arcblock/did';
|
|
6
6
|
import { toStakeAddress } from '@arcblock/did-util';
|
|
7
7
|
import env from '@blocklet/sdk/lib/env';
|
|
8
|
-
import { fromUnitToToken, toBN } from '@ocap/util';
|
|
8
|
+
import { BN, fromUnitToToken, toBN } from '@ocap/util';
|
|
9
9
|
|
|
10
10
|
import { wallet } from '../../libs/auth';
|
|
11
11
|
import { events } from '../../libs/event';
|
|
@@ -189,20 +189,27 @@ export async function getStakeSummaryByDid(did: string, livemode: boolean = true
|
|
|
189
189
|
return {};
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
// FIXME: should use listStakes to find all stakes and summarize here
|
|
193
|
-
const address = toStakeAddress(did, wallet.address);
|
|
194
192
|
const results: GroupedBN = {};
|
|
195
193
|
await Promise.all(
|
|
196
194
|
methods.map(async (method: PaymentMethod) => {
|
|
197
195
|
const client = method.getOcapClient();
|
|
198
|
-
const {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (currency) {
|
|
203
|
-
results[currency.id] = t.value;
|
|
204
|
-
}
|
|
196
|
+
const { stakes } = await client.listStakes({
|
|
197
|
+
addressFilter: {
|
|
198
|
+
receiver: wallet.address,
|
|
199
|
+
},
|
|
205
200
|
});
|
|
201
|
+
(stakes || [])
|
|
202
|
+
.filter((x: any) => x.sender === did)
|
|
203
|
+
.forEach((x: any) => {
|
|
204
|
+
const { tokens } = x;
|
|
205
|
+
(tokens || []).forEach((t: any) => {
|
|
206
|
+
// @ts-ignore
|
|
207
|
+
const currency = method.payment_currencies?.find((c: PaymentCurrency) => t.address === c.contract);
|
|
208
|
+
if (currency) {
|
|
209
|
+
results[currency.id] = new BN(results[currency.id] || '0').add(new BN(t.balance || '0')).toString();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
206
213
|
})
|
|
207
214
|
);
|
|
208
215
|
|
|
@@ -23,6 +23,7 @@ import logger from '../../logger';
|
|
|
23
23
|
import { formatTime } from '../../time';
|
|
24
24
|
import { getExplorerLink } from '../../util';
|
|
25
25
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
26
|
+
import { blocklet } from '../../auth';
|
|
26
27
|
|
|
27
28
|
export interface CustomerRewardSucceededEmailTemplateOptions {
|
|
28
29
|
checkoutSessionId: string;
|
|
@@ -109,16 +110,14 @@ export class CustomerRewardSucceededEmailTemplate
|
|
|
109
110
|
const locale = await getUserLocale(userDid);
|
|
110
111
|
const at: string = formatTime(checkoutSession.created_at);
|
|
111
112
|
|
|
112
|
-
const paymentInfo: string = `${fromUnitToToken(checkoutSession?.amount_total, paymentCurrency.decimal)} ${
|
|
113
|
-
paymentCurrency.symbol
|
|
114
|
-
}`;
|
|
113
|
+
const paymentInfo: string = `${fromUnitToToken(checkoutSession?.amount_total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
115
114
|
const paymentIntent = await PaymentIntent.findByPk(checkoutSession!.payment_intent_id);
|
|
116
115
|
if (!paymentIntent) {
|
|
117
116
|
throw new Error(
|
|
118
117
|
`Payment intent cannot be found for checkoutSession.payment_intent_id${checkoutSession!.payment_intent_id}`
|
|
119
118
|
);
|
|
120
119
|
}
|
|
121
|
-
const rewardDetail: string = this.getRewardDetail({
|
|
120
|
+
const rewardDetail: string = await this.getRewardDetail({
|
|
122
121
|
paymentIntent,
|
|
123
122
|
paymentCurrency,
|
|
124
123
|
locale,
|
|
@@ -166,7 +165,7 @@ export class CustomerRewardSucceededEmailTemplate
|
|
|
166
165
|
viewTxHashLink,
|
|
167
166
|
};
|
|
168
167
|
}
|
|
169
|
-
getRewardDetail({
|
|
168
|
+
async getRewardDetail({
|
|
170
169
|
paymentIntent,
|
|
171
170
|
paymentCurrency,
|
|
172
171
|
locale,
|
|
@@ -174,16 +173,24 @@ export class CustomerRewardSucceededEmailTemplate
|
|
|
174
173
|
paymentIntent: PaymentIntent;
|
|
175
174
|
paymentCurrency: PaymentCurrency;
|
|
176
175
|
locale: LiteralUnion<'zh' | 'en', string>;
|
|
177
|
-
}): string {
|
|
176
|
+
}): Promise<string> {
|
|
178
177
|
if (isEmpty(paymentIntent.beneficiaries)) {
|
|
179
178
|
logger.warn('Payment intent not available', { paymentIntentId: paymentIntent.id });
|
|
180
179
|
return '';
|
|
181
180
|
}
|
|
182
181
|
|
|
182
|
+
const promises = paymentIntent.beneficiaries!.map((x: PaymentBeneficiary) => {
|
|
183
|
+
return blocklet.getUser(x.address);
|
|
184
|
+
});
|
|
185
|
+
const users = await Promise.all(promises);
|
|
186
|
+
if (!users.length) {
|
|
187
|
+
logger.warn('No users found for payment intent', { paymentIntentId: paymentIntent.id });
|
|
188
|
+
return '';
|
|
189
|
+
}
|
|
183
190
|
const rewardDetail: string = paymentIntent
|
|
184
|
-
.beneficiaries!.map((x: PaymentBeneficiary) => {
|
|
191
|
+
.beneficiaries!.map((x: PaymentBeneficiary, index: number) => {
|
|
185
192
|
return translate('notification.customerRewardSucceeded.received', locale, {
|
|
186
|
-
address: `${x.address}`,
|
|
193
|
+
address: `${users[index]?.user?.fullName || ''} ( ${x.address} )`,
|
|
187
194
|
amount: `${fromUnitToToken(x.share, paymentCurrency.decimal)} ${paymentCurrency.symbol}`,
|
|
188
195
|
});
|
|
189
196
|
})
|
|
@@ -5,14 +5,7 @@ import pWaitFor from 'p-wait-for';
|
|
|
5
5
|
|
|
6
6
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
7
7
|
import { translate } from '../../../locales';
|
|
8
|
-
import {
|
|
9
|
-
CheckoutSession,
|
|
10
|
-
Customer,
|
|
11
|
-
NftMintItem,
|
|
12
|
-
PaymentIntent,
|
|
13
|
-
PaymentLink,
|
|
14
|
-
PaymentMethod,
|
|
15
|
-
} from '../../../store/models';
|
|
8
|
+
import { CheckoutSession, Customer, NftMintItem, PaymentIntent, PaymentMethod } from '../../../store/models';
|
|
16
9
|
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
17
10
|
import { getMainProductNameByCheckoutSession } from '../../product';
|
|
18
11
|
import { formatTime } from '../../time';
|
|
@@ -82,13 +75,6 @@ export class OneTimePaymentSucceededEmailTemplate
|
|
|
82
75
|
);
|
|
83
76
|
|
|
84
77
|
const checkoutSession = (await CheckoutSession.findByPk(cs.id)) as CheckoutSession;
|
|
85
|
-
const paymentLink = await PaymentLink.findByPk(checkoutSession.payment_link_id);
|
|
86
|
-
if (!paymentLink) {
|
|
87
|
-
throw new Error(`Payment link cannot be found for payment_link_id(${checkoutSession.payment_link_id})`);
|
|
88
|
-
}
|
|
89
|
-
if (paymentLink.submit_type !== 'auto') {
|
|
90
|
-
throw new Error(`Payment link submit_type(${paymentLink.submit_type}) must be auto`);
|
|
91
|
-
}
|
|
92
78
|
|
|
93
79
|
const paymentCurrency = (await PaymentCurrency.findOne({
|
|
94
80
|
where: {
|
|
@@ -223,21 +209,6 @@ export class OneTimePaymentSucceededEmailTemplate
|
|
|
223
209
|
text: paymentInfo,
|
|
224
210
|
},
|
|
225
211
|
},
|
|
226
|
-
{
|
|
227
|
-
type: 'text',
|
|
228
|
-
data: {
|
|
229
|
-
type: 'plain',
|
|
230
|
-
color: '#9397A1',
|
|
231
|
-
text: translate('notification.common.validityPeriod', locale),
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
type: 'text',
|
|
236
|
-
data: {
|
|
237
|
-
type: 'plain',
|
|
238
|
-
text: translate('notification.common.permanent', locale),
|
|
239
|
-
},
|
|
240
|
-
},
|
|
241
212
|
].filter(Boolean),
|
|
242
213
|
},
|
|
243
214
|
].filter(Boolean),
|
|
@@ -8,9 +8,8 @@ import { translate } from '../../../locales';
|
|
|
8
8
|
import { Customer, PaymentMethod, Refund, Subscription } from '../../../store/models';
|
|
9
9
|
import { Invoice } from '../../../store/models/invoice';
|
|
10
10
|
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
11
|
-
import logger from '../../logger';
|
|
12
11
|
import { getMainProductName } from '../../product';
|
|
13
|
-
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
12
|
+
import { getCustomerSubscriptionPageUrl, getSubscriptionStakeCancellation } from '../../subscription';
|
|
14
13
|
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
15
14
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
16
15
|
|
|
@@ -49,18 +48,6 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
49
48
|
if (subscription.status !== 'canceled') {
|
|
50
49
|
throw new Error(`Subscription(${this.options.subscriptionId}) status(${subscription.status}) must be canceled`);
|
|
51
50
|
}
|
|
52
|
-
if (['payment_failed', 'payment_disputed'].includes(subscription.cancelation_details?.reason as string) === false) {
|
|
53
|
-
// 只有没钱导致订阅被取消了,或者管理员取消了,才会发送通知
|
|
54
|
-
logger.error(
|
|
55
|
-
`Subscription(${this.options.subscriptionId}) cancelation reason must be payment_disputed or payment_failed`,
|
|
56
|
-
{
|
|
57
|
-
cancelation_details: subscription.cancelation_details,
|
|
58
|
-
}
|
|
59
|
-
);
|
|
60
|
-
throw new Error(
|
|
61
|
-
`Subscription(${this.options.subscriptionId}) cancelation reason must be payment_disputed or payment_failed`
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
51
|
|
|
65
52
|
const customer = await Customer.findByPk(subscription.customer_id);
|
|
66
53
|
if (!customer) {
|
|
@@ -88,16 +75,51 @@ export class SubscriptionCanceledEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
88
75
|
locale: getPrettyMsI18nLocale(locale),
|
|
89
76
|
}
|
|
90
77
|
);
|
|
91
|
-
const refund = await Refund.findOne({
|
|
92
|
-
where: {
|
|
93
|
-
subscription_id: subscription.id,
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
const cancellationReason = refund
|
|
97
|
-
? translate('notification.subscriptionCanceled.adminCanceledAndRefunded', locale)
|
|
98
|
-
: translate('notification.subscriptionCanceled.adminCanceled', locale);
|
|
99
|
-
|
|
100
78
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
|
|
79
|
+
|
|
80
|
+
const customerCancelRequest = subscription.cancelation_details?.reason === 'cancellation_requested';
|
|
81
|
+
let cancellationReason = '';
|
|
82
|
+
const { stakeEnough, stakeReturn, stakeSlash, hasStake } = await getSubscriptionStakeCancellation(
|
|
83
|
+
subscription,
|
|
84
|
+
paymentMethod!,
|
|
85
|
+
paymentCurrency
|
|
86
|
+
);
|
|
87
|
+
if (customerCancelRequest) {
|
|
88
|
+
// cancel request from customer
|
|
89
|
+
cancellationReason = translate('notification.subscriptionCanceled.customerCanceled', locale);
|
|
90
|
+
if (stakeReturn && hasStake && stakeEnough) {
|
|
91
|
+
cancellationReason = translate('notification.subscriptionCanceled.customerCanceledAndStakeReturned', locale);
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
// default admin cancel
|
|
95
|
+
const refund = await Refund.findOne({
|
|
96
|
+
where: {
|
|
97
|
+
subscription_id: subscription.id,
|
|
98
|
+
type: 'refund',
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const conditions = [
|
|
102
|
+
{
|
|
103
|
+
condition: refund && stakeSlash,
|
|
104
|
+
key: 'notification.subscriptionCanceled.adminCanceledAndRefundedAndStakeSlashed',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
condition: refund && stakeReturn,
|
|
108
|
+
key: 'notification.subscriptionCanceled.adminCanceledAndRefundedAndStakeReturned',
|
|
109
|
+
},
|
|
110
|
+
{ condition: refund, key: 'notification.subscriptionCanceled.adminCanceledAndRefunded' },
|
|
111
|
+
{ condition: stakeReturn, key: 'notification.subscriptionCanceled.adminCanceledAndStakeReturned' },
|
|
112
|
+
];
|
|
113
|
+
const matchedCondition = conditions.find((item) => item.condition);
|
|
114
|
+
if (matchedCondition) {
|
|
115
|
+
cancellationReason = translate(matchedCondition.key, locale);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (subscription.cancelation_details?.reason === 'payment_failed') {
|
|
120
|
+
cancellationReason = translate('notification.subscriptionCanceled.paymentFailed', locale);
|
|
121
|
+
}
|
|
122
|
+
|
|
101
123
|
// @ts-expect-error
|
|
102
124
|
const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
|
|
103
125
|
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
@@ -8,7 +8,6 @@ import { translate } from '../../../locales';
|
|
|
8
8
|
import { Customer, PaymentIntent, PaymentMethod, Refund } from '../../../store/models';
|
|
9
9
|
import { Invoice } from '../../../store/models/invoice';
|
|
10
10
|
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
11
|
-
import { getCustomerInvoicePageUrl } from '../../invoice';
|
|
12
11
|
import { getMainProductName } from '../../product';
|
|
13
12
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
14
13
|
import { formatTime, getPrettyMsI18nLocale } from '../../time';
|
|
@@ -28,16 +27,16 @@ interface SubscriptionRefundSucceededEmailTemplateContext {
|
|
|
28
27
|
userDid: string;
|
|
29
28
|
paymentInfo: string;
|
|
30
29
|
refundInfo: string;
|
|
31
|
-
currentPeriodStart
|
|
32
|
-
currentPeriodEnd
|
|
30
|
+
currentPeriodStart?: string;
|
|
31
|
+
currentPeriodEnd?: string;
|
|
33
32
|
duration: string;
|
|
34
|
-
unusedPeriodStart
|
|
35
|
-
unusedPeriodEnd
|
|
33
|
+
unusedPeriodStart?: string;
|
|
34
|
+
unusedPeriodEnd?: string;
|
|
36
35
|
unusedDuration: string;
|
|
37
36
|
|
|
38
37
|
viewSubscriptionLink: string;
|
|
39
|
-
viewInvoiceLink: string;
|
|
40
38
|
viewTxHashLink: string | undefined;
|
|
39
|
+
refund: Refund;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
export class SubscriptionRefundSucceededEmailTemplate
|
|
@@ -63,7 +62,6 @@ export class SubscriptionRefundSucceededEmailTemplate
|
|
|
63
62
|
throw new Error(`Customer not found: ${refund.customer_id}`);
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
const invoice = (await Invoice.findByPk(refund.invoice_id)) as Invoice;
|
|
67
65
|
const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
|
|
68
66
|
const paymentCurrency = (await PaymentCurrency.findOne({
|
|
69
67
|
where: {
|
|
@@ -76,42 +74,43 @@ export class SubscriptionRefundSucceededEmailTemplate
|
|
|
76
74
|
const productName = await getMainProductName(refund.subscription_id!);
|
|
77
75
|
const at: string = formatTime(refund.created_at);
|
|
78
76
|
|
|
79
|
-
const paymentInfo: string = `${fromUnitToToken(paymentIntent?.amount_received || '0', paymentCurrency.decimal)} ${
|
|
80
|
-
paymentCurrency.symbol
|
|
81
|
-
}`;
|
|
77
|
+
const paymentInfo: string = `${fromUnitToToken(paymentIntent?.amount_received || '0', paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
82
78
|
const refundInfo: string = `${fromUnitToToken(refund.amount, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
83
79
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
80
|
+
let invoice = null;
|
|
81
|
+
let currentPeriodStart;
|
|
82
|
+
let currentPeriodEnd;
|
|
83
|
+
let duration;
|
|
84
|
+
let unusedPeriodStart;
|
|
85
|
+
let unusedPeriodEnd;
|
|
86
|
+
let unusedDuration;
|
|
87
|
+
if (refund.type === 'refund') {
|
|
88
|
+
invoice = (await Invoice.findByPk(refund.invoice_id)) as Invoice;
|
|
89
|
+
currentPeriodStart = formatTime(invoice.period_start * 1000);
|
|
90
|
+
currentPeriodEnd = formatTime(invoice.period_end * 1000);
|
|
91
|
+
duration = prettyMsI18n(new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(), {
|
|
89
92
|
locale: getPrettyMsI18nLocale(locale),
|
|
90
|
-
}
|
|
91
|
-
);
|
|
93
|
+
});
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
if (refund?.metadata?.unused_period_start && refund?.metadata?.unused_period_end) {
|
|
96
|
+
unusedPeriodStart = formatTime(refund!.metadata!.unused_period_start! * 1000);
|
|
97
|
+
unusedPeriodEnd = formatTime(refund!.metadata!.unused_period_end! * 1000);
|
|
98
|
+
unusedDuration = prettyMsI18n(new Date(unusedPeriodEnd).getTime() - new Date(unusedPeriodStart).getTime(), {
|
|
99
|
+
locale: getPrettyMsI18nLocale(locale),
|
|
100
|
+
});
|
|
99
101
|
}
|
|
100
|
-
|
|
102
|
+
}
|
|
101
103
|
|
|
102
|
-
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(
|
|
104
|
+
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(
|
|
105
|
+
refund.payment_method_id || invoice?.default_payment_method_id
|
|
106
|
+
);
|
|
103
107
|
// @ts-expect-error
|
|
104
108
|
const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
|
|
105
109
|
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
106
|
-
subscriptionId: refund.
|
|
110
|
+
subscriptionId: refund.subscription_id!,
|
|
107
111
|
locale,
|
|
108
112
|
userDid,
|
|
109
113
|
});
|
|
110
|
-
const viewInvoiceLink = getCustomerInvoicePageUrl({
|
|
111
|
-
invoiceId: invoice.id,
|
|
112
|
-
userDid,
|
|
113
|
-
locale,
|
|
114
|
-
});
|
|
115
114
|
|
|
116
115
|
// @ts-expect-error
|
|
117
116
|
const txHash: string | undefined = refund?.payment_details?.[paymentMethod.type]?.tx_hash;
|
|
@@ -140,8 +139,8 @@ export class SubscriptionRefundSucceededEmailTemplate
|
|
|
140
139
|
unusedDuration,
|
|
141
140
|
|
|
142
141
|
viewSubscriptionLink,
|
|
143
|
-
viewInvoiceLink,
|
|
144
142
|
viewTxHashLink,
|
|
143
|
+
refund,
|
|
145
144
|
};
|
|
146
145
|
}
|
|
147
146
|
|
|
@@ -163,8 +162,87 @@ export class SubscriptionRefundSucceededEmailTemplate
|
|
|
163
162
|
|
|
164
163
|
viewSubscriptionLink,
|
|
165
164
|
viewTxHashLink,
|
|
165
|
+
refund,
|
|
166
166
|
} = await this.getContext();
|
|
167
167
|
|
|
168
|
+
if (refund.type === 'stake_return') {
|
|
169
|
+
return {
|
|
170
|
+
title: `${translate('notification.subscriptionStakeReturnSucceeded.title', locale, {
|
|
171
|
+
productName,
|
|
172
|
+
})}`,
|
|
173
|
+
body: `${translate('notification.subscriptionStakeReturnSucceeded.body', locale, {
|
|
174
|
+
at,
|
|
175
|
+
productName,
|
|
176
|
+
refundInfo,
|
|
177
|
+
})}`,
|
|
178
|
+
attachments: [
|
|
179
|
+
{
|
|
180
|
+
type: 'section',
|
|
181
|
+
fields: [
|
|
182
|
+
{
|
|
183
|
+
type: 'text',
|
|
184
|
+
data: {
|
|
185
|
+
type: 'plain',
|
|
186
|
+
color: '#9397A1',
|
|
187
|
+
text: translate('notification.common.account', locale),
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
type: 'text',
|
|
192
|
+
data: {
|
|
193
|
+
type: 'plain',
|
|
194
|
+
text: userDid,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
type: 'text',
|
|
199
|
+
data: {
|
|
200
|
+
type: 'plain',
|
|
201
|
+
color: '#9397A1',
|
|
202
|
+
text: translate('notification.common.product', locale),
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
type: 'text',
|
|
207
|
+
data: {
|
|
208
|
+
type: 'plain',
|
|
209
|
+
text: productName,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
type: 'text',
|
|
214
|
+
data: {
|
|
215
|
+
type: 'plain',
|
|
216
|
+
color: '#9397A1',
|
|
217
|
+
text: translate('notification.common.returnAmount', locale),
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
type: 'text',
|
|
222
|
+
data: {
|
|
223
|
+
type: 'plain',
|
|
224
|
+
text: refundInfo,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
// @ts-ignore
|
|
231
|
+
actions: [
|
|
232
|
+
{
|
|
233
|
+
name: translate('notification.common.viewSubscription', locale),
|
|
234
|
+
title: translate('notification.common.viewSubscription', locale),
|
|
235
|
+
link: viewSubscriptionLink,
|
|
236
|
+
},
|
|
237
|
+
viewTxHashLink && {
|
|
238
|
+
name: translate('notification.common.viewTxHash', locale),
|
|
239
|
+
title: translate('notification.common.viewTxHash', locale),
|
|
240
|
+
link: viewTxHashLink as string,
|
|
241
|
+
},
|
|
242
|
+
].filter(Boolean),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
168
246
|
const template: BaseEmailTemplateType = {
|
|
169
247
|
title: `${translate('notification.subscriptionRefundSucceeded.title', locale, {
|
|
170
248
|
productName,
|
|
@@ -254,21 +332,26 @@ export class SubscriptionRefundSucceededEmailTemplate
|
|
|
254
332
|
text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
|
|
255
333
|
},
|
|
256
334
|
},
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
335
|
+
|
|
336
|
+
...(unusedPeriodStart && unusedPeriodEnd && unusedDuration
|
|
337
|
+
? [
|
|
338
|
+
{
|
|
339
|
+
type: 'text',
|
|
340
|
+
data: {
|
|
341
|
+
type: 'plain',
|
|
342
|
+
color: '#9397A1',
|
|
343
|
+
text: translate('notification.common.refundPeriod', locale),
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
type: 'text',
|
|
348
|
+
data: {
|
|
349
|
+
type: 'plain',
|
|
350
|
+
text: `${unusedPeriodStart} ~ ${unusedPeriodEnd}(${unusedDuration})`,
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
]
|
|
354
|
+
: []),
|
|
272
355
|
].filter(Boolean),
|
|
273
356
|
},
|
|
274
357
|
].filter(Boolean),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
3
3
|
import { fromUnitToToken, toDid } from '@ocap/util';
|
|
4
4
|
import prettyMsI18n from 'pretty-ms-i18n';
|
|
5
|
-
|
|
5
|
+
import logger from '@api/libs/logger';
|
|
6
6
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
7
7
|
import { translate } from '../../../locales';
|
|
8
8
|
import {
|
|
@@ -48,6 +48,7 @@ interface SubscriptionRenewedEmailTemplateContext {
|
|
|
48
48
|
viewSubscriptionLink: string;
|
|
49
49
|
viewInvoiceLink: string;
|
|
50
50
|
viewTxHashLink: string | undefined;
|
|
51
|
+
invoice: Invoice;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<SubscriptionRenewedEmailTemplateContext> {
|
|
@@ -76,6 +77,7 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
76
77
|
? await Invoice.findByPk(this.options.invoiceId)
|
|
77
78
|
: await Invoice.findByPk(subscription.latest_invoice_id)
|
|
78
79
|
) as Invoice;
|
|
80
|
+
|
|
79
81
|
const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
80
82
|
const paymentCurrency = (await PaymentCurrency.findOne({
|
|
81
83
|
where: {
|
|
@@ -148,10 +150,11 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
148
150
|
viewSubscriptionLink,
|
|
149
151
|
viewInvoiceLink,
|
|
150
152
|
viewTxHashLink,
|
|
153
|
+
invoice,
|
|
151
154
|
};
|
|
152
155
|
}
|
|
153
156
|
|
|
154
|
-
async getTemplate(): Promise<BaseEmailTemplateType> {
|
|
157
|
+
async getTemplate(): Promise<BaseEmailTemplateType | null> {
|
|
155
158
|
const {
|
|
156
159
|
locale,
|
|
157
160
|
productName,
|
|
@@ -166,8 +169,13 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
166
169
|
viewSubscriptionLink,
|
|
167
170
|
viewInvoiceLink,
|
|
168
171
|
viewTxHashLink,
|
|
172
|
+
invoice,
|
|
169
173
|
} = await this.getContext();
|
|
170
174
|
|
|
175
|
+
if (invoice.total === '0') {
|
|
176
|
+
logger.info('Invoice amount is 0, skipping renewed notification');
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
171
179
|
const template: BaseEmailTemplateType = {
|
|
172
180
|
title: `${translate('notification.subscriptionRenewed.title', locale, {
|
|
173
181
|
productName,
|