payment-kit 1.19.10 → 1.19.12
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/libs/notification/template/one-time-payment-refund-succeeded.ts +274 -0
- package/api/src/libs/notification/template/one-time-payment-succeeded.ts +10 -21
- package/api/src/libs/notification/template/subscription-refund-succeeded.ts +23 -3
- package/api/src/libs/notification/template/subscription-will-renew.ts +15 -2
- package/api/src/libs/session.ts +70 -1
- package/api/src/locales/en.ts +10 -0
- package/api/src/locales/zh.ts +10 -0
- package/api/src/queues/credit-consume.ts +4 -1
- package/api/src/queues/notification.ts +16 -3
- package/api/src/routes/checkout-sessions.ts +9 -0
- package/blocklet.yml +6 -6
- package/package.json +19 -19
- package/screenshots/0ffe164ebe4aa2eb43f8d87f87683f7f.png +0 -0
- package/screenshots/1ef9e15ac36d4af5bef34941000ba3af.png +0 -0
- package/screenshots/3a4cab81c52c29662db8794b05ccc7c7.png +0 -0
- package/screenshots/77ac49b79ae920f0f253ce8c694ffd65.png +0 -0
- package/screenshots/7ea8ef758865ecf6edb712d3534d2974.png +0 -0
- package/src/components/customer/credit-overview.tsx +2 -2
- package/src/components/meter/actions.tsx +1 -1
- package/src/components/price/form.tsx +0 -19
- package/src/locales/en.tsx +5 -3
- package/src/locales/zh.tsx +5 -2
- package/screenshots/checkout.png +0 -0
- package/screenshots/customer.png +0 -0
- package/screenshots/payment.png +0 -0
- package/screenshots/setting.png +0 -0
- package/screenshots/subscription_detail.png +0 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/brace-style */
|
|
2
|
+
/* eslint-disable @typescript-eslint/indent */
|
|
3
|
+
import { translate } from '../../../locales';
|
|
4
|
+
import { PaymentIntent, PaymentMethod, Refund, Customer, CheckoutSession } from '../../../store/models';
|
|
5
|
+
import { Invoice } from '../../../store/models/invoice';
|
|
6
|
+
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
7
|
+
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
8
|
+
import { getMainProductNameByCheckoutSession } from '../../product';
|
|
9
|
+
import { formatTime } from '../../time';
|
|
10
|
+
import { formatCurrencyInfo, getExplorerLink } from '../../util';
|
|
11
|
+
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
12
|
+
import { getCustomerInvoicePageUrl } from '../../invoice';
|
|
13
|
+
|
|
14
|
+
export interface OneTimePaymentRefundSucceededEmailTemplateOptions {
|
|
15
|
+
refundId: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface OneTimePaymentRefundSucceededEmailTemplateContext {
|
|
19
|
+
locale: string;
|
|
20
|
+
productName: string | undefined;
|
|
21
|
+
at: string;
|
|
22
|
+
chainHost: string | undefined;
|
|
23
|
+
userDid: string;
|
|
24
|
+
paymentInfo: string;
|
|
25
|
+
refundInfo: string;
|
|
26
|
+
viewInvoiceLink: string;
|
|
27
|
+
viewTxHashLink: string | undefined;
|
|
28
|
+
refund: Refund;
|
|
29
|
+
invoiceNumber?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class OneTimePaymentRefundSucceededEmailTemplate
|
|
33
|
+
implements BaseEmailTemplate<OneTimePaymentRefundSucceededEmailTemplateContext>
|
|
34
|
+
{
|
|
35
|
+
options: OneTimePaymentRefundSucceededEmailTemplateOptions;
|
|
36
|
+
|
|
37
|
+
constructor(options: OneTimePaymentRefundSucceededEmailTemplateOptions) {
|
|
38
|
+
this.options = options;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async getContext(): Promise<OneTimePaymentRefundSucceededEmailTemplateContext> {
|
|
42
|
+
const refund = await Refund.findByPk(this.options.refundId);
|
|
43
|
+
if (!refund) {
|
|
44
|
+
throw new Error(`Refund not found: ${this.options.refundId}`);
|
|
45
|
+
}
|
|
46
|
+
if (refund.status !== 'succeeded') {
|
|
47
|
+
throw new Error(`Refund not succeeded: ${this.options.refundId}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const customer = await Customer.findByPk(refund.customer_id);
|
|
51
|
+
if (!customer) {
|
|
52
|
+
throw new Error(`Customer not found: ${refund.customer_id}`);
|
|
53
|
+
}
|
|
54
|
+
const userDid = customer.did;
|
|
55
|
+
const locale = await getUserLocale(userDid);
|
|
56
|
+
|
|
57
|
+
let invoiceNumber: string | undefined;
|
|
58
|
+
let productName: string | undefined;
|
|
59
|
+
let viewInvoiceLink = '';
|
|
60
|
+
|
|
61
|
+
if (refund.invoice_id) {
|
|
62
|
+
const invoice = await Invoice.findByPk(refund.invoice_id);
|
|
63
|
+
if (invoice) {
|
|
64
|
+
invoiceNumber = invoice.number;
|
|
65
|
+
viewInvoiceLink = getCustomerInvoicePageUrl({
|
|
66
|
+
invoiceId: refund.invoice_id,
|
|
67
|
+
userDid,
|
|
68
|
+
locale,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (invoice.checkout_session_id) {
|
|
72
|
+
const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
|
|
73
|
+
if (checkoutSession) {
|
|
74
|
+
productName = await getMainProductNameByCheckoutSession(checkoutSession);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
|
|
81
|
+
const paymentCurrency = await PaymentCurrency.findByPk(refund.currency_id);
|
|
82
|
+
if (!paymentCurrency) {
|
|
83
|
+
throw new Error(`PaymentCurrency not found: ${refund.currency_id}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const paymentMethod = await PaymentMethod.findByPk(refund.payment_method_id);
|
|
87
|
+
|
|
88
|
+
const paymentInfo = formatCurrencyInfo(paymentIntent?.amount_received || '0', paymentCurrency, paymentMethod);
|
|
89
|
+
const refundInfo = formatCurrencyInfo(refund.amount, paymentCurrency, paymentMethod);
|
|
90
|
+
|
|
91
|
+
// @ts-expect-error
|
|
92
|
+
const chainHost = paymentMethod?.settings?.[paymentMethod?.type]?.api_host;
|
|
93
|
+
// @ts-expect-error
|
|
94
|
+
const txHash = refund?.payment_details?.[paymentMethod?.type]?.tx_hash;
|
|
95
|
+
const viewTxHashLink =
|
|
96
|
+
txHash &&
|
|
97
|
+
getExplorerLink({
|
|
98
|
+
type: 'tx',
|
|
99
|
+
did: txHash,
|
|
100
|
+
chainHost,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const at = formatTime(refund.created_at);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
locale,
|
|
107
|
+
productName,
|
|
108
|
+
at,
|
|
109
|
+
userDid,
|
|
110
|
+
chainHost,
|
|
111
|
+
paymentInfo,
|
|
112
|
+
refundInfo,
|
|
113
|
+
viewInvoiceLink,
|
|
114
|
+
viewTxHashLink,
|
|
115
|
+
refund,
|
|
116
|
+
invoiceNumber,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async getTemplate(): Promise<BaseEmailTemplateType> {
|
|
121
|
+
const {
|
|
122
|
+
locale,
|
|
123
|
+
productName,
|
|
124
|
+
at,
|
|
125
|
+
userDid,
|
|
126
|
+
paymentInfo,
|
|
127
|
+
refundInfo,
|
|
128
|
+
viewInvoiceLink,
|
|
129
|
+
viewTxHashLink,
|
|
130
|
+
refund,
|
|
131
|
+
invoiceNumber,
|
|
132
|
+
} = await this.getContext();
|
|
133
|
+
|
|
134
|
+
if (refund.type === 'stake_return') {
|
|
135
|
+
throw new Error('Stake return is not supported for one-time payment refunds');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const commonFields = [
|
|
139
|
+
{
|
|
140
|
+
type: 'text',
|
|
141
|
+
data: {
|
|
142
|
+
type: 'plain',
|
|
143
|
+
color: '#9397A1',
|
|
144
|
+
text: translate('notification.common.account', locale),
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
type: 'text',
|
|
149
|
+
data: {
|
|
150
|
+
type: 'plain',
|
|
151
|
+
text: userDid,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const productFields = productName
|
|
157
|
+
? [
|
|
158
|
+
{
|
|
159
|
+
type: 'text',
|
|
160
|
+
data: {
|
|
161
|
+
type: 'plain',
|
|
162
|
+
color: '#9397A1',
|
|
163
|
+
text: translate('notification.common.product', locale),
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: 'text',
|
|
168
|
+
data: {
|
|
169
|
+
type: 'plain',
|
|
170
|
+
text: productName,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
]
|
|
174
|
+
: [];
|
|
175
|
+
|
|
176
|
+
const amountFields = [
|
|
177
|
+
{
|
|
178
|
+
type: 'text',
|
|
179
|
+
data: {
|
|
180
|
+
type: 'plain',
|
|
181
|
+
color: '#9397A1',
|
|
182
|
+
text: translate('notification.common.paymentAmount', locale),
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
type: 'text',
|
|
187
|
+
data: {
|
|
188
|
+
type: 'plain',
|
|
189
|
+
text: paymentInfo,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
type: 'text',
|
|
194
|
+
data: {
|
|
195
|
+
type: 'plain',
|
|
196
|
+
color: '#9397A1',
|
|
197
|
+
text: translate('notification.common.refundAmount', locale),
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
type: 'text',
|
|
202
|
+
data: {
|
|
203
|
+
type: 'plain',
|
|
204
|
+
text: refundInfo,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
const invoiceFields = invoiceNumber
|
|
210
|
+
? [
|
|
211
|
+
{
|
|
212
|
+
type: 'text',
|
|
213
|
+
data: {
|
|
214
|
+
type: 'plain',
|
|
215
|
+
color: '#9397A1',
|
|
216
|
+
text: translate('notification.common.invoiceNumber', locale),
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
type: 'text',
|
|
221
|
+
data: {
|
|
222
|
+
type: 'plain',
|
|
223
|
+
text: invoiceNumber,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
]
|
|
227
|
+
: [];
|
|
228
|
+
|
|
229
|
+
const actions = [
|
|
230
|
+
viewInvoiceLink && {
|
|
231
|
+
name: translate('notification.common.viewInvoice', locale),
|
|
232
|
+
title: translate('notification.common.viewInvoice', locale),
|
|
233
|
+
link: viewInvoiceLink,
|
|
234
|
+
},
|
|
235
|
+
viewTxHashLink && {
|
|
236
|
+
name: translate('notification.common.viewTxHash', locale),
|
|
237
|
+
title: translate('notification.common.viewTxHash', locale),
|
|
238
|
+
link: viewTxHashLink,
|
|
239
|
+
},
|
|
240
|
+
].filter(Boolean);
|
|
241
|
+
|
|
242
|
+
const titleKey = productName
|
|
243
|
+
? 'notification.oneTimePaymentRefundSucceeded.title'
|
|
244
|
+
: 'notification.oneTimePaymentRefundSucceeded.titleNoProduct';
|
|
245
|
+
|
|
246
|
+
const bodyKey = productName
|
|
247
|
+
? 'notification.oneTimePaymentRefundSucceeded.body'
|
|
248
|
+
: 'notification.oneTimePaymentRefundSucceeded.bodyNoProduct';
|
|
249
|
+
|
|
250
|
+
const template: BaseEmailTemplateType = {
|
|
251
|
+
title: translate(titleKey, locale, {
|
|
252
|
+
productName,
|
|
253
|
+
invoiceNumber: invoiceNumber || '',
|
|
254
|
+
}),
|
|
255
|
+
body: translate(bodyKey, locale, {
|
|
256
|
+
at,
|
|
257
|
+
productName,
|
|
258
|
+
refundInfo,
|
|
259
|
+
invoiceNumber: invoiceNumber || '',
|
|
260
|
+
}),
|
|
261
|
+
// @ts-expect-error
|
|
262
|
+
attachments: [
|
|
263
|
+
{
|
|
264
|
+
type: 'section',
|
|
265
|
+
fields: [...commonFields, ...productFields, ...amountFields, ...invoiceFields].filter(Boolean),
|
|
266
|
+
},
|
|
267
|
+
].filter(Boolean),
|
|
268
|
+
// @ts-ignore
|
|
269
|
+
actions,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return template;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -10,6 +10,7 @@ import { getMainProductNameByCheckoutSession } from '../../product';
|
|
|
10
10
|
import { formatTime } from '../../time';
|
|
11
11
|
import { formatCurrencyInfo, getExplorerLink } from '../../util';
|
|
12
12
|
import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
|
|
13
|
+
import { getCustomerInvoicePageUrl } from '../../invoice';
|
|
13
14
|
|
|
14
15
|
export interface OneTimePaymentSucceededEmailTemplateOptions {
|
|
15
16
|
checkoutSessionId: string;
|
|
@@ -25,7 +26,6 @@ interface OneTimePaymentSucceededEmailTemplateContext {
|
|
|
25
26
|
userDid: string;
|
|
26
27
|
paymentInfo: string;
|
|
27
28
|
|
|
28
|
-
viewSubscriptionLink: string;
|
|
29
29
|
viewInvoiceLink: string;
|
|
30
30
|
viewTxHashLink: string | undefined;
|
|
31
31
|
}
|
|
@@ -99,8 +99,13 @@ export class OneTimePaymentSucceededEmailTemplate
|
|
|
99
99
|
|
|
100
100
|
// @ts-expect-error
|
|
101
101
|
const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
|
|
102
|
-
const
|
|
103
|
-
|
|
102
|
+
const viewInvoiceLink = paymentIntent?.invoice_id
|
|
103
|
+
? getCustomerInvoicePageUrl({
|
|
104
|
+
invoiceId: paymentIntent.invoice_id,
|
|
105
|
+
userDid,
|
|
106
|
+
locale,
|
|
107
|
+
})
|
|
108
|
+
: '';
|
|
104
109
|
|
|
105
110
|
// @ts-expect-error
|
|
106
111
|
const txHash: string | undefined = paymentIntent?.payment_details?.[paymentMethod.type]?.tx_hash;
|
|
@@ -122,25 +127,14 @@ export class OneTimePaymentSucceededEmailTemplate
|
|
|
122
127
|
chainHost,
|
|
123
128
|
paymentInfo,
|
|
124
129
|
|
|
125
|
-
viewSubscriptionLink,
|
|
126
130
|
viewInvoiceLink,
|
|
127
131
|
viewTxHashLink,
|
|
128
132
|
};
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
async getTemplate(): Promise<BaseEmailTemplateType> {
|
|
132
|
-
const {
|
|
133
|
-
|
|
134
|
-
productName,
|
|
135
|
-
at,
|
|
136
|
-
nftMintItem,
|
|
137
|
-
chainHost,
|
|
138
|
-
userDid,
|
|
139
|
-
paymentInfo,
|
|
140
|
-
viewSubscriptionLink,
|
|
141
|
-
viewInvoiceLink,
|
|
142
|
-
viewTxHashLink,
|
|
143
|
-
} = await this.getContext();
|
|
136
|
+
const { locale, productName, at, nftMintItem, chainHost, userDid, paymentInfo, viewInvoiceLink, viewTxHashLink } =
|
|
137
|
+
await this.getContext();
|
|
144
138
|
|
|
145
139
|
const template: BaseEmailTemplateType = {
|
|
146
140
|
title: `${translate('notification.oneTimePaymentSucceeded.title', locale, {
|
|
@@ -213,11 +207,6 @@ export class OneTimePaymentSucceededEmailTemplate
|
|
|
213
207
|
].filter(Boolean),
|
|
214
208
|
// @ts-ignore
|
|
215
209
|
actions: [
|
|
216
|
-
viewSubscriptionLink && {
|
|
217
|
-
name: translate('notification.common.viewSubscription', locale),
|
|
218
|
-
title: translate('notification.common.viewSubscription', locale),
|
|
219
|
-
link: viewSubscriptionLink,
|
|
220
|
-
},
|
|
221
210
|
viewInvoiceLink && {
|
|
222
211
|
name: translate('notification.common.viewInvoice', locale),
|
|
223
212
|
title: translate('notification.common.viewInvoice', locale),
|
|
@@ -4,6 +4,7 @@ import { translate } from '../../../locales';
|
|
|
4
4
|
import { PaymentIntent, PaymentMethod, Refund, Subscription } from '../../../store/models';
|
|
5
5
|
import { Invoice } from '../../../store/models/invoice';
|
|
6
6
|
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
7
|
+
import { getCustomerInvoicePageUrl } from '../../invoice';
|
|
7
8
|
import { formatTime } from '../../time';
|
|
8
9
|
import { formatCurrencyInfo, getExplorerLink } from '../../util';
|
|
9
10
|
import { BaseSubscriptionEmailTemplate, BaseEmailTemplateType } from './base';
|
|
@@ -28,6 +29,7 @@ interface SubscriptionRefundSucceededEmailTemplateContext {
|
|
|
28
29
|
unusedDuration: string;
|
|
29
30
|
viewSubscriptionLink: string;
|
|
30
31
|
viewTxHashLink: string | undefined;
|
|
32
|
+
viewInvoiceLink: string;
|
|
31
33
|
refund: Refund;
|
|
32
34
|
customActions: any[];
|
|
33
35
|
isCreditSubscription: boolean;
|
|
@@ -49,9 +51,12 @@ export class SubscriptionRefundSucceededEmailTemplate extends BaseSubscriptionEm
|
|
|
49
51
|
if (refund.status !== 'succeeded') {
|
|
50
52
|
throw new Error(`Refund not succeeded: ${this.options.refundId}`);
|
|
51
53
|
}
|
|
54
|
+
if (!refund.subscription_id) {
|
|
55
|
+
throw new Error(`Refund must have subscription_id: ${this.options.refundId}`);
|
|
56
|
+
}
|
|
52
57
|
|
|
53
58
|
// 获取基础订阅数据
|
|
54
|
-
const basicData = await this.getSubscriptionBasicData(refund.subscription_id
|
|
59
|
+
const basicData = await this.getSubscriptionBasicData(refund.subscription_id);
|
|
55
60
|
const { userDid, locale, productName, isCreditSubscription } = basicData;
|
|
56
61
|
|
|
57
62
|
// 获取支付相关信息
|
|
@@ -102,6 +107,7 @@ export class SubscriptionRefundSucceededEmailTemplate extends BaseSubscriptionEm
|
|
|
102
107
|
// 获取链接
|
|
103
108
|
let customActions: any[] = [];
|
|
104
109
|
let viewSubscriptionLink = '';
|
|
110
|
+
|
|
105
111
|
if (refund?.subscription_id) {
|
|
106
112
|
const subscription = await Subscription.findByPk(refund.subscription_id);
|
|
107
113
|
if (subscription) {
|
|
@@ -118,6 +124,13 @@ export class SubscriptionRefundSucceededEmailTemplate extends BaseSubscriptionEm
|
|
|
118
124
|
}
|
|
119
125
|
}
|
|
120
126
|
|
|
127
|
+
const viewInvoiceLink = refund.invoice_id
|
|
128
|
+
? getCustomerInvoicePageUrl({
|
|
129
|
+
invoiceId: refund.invoice_id,
|
|
130
|
+
locale,
|
|
131
|
+
userDid,
|
|
132
|
+
})
|
|
133
|
+
: '';
|
|
121
134
|
// 获取交易哈希链接
|
|
122
135
|
// @ts-expect-error
|
|
123
136
|
const chainHost = paymentMethod?.settings?.[paymentMethod?.type]?.api_host;
|
|
@@ -152,6 +165,7 @@ export class SubscriptionRefundSucceededEmailTemplate extends BaseSubscriptionEm
|
|
|
152
165
|
refund,
|
|
153
166
|
customActions,
|
|
154
167
|
isCreditSubscription,
|
|
168
|
+
viewInvoiceLink,
|
|
155
169
|
};
|
|
156
170
|
}
|
|
157
171
|
|
|
@@ -174,6 +188,7 @@ export class SubscriptionRefundSucceededEmailTemplate extends BaseSubscriptionEm
|
|
|
174
188
|
viewTxHashLink,
|
|
175
189
|
refund,
|
|
176
190
|
isCreditSubscription,
|
|
191
|
+
viewInvoiceLink,
|
|
177
192
|
} = context;
|
|
178
193
|
|
|
179
194
|
// 如果是质押返还类型,使用特殊模板
|
|
@@ -266,11 +281,16 @@ export class SubscriptionRefundSucceededEmailTemplate extends BaseSubscriptionEm
|
|
|
266
281
|
|
|
267
282
|
// 构建操作按钮
|
|
268
283
|
const actions = [
|
|
269
|
-
{
|
|
284
|
+
viewSubscriptionLink && {
|
|
270
285
|
name: translate('notification.common.viewSubscription', locale),
|
|
271
286
|
title: translate('notification.common.viewSubscription', locale),
|
|
272
287
|
link: viewSubscriptionLink,
|
|
273
288
|
},
|
|
289
|
+
viewInvoiceLink && {
|
|
290
|
+
name: translate('notification.common.viewInvoice', locale),
|
|
291
|
+
title: translate('notification.common.viewInvoice', locale),
|
|
292
|
+
link: viewInvoiceLink,
|
|
293
|
+
},
|
|
274
294
|
viewTxHashLink && {
|
|
275
295
|
name: translate('notification.common.viewTxHash', locale),
|
|
276
296
|
title: translate('notification.common.viewTxHash', locale),
|
|
@@ -331,7 +351,7 @@ export class SubscriptionRefundSucceededEmailTemplate extends BaseSubscriptionEm
|
|
|
331
351
|
|
|
332
352
|
// 构建操作按钮
|
|
333
353
|
const actions = [
|
|
334
|
-
{
|
|
354
|
+
viewSubscriptionLink && {
|
|
335
355
|
name: translate('notification.common.viewSubscription', locale),
|
|
336
356
|
title: translate('notification.common.viewSubscription', locale),
|
|
337
357
|
link: viewSubscriptionLink,
|
|
@@ -10,6 +10,7 @@ import { translate } from '../../../locales';
|
|
|
10
10
|
import { Invoice, Price, SubscriptionItem } from '../../../store/models';
|
|
11
11
|
import type { PaymentDetail } from '../../payment';
|
|
12
12
|
import { getSubscriptionPaymentAddress, getPaymentAmountForCycleSubscription } from '../../subscription';
|
|
13
|
+
|
|
13
14
|
import { formatTime, getSimplifyDuration } from '../../time';
|
|
14
15
|
import { formatCurrencyInfo, getCustomerRechargeLink } from '../../util';
|
|
15
16
|
import { BaseSubscriptionEmailTemplate, BaseEmailTemplateType } from './base';
|
|
@@ -38,6 +39,7 @@ interface SubscriptionWillRenewEmailTemplateContext {
|
|
|
38
39
|
customActions: any[];
|
|
39
40
|
isCreditSubscription: boolean;
|
|
40
41
|
isStripe: boolean;
|
|
42
|
+
isMetered: boolean;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTemplate<SubscriptionWillRenewEmailTemplateContext> {
|
|
@@ -102,17 +104,24 @@ export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTem
|
|
|
102
104
|
|
|
103
105
|
// 获取支付类型和周期信息
|
|
104
106
|
const { isPrePaid, interval } = await this.getPaymentCategory(subscription.id);
|
|
107
|
+
const isMetered = !isPrePaid; // 按量计费就是后付费
|
|
108
|
+
|
|
105
109
|
const paidType = isPrePaid
|
|
106
110
|
? translate('notification.common.prepaid', locale)
|
|
107
111
|
: translate('notification.common.postpaid', locale);
|
|
108
112
|
|
|
109
|
-
|
|
113
|
+
// 对于按量计费,在金额后添加预估说明
|
|
114
|
+
const basePaymentInfo = formatCurrencyInfo(
|
|
110
115
|
paymentDetail?.price || '0',
|
|
111
116
|
paymentCurrency,
|
|
112
117
|
paymentInfoResult.paymentMethod,
|
|
113
118
|
true
|
|
114
119
|
);
|
|
115
120
|
|
|
121
|
+
const paymentInfo = isMetered
|
|
122
|
+
? translate('notification.subscriptionWillRenew.estimatedAmountNote', locale, { amount: basePaymentInfo })
|
|
123
|
+
: basePaymentInfo;
|
|
124
|
+
|
|
116
125
|
// 计算周期时间 - 使用安全的回退机制
|
|
117
126
|
const periodStart = invoice?.period_start || subscription.current_period_start;
|
|
118
127
|
const periodInfo = this.formatSubscriptionPeriod(
|
|
@@ -148,6 +157,7 @@ export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTem
|
|
|
148
157
|
customActions: links.customActions,
|
|
149
158
|
isCreditSubscription,
|
|
150
159
|
isStripe,
|
|
160
|
+
isMetered: !isPrePaid,
|
|
151
161
|
};
|
|
152
162
|
}
|
|
153
163
|
|
|
@@ -198,6 +208,7 @@ export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTem
|
|
|
198
208
|
customActions,
|
|
199
209
|
isCreditSubscription,
|
|
200
210
|
isStripe,
|
|
211
|
+
isMetered,
|
|
201
212
|
} = context;
|
|
202
213
|
|
|
203
214
|
// 如果当前时间大于预计扣费时间,那么不发送通知
|
|
@@ -206,7 +217,9 @@ export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTem
|
|
|
206
217
|
}
|
|
207
218
|
|
|
208
219
|
const canPay = paymentDetail.balance >= paymentDetail.price;
|
|
209
|
-
|
|
220
|
+
// 如果是按量付费的预估金额,余额充足,不发送通知
|
|
221
|
+
// 如果是预付费,余额充足,非必要不发送通知
|
|
222
|
+
if (canPay && (!this.options.required || isMetered)) {
|
|
210
223
|
return null;
|
|
211
224
|
}
|
|
212
225
|
|
package/api/src/libs/session.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { env } from '@blocklet/sdk/lib/config';
|
|
2
2
|
import type { TransactionInput } from '@ocap/client';
|
|
3
|
-
import { BN } from '@ocap/util';
|
|
3
|
+
import { BN, fromTokenToUnit } from '@ocap/util';
|
|
4
4
|
import cloneDeep from 'lodash/cloneDeep';
|
|
5
5
|
import isEqual from 'lodash/isEqual';
|
|
6
6
|
import pAll from 'p-all';
|
|
@@ -983,3 +983,72 @@ export function isCreditMetered(price: TPrice | TPriceExpanded) {
|
|
|
983
983
|
export function isCreditMeteredLineItems(lineItems: TLineItemExpanded[]) {
|
|
984
984
|
return lineItems.every((item) => item.price && isCreditMetered(item.price));
|
|
985
985
|
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Validates payment amounts meet minimum requirements
|
|
989
|
+
* @param lineItems Line items to validate
|
|
990
|
+
* @param currencyId Currency ID for validation
|
|
991
|
+
* @param mode Checkout mode
|
|
992
|
+
* @returns Validation result with error message if any
|
|
993
|
+
*/
|
|
994
|
+
export function validatePaymentAmounts(
|
|
995
|
+
lineItems: TLineItemExpanded[],
|
|
996
|
+
currency: PaymentCurrency,
|
|
997
|
+
checkoutSession: CheckoutSession
|
|
998
|
+
): { valid: boolean; error?: string } {
|
|
999
|
+
const enableGrouping = checkoutSession.enable_subscription_grouping;
|
|
1000
|
+
const oneTimeItems = getOneTimeLineItems(lineItems);
|
|
1001
|
+
const recurringItems = getRecurringLineItems(lineItems);
|
|
1002
|
+
|
|
1003
|
+
const minAmountInUnits = fromTokenToUnit(0.5, currency.decimal);
|
|
1004
|
+
|
|
1005
|
+
// Case 1: All one-time items - validate total payment amount
|
|
1006
|
+
if (recurringItems.length === 0 && oneTimeItems.length > 0) {
|
|
1007
|
+
const { total } = getCheckoutAmount(lineItems, currency.id);
|
|
1008
|
+
if (new BN(total).lt(new BN(minAmountInUnits))) {
|
|
1009
|
+
return {
|
|
1010
|
+
valid: false,
|
|
1011
|
+
error: 'Total payment amount must be greater or equal to 0.5 USD',
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
return { valid: true };
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Case 2: Mixed or subscription only - validate subscription pricing
|
|
1018
|
+
if (recurringItems.length > 0) {
|
|
1019
|
+
if (enableGrouping) {
|
|
1020
|
+
// When grouping is enabled, validate each subscription product's unit price
|
|
1021
|
+
const priceGroups = groupLineItemsByPrice(lineItems);
|
|
1022
|
+
|
|
1023
|
+
for (const [priceId, items] of Object.entries(priceGroups)) {
|
|
1024
|
+
// Calculate total unit price for this subscription product
|
|
1025
|
+
let totalUnitPrice = new BN(0);
|
|
1026
|
+
|
|
1027
|
+
items.forEach((item) => {
|
|
1028
|
+
const price = item.upsell_price || item.price;
|
|
1029
|
+
const unitPrice = getPriceUintAmountByCurrency(price, currency.id);
|
|
1030
|
+
totalUnitPrice = totalUnitPrice.add(new BN(unitPrice).mul(new BN(item.quantity)));
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
if (totalUnitPrice.lt(new BN(minAmountInUnits))) {
|
|
1034
|
+
const productName = items[0]?.price?.product?.name || `Product ${priceId}`;
|
|
1035
|
+
return {
|
|
1036
|
+
valid: false,
|
|
1037
|
+
error: `product "${productName}" unit price must be greater or equal to 0.5 USD`,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
} else {
|
|
1042
|
+
// When grouping is disabled, validate total subscription amount
|
|
1043
|
+
const { renew } = getCheckoutAmount(lineItems, currency.id);
|
|
1044
|
+
if (new BN(renew).lt(new BN(minAmountInUnits))) {
|
|
1045
|
+
return {
|
|
1046
|
+
valid: false,
|
|
1047
|
+
error: 'Total subscription amount must be greater or equal to 0.5 USD',
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return { valid: true };
|
|
1054
|
+
}
|
package/api/src/locales/en.ts
CHANGED
|
@@ -49,6 +49,7 @@ export default flat({
|
|
|
49
49
|
shouldPayAmount: 'Should pay amount',
|
|
50
50
|
billedAmount: 'Billed amount',
|
|
51
51
|
viewCreditGrant: 'View Credit Balance',
|
|
52
|
+
invoiceNumber: 'Invoice Number',
|
|
52
53
|
},
|
|
53
54
|
|
|
54
55
|
billingDiscrepancy: {
|
|
@@ -112,6 +113,7 @@ export default flat({
|
|
|
112
113
|
unableToPayReason:
|
|
113
114
|
'The estimated payment amount is {price}, but the current balance is insufficient ({balance}), please ensure that your account has enough balance to avoid payment failure.',
|
|
114
115
|
renewAmount: 'Payment amount',
|
|
116
|
+
estimatedAmountNote: 'Estimate {amount}, billed based on final usage',
|
|
115
117
|
},
|
|
116
118
|
|
|
117
119
|
subscriptionRenewed: {
|
|
@@ -145,6 +147,14 @@ export default flat({
|
|
|
145
147
|
body: 'Your subscription to {productName} has been successfully refunded on {at}, with a refund amount of {refundInfo}. If you have any questions, please feel free to contact us.',
|
|
146
148
|
},
|
|
147
149
|
|
|
150
|
+
oneTimePaymentRefundSucceeded: {
|
|
151
|
+
title: '{productName} refund successful',
|
|
152
|
+
body: 'Your purchase of {productName} has been successfully refunded on {at}, with a refund amount of {refundInfo}. If you have any questions, please feel free to contact us.',
|
|
153
|
+
titleNoProduct: 'You have a refund from invoice {invoiceNumber}',
|
|
154
|
+
bodyNoProduct:
|
|
155
|
+
'You have a refund from invoice {invoiceNumber} that has been successfully processed on {at}, with a refund amount of {refundInfo}. If you have any questions, please feel free to contact us.',
|
|
156
|
+
},
|
|
157
|
+
|
|
148
158
|
subscriptionStakeReturnSucceeded: {
|
|
149
159
|
title: '{productName} stake return successful',
|
|
150
160
|
body: 'The stake for your subscription to {productName} has been returned on {at}, with a return amount of {refundInfo}. If you have any questions, please feel free to contact us.',
|
package/api/src/locales/zh.ts
CHANGED
|
@@ -49,6 +49,7 @@ export default flat({
|
|
|
49
49
|
shouldPayAmount: '应收金额',
|
|
50
50
|
billedAmount: '实缴金额',
|
|
51
51
|
viewCreditGrant: '查看额度',
|
|
52
|
+
invoiceNumber: '账单编号',
|
|
52
53
|
},
|
|
53
54
|
|
|
54
55
|
sendTo: '发送给',
|
|
@@ -111,6 +112,7 @@ export default flat({
|
|
|
111
112
|
unableToPayReason:
|
|
112
113
|
'预计扣款金额为 {price},但当前余额不足(余额为 {balance}),请确保您的账户余额充足,避免扣费失败。',
|
|
113
114
|
renewAmount: '扣费金额',
|
|
115
|
+
estimatedAmountNote: '预估 {amount},按最终用量计费',
|
|
114
116
|
},
|
|
115
117
|
|
|
116
118
|
subscriptionRenewed: {
|
|
@@ -141,6 +143,14 @@ export default flat({
|
|
|
141
143
|
body: '您订阅的 {productName} 在 {at} 已退款成功,退款金额为 {refundInfo}。如有任何疑问,请随时与我们联系。',
|
|
142
144
|
},
|
|
143
145
|
|
|
146
|
+
oneTimePaymentRefundSucceeded: {
|
|
147
|
+
title: '{productName} 退款成功',
|
|
148
|
+
body: '您购买的 {productName} 在 {at} 已退款成功,退款金额为 {refundInfo}。如有任何疑问,请随时与我们联系。',
|
|
149
|
+
titleNoProduct: '您有一笔来自 {invoiceNumber} 账单的退款',
|
|
150
|
+
bodyNoProduct:
|
|
151
|
+
'您有一笔来自 {invoiceNumber} 账单的退款在 {at} 已退款成功,退款金额为 {refundInfo}。如有任何疑问,请随时与我们联系。',
|
|
152
|
+
},
|
|
153
|
+
|
|
144
154
|
subscriptionStakeReturnSucceeded: {
|
|
145
155
|
title: '{productName} 质押退还成功',
|
|
146
156
|
body: '您订阅的 {productName} 在 {at} 已退还押金,退还金额为 {refundInfo}。如有任何疑问,请随时与我们联系。',
|
|
@@ -334,10 +334,13 @@ async function createCreditTransaction(
|
|
|
334
334
|
meterEventId: context.meterEvent.id,
|
|
335
335
|
});
|
|
336
336
|
|
|
337
|
-
let description = `Consume ${fromUnitToToken(consumeAmount, context.meter.paymentCurrency.decimal)}${context.meter.paymentCurrency.symbol}`;
|
|
337
|
+
let description = `Consume ${fromUnitToToken(consumeAmount, context.meter.paymentCurrency.decimal)} ${context.meter.paymentCurrency.symbol}`;
|
|
338
338
|
if (context.meterEvent.getSubscriptionId()) {
|
|
339
339
|
description += 'for Subscription';
|
|
340
340
|
}
|
|
341
|
+
if (context.meterEvent.metadata?.description) {
|
|
342
|
+
description = context.meterEvent.metadata.description;
|
|
343
|
+
}
|
|
341
344
|
|
|
342
345
|
try {
|
|
343
346
|
const transaction = await CreditTransaction.create({
|
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
SubscriptionRefundSucceededEmailTemplate,
|
|
22
22
|
SubscriptionRefundSucceededEmailTemplateOptions,
|
|
23
23
|
} from '../libs/notification/template/subscription-refund-succeeded';
|
|
24
|
+
import {
|
|
25
|
+
OneTimePaymentRefundSucceededEmailTemplate,
|
|
26
|
+
OneTimePaymentRefundSucceededEmailTemplateOptions,
|
|
27
|
+
} from '../libs/notification/template/one-time-payment-refund-succeeded';
|
|
24
28
|
import {
|
|
25
29
|
SubscriptionRenewFailedEmailTemplate,
|
|
26
30
|
SubscriptionRenewFailedEmailTemplateOptions,
|
|
@@ -187,7 +191,7 @@ function calculateNextNotificationTime(preference: NotificationPreference): numb
|
|
|
187
191
|
return Math.floor(nextTime.valueOf() / 1000);
|
|
188
192
|
}
|
|
189
193
|
|
|
190
|
-
function getNotificationTemplate(job: NotificationQueueJob): BaseEmailTemplate {
|
|
194
|
+
async function getNotificationTemplate(job: NotificationQueueJob): Promise<BaseEmailTemplate> {
|
|
191
195
|
if (job.type === 'usage.report.empty') {
|
|
192
196
|
return new UsageReportEmptyEmailTemplate(job.options as UsageReportEmptyEmailTemplateOptions);
|
|
193
197
|
}
|
|
@@ -225,7 +229,16 @@ function getNotificationTemplate(job: NotificationQueueJob): BaseEmailTemplate {
|
|
|
225
229
|
return new SubscriptionCanceledEmailTemplate(job.options as SubscriptionCanceledEmailTemplateOptions);
|
|
226
230
|
}
|
|
227
231
|
if (job.type === 'refund.succeeded') {
|
|
228
|
-
|
|
232
|
+
const { refundId } = job.options as { refundId: string };
|
|
233
|
+
const refund = await Refund.findByPk(refundId);
|
|
234
|
+
if (refund?.subscription_id) {
|
|
235
|
+
return new SubscriptionRefundSucceededEmailTemplate(
|
|
236
|
+
job.options as SubscriptionRefundSucceededEmailTemplateOptions
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return new OneTimePaymentRefundSucceededEmailTemplate(
|
|
240
|
+
job.options as OneTimePaymentRefundSucceededEmailTemplateOptions
|
|
241
|
+
);
|
|
229
242
|
}
|
|
230
243
|
if (job.type === 'customer.reward.succeeded') {
|
|
231
244
|
return new CustomerRewardSucceededEmailTemplate(job.options as CustomerRewardSucceededEmailTemplateOptions);
|
|
@@ -264,7 +277,7 @@ function getNotificationTemplate(job: NotificationQueueJob): BaseEmailTemplate {
|
|
|
264
277
|
|
|
265
278
|
async function handleNotificationJob(job: NotificationQueueJob): Promise<void> {
|
|
266
279
|
try {
|
|
267
|
-
const template = getNotificationTemplate(job);
|
|
280
|
+
const template = await getNotificationTemplate(job);
|
|
268
281
|
|
|
269
282
|
await new Notification(template, job.type).send();
|
|
270
283
|
logger.info('handleImmediateNotificationJob.success', { job });
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
getCheckoutSessionSubscriptionIds,
|
|
42
42
|
getSubscriptionLineItems,
|
|
43
43
|
isCreditMeteredLineItems,
|
|
44
|
+
validatePaymentAmounts,
|
|
44
45
|
} from '../libs/session';
|
|
45
46
|
import { getDaysUntilCancel, getDaysUntilDue, getSubscriptionTrialSetup } from '../libs/subscription';
|
|
46
47
|
import {
|
|
@@ -1074,6 +1075,14 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1074
1075
|
true
|
|
1075
1076
|
);
|
|
1076
1077
|
|
|
1078
|
+
// Validate payment amounts meet minimum requirements
|
|
1079
|
+
if (paymentMethod.type === 'stripe') {
|
|
1080
|
+
const result = validatePaymentAmounts(lineItems, paymentCurrency, checkoutSession);
|
|
1081
|
+
if (!result.valid) {
|
|
1082
|
+
return res.status(400).json({ error: result.error });
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1077
1086
|
let customer = await Customer.findOne({ where: { did: req.user.did } });
|
|
1078
1087
|
if (!customer) {
|
|
1079
1088
|
const { user: userInfo } = await blocklet.getUser(req.user.did);
|
package/blocklet.yml
CHANGED
|
@@ -14,7 +14,7 @@ repository:
|
|
|
14
14
|
type: git
|
|
15
15
|
url: git+https://github.com/blocklet/payment-kit.git
|
|
16
16
|
specVersion: 1.2.8
|
|
17
|
-
version: 1.19.
|
|
17
|
+
version: 1.19.12
|
|
18
18
|
logo: logo.png
|
|
19
19
|
files:
|
|
20
20
|
- dist
|
|
@@ -72,11 +72,11 @@ capabilities:
|
|
|
72
72
|
clusterMode: false
|
|
73
73
|
component: true
|
|
74
74
|
screenshots:
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
75
|
+
- 3a4cab81c52c29662db8794b05ccc7c7.png
|
|
76
|
+
- 77ac49b79ae920f0f253ce8c694ffd65.png
|
|
77
|
+
- 1ef9e15ac36d4af5bef34941000ba3af.png
|
|
78
|
+
- 7ea8ef758865ecf6edb712d3534d2974.png
|
|
79
|
+
- 0ffe164ebe4aa2eb43f8d87f87683f7f.png
|
|
80
80
|
components:
|
|
81
81
|
- name: image-bin
|
|
82
82
|
source:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.12",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -44,31 +44,31 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@abtnode/cron": "^1.16.46",
|
|
47
|
-
"@arcblock/did": "^1.21.
|
|
47
|
+
"@arcblock/did": "^1.21.1",
|
|
48
48
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
49
|
-
"@arcblock/did-connect": "^3.0.
|
|
50
|
-
"@arcblock/did-util": "^1.21.
|
|
51
|
-
"@arcblock/jwt": "^1.21.
|
|
52
|
-
"@arcblock/ux": "^3.0.
|
|
53
|
-
"@arcblock/validator": "^1.21.
|
|
54
|
-
"@blocklet/did-space-js": "^1.1.
|
|
49
|
+
"@arcblock/did-connect": "^3.0.39",
|
|
50
|
+
"@arcblock/did-util": "^1.21.1",
|
|
51
|
+
"@arcblock/jwt": "^1.21.1",
|
|
52
|
+
"@arcblock/ux": "^3.0.39",
|
|
53
|
+
"@arcblock/validator": "^1.21.1",
|
|
54
|
+
"@blocklet/did-space-js": "^1.1.13",
|
|
55
55
|
"@blocklet/error": "^0.2.5",
|
|
56
56
|
"@blocklet/js-sdk": "^1.16.46",
|
|
57
57
|
"@blocklet/logger": "^1.16.46",
|
|
58
|
-
"@blocklet/payment-react": "1.19.
|
|
58
|
+
"@blocklet/payment-react": "1.19.12",
|
|
59
59
|
"@blocklet/sdk": "^1.16.46",
|
|
60
|
-
"@blocklet/ui-react": "^3.0.
|
|
61
|
-
"@blocklet/uploader": "^0.2.
|
|
62
|
-
"@blocklet/xss": "^0.2.
|
|
60
|
+
"@blocklet/ui-react": "^3.0.39",
|
|
61
|
+
"@blocklet/uploader": "^0.2.5",
|
|
62
|
+
"@blocklet/xss": "^0.2.3",
|
|
63
63
|
"@mui/icons-material": "^7.1.2",
|
|
64
64
|
"@mui/lab": "7.0.0-beta.14",
|
|
65
65
|
"@mui/material": "^7.1.2",
|
|
66
66
|
"@mui/system": "^7.1.1",
|
|
67
|
-
"@ocap/asset": "^1.21.
|
|
68
|
-
"@ocap/client": "^1.21.
|
|
69
|
-
"@ocap/mcrypto": "^1.21.
|
|
70
|
-
"@ocap/util": "^1.21.
|
|
71
|
-
"@ocap/wallet": "^1.21.
|
|
67
|
+
"@ocap/asset": "^1.21.1",
|
|
68
|
+
"@ocap/client": "^1.21.1",
|
|
69
|
+
"@ocap/mcrypto": "^1.21.1",
|
|
70
|
+
"@ocap/util": "^1.21.1",
|
|
71
|
+
"@ocap/wallet": "^1.21.1",
|
|
72
72
|
"@stripe/react-stripe-js": "^2.9.0",
|
|
73
73
|
"@stripe/stripe-js": "^2.4.0",
|
|
74
74
|
"ahooks": "^3.8.5",
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
"devDependencies": {
|
|
125
125
|
"@abtnode/types": "^1.16.46",
|
|
126
126
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
127
|
-
"@blocklet/payment-types": "1.19.
|
|
127
|
+
"@blocklet/payment-types": "1.19.12",
|
|
128
128
|
"@types/cookie-parser": "^1.4.9",
|
|
129
129
|
"@types/cors": "^2.8.19",
|
|
130
130
|
"@types/debug": "^4.1.12",
|
|
@@ -170,5 +170,5 @@
|
|
|
170
170
|
"parser": "typescript"
|
|
171
171
|
}
|
|
172
172
|
},
|
|
173
|
-
"gitHead": "
|
|
173
|
+
"gitHead": "2496138990f95bb979125127a06c5e9d731877c9"
|
|
174
174
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -190,8 +190,8 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
190
190
|
},
|
|
191
191
|
}}>
|
|
192
192
|
<Tab label={t('admin.creditGrants.overview')} value={CreditTab.OVERVIEW} />
|
|
193
|
-
<Tab label={t('admin.creditGrants.
|
|
194
|
-
<Tab label={t('admin.creditTransactions.
|
|
193
|
+
<Tab label={t('admin.creditGrants.tab')} value={CreditTab.GRANTS} />
|
|
194
|
+
<Tab label={t('admin.creditTransactions.tab')} value={CreditTab.TRANSACTIONS} />
|
|
195
195
|
</Tabs>
|
|
196
196
|
{/* 概览标签页 */}
|
|
197
197
|
{creditTab === CreditTab.OVERVIEW && (
|
|
@@ -66,7 +66,7 @@ export default function MeterActions({ data, variant = 'compact', onChange }: Me
|
|
|
66
66
|
if (variant === 'compact') {
|
|
67
67
|
actions.push({
|
|
68
68
|
label: t('admin.meter.view'),
|
|
69
|
-
handler: () => navigate(`/admin/billing
|
|
69
|
+
handler: () => navigate(`/admin/billing/${data.id}`),
|
|
70
70
|
color: 'primary',
|
|
71
71
|
});
|
|
72
72
|
}
|
|
@@ -15,7 +15,6 @@ import type {
|
|
|
15
15
|
TMeter,
|
|
16
16
|
TMeterExpanded,
|
|
17
17
|
TPaymentCurrency,
|
|
18
|
-
TPaymentCurrencyExpanded,
|
|
19
18
|
TPaymentMethodExpanded,
|
|
20
19
|
TPrice,
|
|
21
20
|
TPriceExpanded,
|
|
@@ -102,16 +101,6 @@ const hasMoreCurrency = (methods: TPaymentMethodExpanded[] = []) => {
|
|
|
102
101
|
return methods.every((method) => method.payment_currencies.length > 1) || methods.length > 1;
|
|
103
102
|
};
|
|
104
103
|
|
|
105
|
-
function stripeCurrencyValidate(v: number, currency: TPaymentCurrencyExpanded | null) {
|
|
106
|
-
if (!currency) {
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
if (currency.paymentMethod?.type === 'stripe') {
|
|
110
|
-
return v >= 0.5;
|
|
111
|
-
}
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
104
|
const fetchMeters = (): Promise<{ list: TMeterExpanded[]; count: number }> => {
|
|
116
105
|
return api.get('/api/meters?status=active&limit=100').then((res: any) => res.data);
|
|
117
106
|
};
|
|
@@ -397,10 +386,6 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
|
|
|
397
386
|
required: t('admin.price.unit_amount.required'),
|
|
398
387
|
validate: (v) => {
|
|
399
388
|
const currency = findCurrency(settings.paymentMethods, defaultCurrencyId);
|
|
400
|
-
const hasStripError = !stripeCurrencyValidate(v, currency);
|
|
401
|
-
if (hasStripError) {
|
|
402
|
-
return t('admin.price.unit_amount.stripeTip');
|
|
403
|
-
}
|
|
404
389
|
return validateAmount(v, currency ?? {});
|
|
405
390
|
},
|
|
406
391
|
}}
|
|
@@ -482,10 +467,6 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
|
|
|
482
467
|
rules={{
|
|
483
468
|
required: t('admin.price.unit_amount.required'),
|
|
484
469
|
validate: (v) => {
|
|
485
|
-
const hasStripError = !stripeCurrencyValidate(v, currency);
|
|
486
|
-
if (hasStripError) {
|
|
487
|
-
return t('admin.price.unit_amount.stripeTip');
|
|
488
|
-
}
|
|
489
470
|
return validateAmount(v, currency ?? {});
|
|
490
471
|
},
|
|
491
472
|
}}
|
package/src/locales/en.tsx
CHANGED
|
@@ -1144,11 +1144,12 @@ export default flat({
|
|
|
1144
1144
|
appBalance: 'App Balance',
|
|
1145
1145
|
},
|
|
1146
1146
|
creditGrants: {
|
|
1147
|
-
|
|
1147
|
+
tab: 'Grants',
|
|
1148
|
+
title: 'Credits',
|
|
1148
1149
|
summary: 'Credit Summary',
|
|
1149
1150
|
noGrants: 'No credit grants found',
|
|
1150
1151
|
grantDetail: 'Credit Grant Details',
|
|
1151
|
-
overview: '
|
|
1152
|
+
overview: 'Overview',
|
|
1152
1153
|
overviewDescription: 'Monitor credit balances, usage, and outstanding debt across all currencies.',
|
|
1153
1154
|
availableBalance: 'Available Balance',
|
|
1154
1155
|
usage: 'Usage',
|
|
@@ -1180,7 +1181,8 @@ export default flat({
|
|
|
1180
1181
|
viewDetails: 'View Details',
|
|
1181
1182
|
},
|
|
1182
1183
|
creditTransactions: {
|
|
1183
|
-
|
|
1184
|
+
tab: 'Transactions',
|
|
1185
|
+
title: 'Transactions',
|
|
1184
1186
|
summary: 'Transaction Summary',
|
|
1185
1187
|
noTransactions: 'No credit transactions found',
|
|
1186
1188
|
totalTransactions: 'Total Transactions',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -927,11 +927,12 @@ export default flat({
|
|
|
927
927
|
postal_code: '邮政编码',
|
|
928
928
|
},
|
|
929
929
|
creditGrants: {
|
|
930
|
+
tab: '信用额度',
|
|
930
931
|
title: '信用额度',
|
|
931
932
|
summary: '额度汇总',
|
|
932
933
|
noGrants: '未找到信用额度',
|
|
933
934
|
grantDetail: '信用额度详情',
|
|
934
|
-
overview: '
|
|
935
|
+
overview: '概览',
|
|
935
936
|
overviewDescription: '监控所有货币的信用余额、使用情况和未偿债务。',
|
|
936
937
|
availableBalance: '可用余额',
|
|
937
938
|
usage: '使用情况',
|
|
@@ -1103,7 +1104,8 @@ export default flat({
|
|
|
1103
1104
|
appBalance: '热钱包余额',
|
|
1104
1105
|
},
|
|
1105
1106
|
creditGrants: {
|
|
1106
|
-
|
|
1107
|
+
tab: '信用额度',
|
|
1108
|
+
title: '额度',
|
|
1107
1109
|
summary: '额度汇总',
|
|
1108
1110
|
noGrants: '未找到信用额度',
|
|
1109
1111
|
grantDetail: '信用额度详情',
|
|
@@ -1139,6 +1141,7 @@ export default flat({
|
|
|
1139
1141
|
viewDetails: '查看详情',
|
|
1140
1142
|
},
|
|
1141
1143
|
creditTransactions: {
|
|
1144
|
+
tab: '账单',
|
|
1142
1145
|
title: '额度账单',
|
|
1143
1146
|
summary: '账单汇总',
|
|
1144
1147
|
noTransactions: '未找到额度账单',
|
package/screenshots/checkout.png
DELETED
|
Binary file
|
package/screenshots/customer.png
DELETED
|
Binary file
|
package/screenshots/payment.png
DELETED
|
Binary file
|
package/screenshots/setting.png
DELETED
|
Binary file
|
|
Binary file
|