payment-kit 1.24.4 → 1.25.1
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/index.ts +3 -0
- package/api/src/libs/credit-utils.ts +21 -0
- package/api/src/libs/discount/discount.ts +13 -0
- package/api/src/libs/env.ts +5 -0
- package/api/src/libs/error.ts +14 -0
- package/api/src/libs/exchange-rate/coingecko-provider.ts +193 -0
- package/api/src/libs/exchange-rate/coinmarketcap-provider.ts +180 -0
- package/api/src/libs/exchange-rate/index.ts +5 -0
- package/api/src/libs/exchange-rate/service.ts +583 -0
- package/api/src/libs/exchange-rate/token-address-mapping.ts +84 -0
- package/api/src/libs/exchange-rate/token-addresses.json +2147 -0
- package/api/src/libs/exchange-rate/token-data-provider.ts +142 -0
- package/api/src/libs/exchange-rate/types.ts +114 -0
- package/api/src/libs/exchange-rate/validator.ts +319 -0
- package/api/src/libs/invoice-quote.ts +158 -0
- package/api/src/libs/invoice.ts +143 -7
- package/api/src/libs/math-utils.ts +46 -0
- package/api/src/libs/notification/template/billing-discrepancy.ts +3 -4
- package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +174 -79
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +2 -3
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +3 -3
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +3 -3
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +2 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +9 -4
- package/api/src/libs/notification/template/exchange-rate-alert.ts +202 -0
- package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +203 -0
- package/api/src/libs/notification/template/subscription-slippage-warning.ts +212 -0
- package/api/src/libs/notification/template/subscription-will-canceled.ts +2 -2
- package/api/src/libs/notification/template/subscription-will-renew.ts +22 -8
- package/api/src/libs/payment.ts +3 -1
- package/api/src/libs/price.ts +4 -1
- package/api/src/libs/queue/index.ts +8 -0
- package/api/src/libs/quote-service.ts +1132 -0
- package/api/src/libs/quote-validation.ts +388 -0
- package/api/src/libs/session.ts +686 -39
- package/api/src/libs/slippage.ts +135 -0
- package/api/src/libs/subscription.ts +185 -15
- package/api/src/libs/util.ts +64 -3
- package/api/src/locales/en.ts +50 -0
- package/api/src/locales/zh.ts +48 -0
- package/api/src/queues/auto-recharge.ts +295 -21
- package/api/src/queues/exchange-rate-health.ts +242 -0
- package/api/src/queues/invoice.ts +48 -1
- package/api/src/queues/notification.ts +167 -1
- package/api/src/queues/payment.ts +177 -7
- package/api/src/queues/refund.ts +41 -9
- package/api/src/queues/subscription.ts +436 -6
- package/api/src/routes/auto-recharge-configs.ts +71 -6
- package/api/src/routes/checkout-sessions.ts +1730 -81
- package/api/src/routes/connect/auto-recharge-auth.ts +2 -0
- package/api/src/routes/connect/change-payer.ts +2 -0
- package/api/src/routes/connect/change-payment.ts +61 -8
- package/api/src/routes/connect/change-plan.ts +161 -17
- package/api/src/routes/connect/collect.ts +9 -6
- package/api/src/routes/connect/delegation.ts +1 -0
- package/api/src/routes/connect/pay.ts +157 -0
- package/api/src/routes/connect/setup.ts +32 -10
- package/api/src/routes/connect/shared.ts +159 -13
- package/api/src/routes/connect/subscribe.ts +32 -9
- package/api/src/routes/credit-grants.ts +99 -0
- package/api/src/routes/exchange-rate-providers.ts +248 -0
- package/api/src/routes/exchange-rates.ts +87 -0
- package/api/src/routes/index.ts +4 -0
- package/api/src/routes/invoices.ts +280 -2
- package/api/src/routes/payment-links.ts +13 -0
- package/api/src/routes/prices.ts +84 -2
- package/api/src/routes/subscriptions.ts +526 -15
- package/api/src/store/migrations/20251220-dynamic-pricing.ts +245 -0
- package/api/src/store/migrations/20251223-exchange-rate-provider-type.ts +28 -0
- package/api/src/store/migrations/20260110-add-quote-locked-at.ts +23 -0
- package/api/src/store/migrations/20260112-add-checkout-session-slippage-percent.ts +22 -0
- package/api/src/store/migrations/20260113-add-price-quote-slippage-fields.ts +45 -0
- package/api/src/store/migrations/20260116-subscription-slippage.ts +21 -0
- package/api/src/store/migrations/20260120-auto-recharge-slippage.ts +21 -0
- package/api/src/store/models/auto-recharge-config.ts +12 -0
- package/api/src/store/models/checkout-session.ts +7 -0
- package/api/src/store/models/exchange-rate-provider.ts +225 -0
- package/api/src/store/models/index.ts +6 -0
- package/api/src/store/models/payment-intent.ts +6 -0
- package/api/src/store/models/price-quote.ts +284 -0
- package/api/src/store/models/price.ts +53 -5
- package/api/src/store/models/subscription.ts +11 -0
- package/api/src/store/models/types.ts +61 -1
- package/api/tests/libs/change-payment-plan.spec.ts +282 -0
- package/api/tests/libs/exchange-rate-service.spec.ts +341 -0
- package/api/tests/libs/quote-service.spec.ts +199 -0
- package/api/tests/libs/session.spec.ts +464 -0
- package/api/tests/libs/slippage.spec.ts +109 -0
- package/api/tests/libs/token-data-provider.spec.ts +267 -0
- package/api/tests/models/exchange-rate-provider.spec.ts +121 -0
- package/api/tests/models/price-dynamic.spec.ts +100 -0
- package/api/tests/models/price-quote.spec.ts +112 -0
- package/api/tests/routes/exchange-rate-providers.spec.ts +215 -0
- package/api/tests/routes/subscription-slippage.spec.ts +254 -0
- package/blocklet.yml +1 -1
- package/package.json +7 -6
- package/src/components/customer/credit-overview.tsx +14 -0
- package/src/components/discount/discount-info.tsx +8 -2
- package/src/components/invoice/list.tsx +146 -16
- package/src/components/invoice/table.tsx +276 -71
- package/src/components/invoice-pdf/template.tsx +3 -7
- package/src/components/metadata/form.tsx +6 -8
- package/src/components/price/form.tsx +519 -149
- package/src/components/promotion/active-redemptions.tsx +5 -3
- package/src/components/quote/info.tsx +234 -0
- package/src/hooks/subscription.ts +132 -2
- package/src/locales/en.tsx +145 -0
- package/src/locales/zh.tsx +143 -1
- package/src/pages/admin/billing/invoices/detail.tsx +41 -4
- package/src/pages/admin/products/exchange-rate-providers/edit-dialog.tsx +354 -0
- package/src/pages/admin/products/exchange-rate-providers/index.tsx +363 -0
- package/src/pages/admin/products/index.tsx +12 -1
- package/src/pages/customer/invoice/detail.tsx +36 -12
- package/src/pages/customer/subscription/change-payment.tsx +65 -3
- package/src/pages/customer/subscription/change-plan.tsx +207 -38
- package/src/pages/customer/subscription/detail.tsx +599 -419
|
@@ -9,15 +9,28 @@ import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
|
9
9
|
import { getCustomerInvoicePageUrl } from '../../invoice';
|
|
10
10
|
import { SufficientForPaymentResult, getPaymentDetail } from '../../payment';
|
|
11
11
|
import { formatTime } from '../../time';
|
|
12
|
-
import { formatCurrencyInfo } from '../../util';
|
|
12
|
+
import { formatCurrencyInfo, getCustomerAutoRechargeSettingsUrl } from '../../util';
|
|
13
13
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
14
14
|
|
|
15
|
+
// Extended result type that includes skipped reasons
|
|
16
|
+
export interface AutoRechargeResult extends Partial<SufficientForPaymentResult> {
|
|
17
|
+
sufficient?: boolean;
|
|
18
|
+
reason?: string;
|
|
19
|
+
// Skipped scenario fields
|
|
20
|
+
currentRate?: string;
|
|
21
|
+
minAcceptableRate?: string;
|
|
22
|
+
// Additional context for better notifications
|
|
23
|
+
paymentCurrencySymbol?: string;
|
|
24
|
+
paymentCurrencyName?: string;
|
|
25
|
+
creditCurrencyName?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
export interface CustomerAutoRechargeFailedEmailTemplateOptions {
|
|
16
29
|
customerId: string;
|
|
17
30
|
autoRechargeConfigId: string;
|
|
18
|
-
invoiceId
|
|
19
|
-
paymentIntentId
|
|
20
|
-
result:
|
|
31
|
+
invoiceId?: string; // Optional for skipped scenarios
|
|
32
|
+
paymentIntentId?: string; // Optional for skipped scenarios
|
|
33
|
+
result: AutoRechargeResult;
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
interface CustomerAutoRechargeFailedEmailTemplateContext {
|
|
@@ -25,10 +38,12 @@ interface CustomerAutoRechargeFailedEmailTemplateContext {
|
|
|
25
38
|
userDid: string;
|
|
26
39
|
at: string;
|
|
27
40
|
reason: string;
|
|
28
|
-
paymentInfo
|
|
29
|
-
autoRechargeAmount
|
|
41
|
+
paymentInfo?: string;
|
|
42
|
+
autoRechargeAmount?: string;
|
|
30
43
|
creditCurrencyName: string;
|
|
31
|
-
viewInvoiceLink
|
|
44
|
+
viewInvoiceLink?: string;
|
|
45
|
+
settingsUrl?: string;
|
|
46
|
+
isSkipped: boolean;
|
|
32
47
|
}
|
|
33
48
|
|
|
34
49
|
export class CustomerAutoRechargeFailedEmailTemplate
|
|
@@ -40,7 +55,34 @@ export class CustomerAutoRechargeFailedEmailTemplate
|
|
|
40
55
|
this.options = options;
|
|
41
56
|
}
|
|
42
57
|
|
|
43
|
-
private
|
|
58
|
+
private isSkippedScenario(): boolean {
|
|
59
|
+
const skippedReasons = ['slippage_exceeded', 'exchange_rate_not_supported', 'exchange_rate_fetch_failed'];
|
|
60
|
+
return skippedReasons.includes(this.options.result?.reason || '');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Get the title key based on the skip reason
|
|
64
|
+
private getSkippedTitleKey(): string {
|
|
65
|
+
const reason = this.options.result?.reason;
|
|
66
|
+
const titleMap: Record<string, string> = {
|
|
67
|
+
slippage_exceeded: 'titleSkippedSlippageExceeded',
|
|
68
|
+
exchange_rate_not_supported: 'titleSkippedExchangeRateNotSupported',
|
|
69
|
+
exchange_rate_fetch_failed: 'titleSkippedExchangeRateFetchFailed',
|
|
70
|
+
};
|
|
71
|
+
return `notification.autoRechargeFailed.${titleMap[reason || ''] || 'titleSkipped'}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private getSkippedReason(locale: string): string {
|
|
75
|
+
const { result } = this.options;
|
|
76
|
+
const i18nText = `notification.autoRechargeFailed.reason.${camelCase(result.reason as string)}`;
|
|
77
|
+
|
|
78
|
+
return translate(i18nText, locale, {
|
|
79
|
+
currentRate: result.currentRate || '-',
|
|
80
|
+
minAcceptableRate: result.minAcceptableRate || '-',
|
|
81
|
+
paymentCurrency: result.paymentCurrencySymbol || result.paymentCurrencyName || '-',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async getFailedReason(userDid: string, invoice: Invoice, locale: string): Promise<string> {
|
|
44
86
|
if (this.options.result?.sufficient) {
|
|
45
87
|
throw new Error(`SufficientForPaymentResult.sufficient should be false: ${JSON.stringify(this.options.result)}`);
|
|
46
88
|
}
|
|
@@ -75,6 +117,38 @@ export class CustomerAutoRechargeFailedEmailTemplate
|
|
|
75
117
|
throw new Error(`AutoRechargeConfig not found: ${this.options.autoRechargeConfigId}`);
|
|
76
118
|
}
|
|
77
119
|
|
|
120
|
+
const userDid: string = customer.did;
|
|
121
|
+
const locale = await getUserLocale(userDid);
|
|
122
|
+
const at: string = formatTime(Date.now());
|
|
123
|
+
const isSkipped = this.isSkippedScenario();
|
|
124
|
+
|
|
125
|
+
// Get credit currency
|
|
126
|
+
const creditCurrency = await PaymentCurrency.findByPk(autoRechargeConfig.currency_id);
|
|
127
|
+
if (!creditCurrency) {
|
|
128
|
+
throw new Error(`Credit currency not found: ${autoRechargeConfig.currency_id}`);
|
|
129
|
+
}
|
|
130
|
+
const creditCurrencyName = creditCurrency.name || 'Credits';
|
|
131
|
+
|
|
132
|
+
// Handle skipped scenario (no invoice)
|
|
133
|
+
if (isSkipped) {
|
|
134
|
+
const reason = this.getSkippedReason(locale);
|
|
135
|
+
const settingsUrl = getCustomerAutoRechargeSettingsUrl({
|
|
136
|
+
locale,
|
|
137
|
+
userDid,
|
|
138
|
+
currencyId: autoRechargeConfig.currency_id,
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
locale,
|
|
142
|
+
userDid,
|
|
143
|
+
at,
|
|
144
|
+
reason,
|
|
145
|
+
creditCurrencyName,
|
|
146
|
+
settingsUrl,
|
|
147
|
+
isSkipped: true,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Handle normal failed scenario (with invoice)
|
|
78
152
|
const invoice = await Invoice.findByPk(this.options.invoiceId);
|
|
79
153
|
if (!invoice) {
|
|
80
154
|
throw new Error(`Invoice not found: ${this.options.invoiceId}`);
|
|
@@ -86,25 +160,14 @@ export class CustomerAutoRechargeFailedEmailTemplate
|
|
|
86
160
|
},
|
|
87
161
|
})) as PaymentCurrency;
|
|
88
162
|
|
|
89
|
-
const
|
|
90
|
-
const locale = await getUserLocale(userDid);
|
|
91
|
-
const at: string = formatTime(Date.now());
|
|
92
|
-
const reason: string = await this.getReason(userDid, invoice, locale);
|
|
163
|
+
const reason: string = await this.getFailedReason(userDid, invoice, locale);
|
|
93
164
|
|
|
94
165
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
|
|
95
166
|
const paymentInfo: string = formatCurrencyInfo(invoice.amount_remaining, paymentCurrency, paymentMethod);
|
|
96
167
|
|
|
97
|
-
//
|
|
98
|
-
const creditCurrency = await PaymentCurrency.findByPk(autoRechargeConfig.currency_id);
|
|
99
|
-
if (!creditCurrency) {
|
|
100
|
-
throw new Error(`Credit currency not found: ${autoRechargeConfig.currency_id}`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 格式化自动充值金额(根据 invoice 的金额)
|
|
168
|
+
// Format auto recharge amount (based on invoice amount)
|
|
104
169
|
const autoRechargeAmount = formatCurrencyInfo(invoice.amount_remaining, paymentCurrency, paymentMethod);
|
|
105
170
|
|
|
106
|
-
const creditCurrencyName = creditCurrency.name || 'Credits';
|
|
107
|
-
|
|
108
171
|
const viewInvoiceLink = getCustomerInvoicePageUrl({
|
|
109
172
|
invoiceId: invoice.id,
|
|
110
173
|
userDid,
|
|
@@ -121,15 +184,100 @@ export class CustomerAutoRechargeFailedEmailTemplate
|
|
|
121
184
|
autoRechargeAmount,
|
|
122
185
|
creditCurrencyName,
|
|
123
186
|
viewInvoiceLink,
|
|
187
|
+
isSkipped: false,
|
|
124
188
|
};
|
|
125
189
|
}
|
|
126
190
|
|
|
127
191
|
async getTemplate(): Promise<BaseEmailTemplateType> {
|
|
128
|
-
const
|
|
192
|
+
const context = await this.getContext();
|
|
193
|
+
const { locale, userDid, at, reason, paymentInfo, creditCurrencyName, viewInvoiceLink, settingsUrl, isSkipped } =
|
|
194
|
+
context;
|
|
195
|
+
|
|
196
|
+
// Use different title/body for skipped scenarios
|
|
197
|
+
// For skipped scenarios, use reason-specific title
|
|
198
|
+
const titleKey = isSkipped ? this.getSkippedTitleKey() : 'notification.autoRechargeFailed.title';
|
|
199
|
+
const bodyKey = isSkipped ? 'notification.autoRechargeFailed.bodySkipped' : 'notification.autoRechargeFailed.body';
|
|
200
|
+
|
|
201
|
+
const fields = [
|
|
202
|
+
{
|
|
203
|
+
type: 'text',
|
|
204
|
+
data: {
|
|
205
|
+
type: 'plain',
|
|
206
|
+
color: '#9397A1',
|
|
207
|
+
text: translate('notification.common.account', locale),
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
type: 'text',
|
|
212
|
+
data: {
|
|
213
|
+
type: 'plain',
|
|
214
|
+
text: userDid,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
// Add payment amount field only for non-skipped scenarios
|
|
220
|
+
if (!isSkipped && paymentInfo) {
|
|
221
|
+
fields.push(
|
|
222
|
+
{
|
|
223
|
+
type: 'text',
|
|
224
|
+
data: {
|
|
225
|
+
type: 'plain',
|
|
226
|
+
color: '#9397A1',
|
|
227
|
+
text: translate('notification.common.paymentAmount', locale),
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
type: 'text',
|
|
232
|
+
data: {
|
|
233
|
+
type: 'plain',
|
|
234
|
+
text: paymentInfo,
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Add reason field
|
|
241
|
+
fields.push(
|
|
242
|
+
{
|
|
243
|
+
type: 'text',
|
|
244
|
+
data: {
|
|
245
|
+
type: 'plain',
|
|
246
|
+
color: '#9397A1',
|
|
247
|
+
text: translate('notification.common.failReason', locale),
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
type: 'text',
|
|
252
|
+
data: {
|
|
253
|
+
type: 'plain',
|
|
254
|
+
color: isSkipped ? '#FF9500' : '#FF0000', // Orange for skipped, red for failed
|
|
255
|
+
text: reason,
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Build actions based on scenario
|
|
261
|
+
const actions = [];
|
|
262
|
+
if (isSkipped && settingsUrl) {
|
|
263
|
+
// For skipped scenarios, link to settings to adjust slippage config
|
|
264
|
+
actions.push({
|
|
265
|
+
name: translate('notification.autoRechargeFailed.adjustSettings', locale),
|
|
266
|
+
title: translate('notification.autoRechargeFailed.adjustSettings', locale),
|
|
267
|
+
link: settingsUrl,
|
|
268
|
+
});
|
|
269
|
+
} else if (viewInvoiceLink) {
|
|
270
|
+
// For failed scenarios, link to invoice to retry payment
|
|
271
|
+
actions.push({
|
|
272
|
+
name: translate('notification.common.renewNow', locale),
|
|
273
|
+
title: translate('notification.common.renewNow', locale),
|
|
274
|
+
link: viewInvoiceLink,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
129
277
|
|
|
130
278
|
const template: BaseEmailTemplateType = {
|
|
131
|
-
title: translate(
|
|
132
|
-
body: translate(
|
|
279
|
+
title: translate(titleKey, locale),
|
|
280
|
+
body: translate(bodyKey, locale, {
|
|
133
281
|
at,
|
|
134
282
|
creditCurrencyName,
|
|
135
283
|
}),
|
|
@@ -137,64 +285,11 @@ export class CustomerAutoRechargeFailedEmailTemplate
|
|
|
137
285
|
attachments: [
|
|
138
286
|
{
|
|
139
287
|
type: 'section',
|
|
140
|
-
fields:
|
|
141
|
-
{
|
|
142
|
-
type: 'text',
|
|
143
|
-
data: {
|
|
144
|
-
type: 'plain',
|
|
145
|
-
color: '#9397A1',
|
|
146
|
-
text: translate('notification.common.account', locale),
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
type: 'text',
|
|
151
|
-
data: {
|
|
152
|
-
type: 'plain',
|
|
153
|
-
text: userDid,
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
type: 'text',
|
|
158
|
-
data: {
|
|
159
|
-
type: 'plain',
|
|
160
|
-
color: '#9397A1',
|
|
161
|
-
text: translate('notification.common.paymentAmount', locale),
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
type: 'text',
|
|
166
|
-
data: {
|
|
167
|
-
type: 'plain',
|
|
168
|
-
text: paymentInfo,
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
type: 'text',
|
|
173
|
-
data: {
|
|
174
|
-
type: 'plain',
|
|
175
|
-
color: '#9397A1',
|
|
176
|
-
text: translate('notification.common.failReason', locale),
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
type: 'text',
|
|
181
|
-
data: {
|
|
182
|
-
type: 'plain',
|
|
183
|
-
color: '#FF0000',
|
|
184
|
-
text: reason,
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
].filter(Boolean),
|
|
288
|
+
fields: fields.filter(Boolean),
|
|
188
289
|
},
|
|
189
290
|
].filter(Boolean),
|
|
190
291
|
// @ts-ignore
|
|
191
|
-
actions
|
|
192
|
-
{
|
|
193
|
-
name: translate('notification.common.renewNow', locale),
|
|
194
|
-
title: translate('notification.common.renewNow', locale),
|
|
195
|
-
link: viewInvoiceLink,
|
|
196
|
-
},
|
|
197
|
-
].filter(Boolean),
|
|
292
|
+
actions,
|
|
198
293
|
};
|
|
199
294
|
|
|
200
295
|
return template;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/brace-style */
|
|
2
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
3
|
-
import { fromUnitToToken } from '@ocap/util';
|
|
4
3
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
5
4
|
import { translate } from '../../../locales';
|
|
6
5
|
import { CreditGrant, Customer, PaymentCurrency } from '../../../store/models';
|
|
7
6
|
import { formatTime } from '../../time';
|
|
8
|
-
import { formatCreditAmount,
|
|
7
|
+
import { formatCreditAmount, formatTokenAmount, getCustomerIndexUrl } from '../../util';
|
|
9
8
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
10
9
|
|
|
11
10
|
export interface CustomerCreditGrantGrantedEmailTemplateOptions {
|
|
@@ -60,7 +59,7 @@ export class CustomerCreditGrantGrantedEmailTemplate
|
|
|
60
59
|
userDid,
|
|
61
60
|
currencySymbol,
|
|
62
61
|
grantedAmount: formatCreditAmount(
|
|
63
|
-
|
|
62
|
+
formatTokenAmount(creditGrant.amount.toString(), paymentCurrency.decimal) || '0',
|
|
64
63
|
currencySymbol
|
|
65
64
|
),
|
|
66
65
|
expiresAt,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/brace-style */
|
|
2
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
3
|
-
import { BN
|
|
3
|
+
import { BN } from '@ocap/util';
|
|
4
4
|
import { withQuery } from 'ufo';
|
|
5
5
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
6
6
|
import { translate } from '../../../locales';
|
|
@@ -9,7 +9,7 @@ import { getMainProductName } from '../../product';
|
|
|
9
9
|
import { getCustomerSubscriptionPageUrl } from '../../subscription';
|
|
10
10
|
import { formatTime } from '../../time';
|
|
11
11
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
12
|
-
import { formatCreditAmount, getConnectQueryParam, getCustomerIndexUrl
|
|
12
|
+
import { formatCreditAmount, formatTokenAmount, getConnectQueryParam, getCustomerIndexUrl } from '../../util';
|
|
13
13
|
import { getRechargePaymentUrl } from '../../currency';
|
|
14
14
|
|
|
15
15
|
export interface CustomerCreditInsufficientEmailTemplateOptions {
|
|
@@ -85,7 +85,7 @@ export class CustomerCreditInsufficientEmailTemplate
|
|
|
85
85
|
|
|
86
86
|
const pendingAmountValue = this.options.pendingAmount || '0';
|
|
87
87
|
const pendingAmountBN = new BN(pendingAmountValue);
|
|
88
|
-
const formattedPending =
|
|
88
|
+
const formattedPending = formatTokenAmount(pendingAmountValue, paymentCurrency.decimal) || '0';
|
|
89
89
|
|
|
90
90
|
return {
|
|
91
91
|
locale,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/brace-style */
|
|
2
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
3
3
|
import { withQuery } from 'ufo';
|
|
4
|
-
import { BN
|
|
4
|
+
import { BN } from '@ocap/util';
|
|
5
5
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
6
6
|
import { translate } from '../../../locales';
|
|
7
7
|
import { Customer, PaymentCurrency, CreditGrant } from '../../../store/models';
|
|
8
8
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
9
|
-
import { formatCreditAmount, getConnectQueryParam, getCustomerIndexUrl
|
|
9
|
+
import { formatCreditAmount, formatTokenAmount, getConnectQueryParam, getCustomerIndexUrl } from '../../util';
|
|
10
10
|
import { getRechargePaymentUrl } from '../../currency';
|
|
11
11
|
|
|
12
12
|
export interface CustomerCreditLowBalanceEmailTemplateOptions {
|
|
@@ -102,7 +102,7 @@ export class CustomerCreditLowBalanceEmailTemplate
|
|
|
102
102
|
const userDid = customer.did;
|
|
103
103
|
const locale = await getUserLocale(userDid);
|
|
104
104
|
|
|
105
|
-
const formattedAmount =
|
|
105
|
+
const formattedAmount = formatTokenAmount(remainingAmount.toString(), paymentCurrency.decimal);
|
|
106
106
|
const available = formatCreditAmount(formattedAmount || '0', paymentCurrency.symbol);
|
|
107
107
|
const isCritical = currentPercentage < 1;
|
|
108
108
|
const lowBalancePercentage = isCritical
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/brace-style */
|
|
2
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
3
|
-
import { fromUnitToToken } from '@ocap/util';
|
|
4
3
|
import { getUrl } from '@blocklet/sdk';
|
|
5
4
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
6
5
|
import { translate } from '../../../locales';
|
|
7
6
|
import { CheckoutSession, Customer, PaymentLink, PaymentMethod, Payout } from '../../../store/models';
|
|
8
7
|
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
9
8
|
import { formatTime } from '../../time';
|
|
10
|
-
import { getCustomerProfileUrl, getExplorerLink, getUserOrAppInfo } from '../../util';
|
|
9
|
+
import { formatTokenAmount, getCustomerProfileUrl, getExplorerLink, getUserOrAppInfo } from '../../util';
|
|
11
10
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
12
11
|
import { getCustomerPayoutPageUrl } from '../../payout';
|
|
13
12
|
import { PaymentIntent } from '../../../store/models/payment-intent';
|
|
@@ -105,7 +104,7 @@ export class CustomerRevenueSucceededEmailTemplate
|
|
|
105
104
|
throw new Error(`Payer not found for paymentIntent: ${payout.paymentIntent?.customer_id}`);
|
|
106
105
|
}
|
|
107
106
|
|
|
108
|
-
const paymentInfo: string = `${
|
|
107
|
+
const paymentInfo: string = `${formatTokenAmount(payout.amount, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
109
108
|
const userInfo = await getUserOrAppInfo(payer.did);
|
|
110
109
|
const revenueDetail = {
|
|
111
110
|
url: getCustomerProfileUrl({ userDid: payer.did, locale }),
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/brace-style */
|
|
2
2
|
/* eslint-disable @typescript-eslint/indent */
|
|
3
|
-
import { fromUnitToToken } from '@ocap/util';
|
|
4
3
|
import isEmpty from 'lodash/isEmpty';
|
|
5
4
|
import pWaitFor from 'p-wait-for';
|
|
6
5
|
import type { LiteralUnion } from 'type-fest';
|
|
@@ -22,7 +21,13 @@ import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
|
22
21
|
import { getCustomerInvoicePageUrl } from '../../invoice';
|
|
23
22
|
import logger from '../../logger';
|
|
24
23
|
import { formatTime } from '../../time';
|
|
25
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
formatTokenAmount,
|
|
26
|
+
getBlockletJson,
|
|
27
|
+
getCustomerProfileUrl,
|
|
28
|
+
getExplorerLink,
|
|
29
|
+
getUserOrAppInfo,
|
|
30
|
+
} from '../../util';
|
|
26
31
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
27
32
|
|
|
28
33
|
export interface CustomerRewardSucceededEmailTemplateOptions {
|
|
@@ -140,7 +145,7 @@ export class CustomerRewardSucceededEmailTemplate
|
|
|
140
145
|
const locale = await getUserLocale(userDid);
|
|
141
146
|
const at: string = formatTime(checkoutSession.created_at);
|
|
142
147
|
|
|
143
|
-
const paymentInfo: string = `${
|
|
148
|
+
const paymentInfo: string = `${formatTokenAmount(checkoutSession?.amount_total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
144
149
|
|
|
145
150
|
const rewardDetail = await this.getRewardDetail({
|
|
146
151
|
paymentIntent,
|
|
@@ -229,7 +234,7 @@ export class CustomerRewardSucceededEmailTemplate
|
|
|
229
234
|
url: users[index]?.url || getCustomerProfileUrl({ userDid: x.address, locale }),
|
|
230
235
|
title: translate('notification.customerRewardSucceeded.received', locale, {
|
|
231
236
|
address: users[index]?.name || x.address,
|
|
232
|
-
amount: `${
|
|
237
|
+
amount: `${formatTokenAmount(x.share, paymentCurrency.decimal)} ${paymentCurrency.symbol}`,
|
|
233
238
|
}),
|
|
234
239
|
logo: users[index]?.avatar ? users[index]?.avatar : getUrl('/methods/default.png'),
|
|
235
240
|
appDID: x.address,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
2
|
+
import { withQuery } from 'ufo';
|
|
3
|
+
import { getConnectQueryParam, getOwnerDid } from '../../util';
|
|
4
|
+
import { translate } from '../../../locales';
|
|
5
|
+
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
6
|
+
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Exchange Rate Alert Types
|
|
10
|
+
*/
|
|
11
|
+
export type ExchangeRateAlertType = 'spread_exceeded' | 'providers_unavailable';
|
|
12
|
+
|
|
13
|
+
export interface ExchangeRateAlertEmailTemplateOptions {
|
|
14
|
+
alertType: ExchangeRateAlertType;
|
|
15
|
+
symbol: string;
|
|
16
|
+
severity: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
// For spread_exceeded
|
|
19
|
+
spreadPercent?: string;
|
|
20
|
+
threshold?: number;
|
|
21
|
+
// Provider info
|
|
22
|
+
providers: Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
rate?: string;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ExchangeRateAlertEmailTemplateContext {
|
|
30
|
+
locale: string;
|
|
31
|
+
userDid: string;
|
|
32
|
+
alertType: ExchangeRateAlertType;
|
|
33
|
+
symbol: string;
|
|
34
|
+
severity: string;
|
|
35
|
+
timestamp: string;
|
|
36
|
+
spreadPercent?: string;
|
|
37
|
+
threshold?: number;
|
|
38
|
+
providers: Array<{
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
rate?: string;
|
|
42
|
+
}>;
|
|
43
|
+
viewProvidersLink: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class ExchangeRateAlertEmailTemplate implements BaseEmailTemplate<ExchangeRateAlertEmailTemplateContext> {
|
|
47
|
+
options: ExchangeRateAlertEmailTemplateOptions;
|
|
48
|
+
|
|
49
|
+
constructor(options: ExchangeRateAlertEmailTemplateOptions) {
|
|
50
|
+
this.options = options;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async getContext(): Promise<ExchangeRateAlertEmailTemplateContext> {
|
|
54
|
+
const userDid = await getOwnerDid();
|
|
55
|
+
if (!userDid) {
|
|
56
|
+
throw new Error('get owner did failed');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const locale = await getUserLocale(userDid);
|
|
60
|
+
|
|
61
|
+
const viewProvidersLink = getUrl(
|
|
62
|
+
withQuery('admin/settings/exchange-rate-providers', {
|
|
63
|
+
locale,
|
|
64
|
+
...getConnectQueryParam({ userDid }),
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
userDid,
|
|
70
|
+
locale,
|
|
71
|
+
alertType: this.options.alertType,
|
|
72
|
+
symbol: this.options.symbol,
|
|
73
|
+
severity: this.options.severity,
|
|
74
|
+
timestamp: this.options.timestamp,
|
|
75
|
+
spreadPercent: this.options.spreadPercent,
|
|
76
|
+
threshold: this.options.threshold,
|
|
77
|
+
providers: this.options.providers,
|
|
78
|
+
viewProvidersLink,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async getTemplate(): Promise<BaseEmailTemplateType> {
|
|
83
|
+
const { locale, alertType, symbol, spreadPercent, threshold, providers, timestamp, viewProvidersLink } =
|
|
84
|
+
await this.getContext();
|
|
85
|
+
|
|
86
|
+
const titleKey = `notification.exchangeRateAlert.${alertType}.title`;
|
|
87
|
+
const bodyKey = `notification.exchangeRateAlert.${alertType}.body`;
|
|
88
|
+
|
|
89
|
+
const titleParams: Record<string, any> = { symbol };
|
|
90
|
+
const bodyParams: Record<string, any> = { symbol };
|
|
91
|
+
|
|
92
|
+
if (alertType === 'spread_exceeded') {
|
|
93
|
+
titleParams.spreadPercent = spreadPercent;
|
|
94
|
+
bodyParams.spreadPercent = spreadPercent;
|
|
95
|
+
bodyParams.threshold = threshold;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Build provider list for attachments
|
|
99
|
+
const providerFields: any[] = [];
|
|
100
|
+
|
|
101
|
+
// Add timestamp
|
|
102
|
+
providerFields.push(
|
|
103
|
+
{
|
|
104
|
+
type: 'text',
|
|
105
|
+
data: {
|
|
106
|
+
type: 'plain',
|
|
107
|
+
color: '#9397A1',
|
|
108
|
+
text: translate('notification.exchangeRateAlert.timestamp', locale),
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
type: 'text',
|
|
113
|
+
data: {
|
|
114
|
+
type: 'plain',
|
|
115
|
+
text: timestamp,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Add symbol
|
|
121
|
+
providerFields.push(
|
|
122
|
+
{
|
|
123
|
+
type: 'text',
|
|
124
|
+
data: {
|
|
125
|
+
type: 'plain',
|
|
126
|
+
color: '#9397A1',
|
|
127
|
+
text: translate('notification.exchangeRateAlert.symbol', locale),
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: 'text',
|
|
132
|
+
data: {
|
|
133
|
+
type: 'plain',
|
|
134
|
+
text: symbol,
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Add spread info for spread_exceeded
|
|
140
|
+
if (alertType === 'spread_exceeded' && spreadPercent) {
|
|
141
|
+
providerFields.push(
|
|
142
|
+
{
|
|
143
|
+
type: 'text',
|
|
144
|
+
data: {
|
|
145
|
+
type: 'plain',
|
|
146
|
+
color: '#9397A1',
|
|
147
|
+
text: translate('notification.exchangeRateAlert.spread', locale),
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
type: 'text',
|
|
152
|
+
data: {
|
|
153
|
+
type: 'plain',
|
|
154
|
+
color: '#FF6600',
|
|
155
|
+
text: `${spreadPercent}% (threshold: ${threshold}%)`,
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Add provider rates
|
|
162
|
+
if (providers.length > 0) {
|
|
163
|
+
providerFields.push(
|
|
164
|
+
{
|
|
165
|
+
type: 'text',
|
|
166
|
+
data: {
|
|
167
|
+
type: 'plain',
|
|
168
|
+
color: '#9397A1',
|
|
169
|
+
text: translate('notification.exchangeRateAlert.providers', locale),
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: 'text',
|
|
174
|
+
data: {
|
|
175
|
+
type: 'plain',
|
|
176
|
+
text: providers.map((p) => `${p.name}: ${p.rate || 'N/A'}`).join('\n'),
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const template: BaseEmailTemplateType = {
|
|
183
|
+
title: translate(titleKey, locale, titleParams),
|
|
184
|
+
body: translate(bodyKey, locale, bodyParams),
|
|
185
|
+
attachments: [
|
|
186
|
+
{
|
|
187
|
+
type: 'section',
|
|
188
|
+
fields: providerFields,
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
actions: [
|
|
192
|
+
{
|
|
193
|
+
name: translate('notification.exchangeRateAlert.viewProviders', locale),
|
|
194
|
+
title: translate('notification.exchangeRateAlert.viewProviders', locale),
|
|
195
|
+
link: viewProvidersLink,
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return template;
|
|
201
|
+
}
|
|
202
|
+
}
|