payment-kit 1.15.17 → 1.15.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/integrations/stripe/handlers/invoice.ts +20 -0
- package/api/src/libs/audit.ts +1 -1
- package/api/src/libs/invoice.ts +81 -1
- package/api/src/libs/notification/template/billing-discrepancy.ts +223 -0
- package/api/src/libs/notification/template/subscription-canceled.ts +11 -0
- package/api/src/libs/notification/template/subscription-refund-succeeded.ts +10 -2
- package/api/src/libs/notification/template/subscription-renew-failed.ts +10 -2
- package/api/src/libs/notification/template/subscription-renewed.ts +10 -2
- package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +11 -1
- package/api/src/libs/notification/template/subscription-succeeded.ts +11 -1
- package/api/src/libs/notification/template/subscription-trial-start.ts +11 -0
- package/api/src/libs/notification/template/subscription-trial-will-end.ts +11 -0
- package/api/src/libs/notification/template/subscription-upgraded.ts +11 -1
- package/api/src/libs/notification/template/subscription-will-canceled.ts +10 -0
- package/api/src/libs/notification/template/subscription-will-renew.ts +10 -1
- package/api/src/libs/notification/template/usage-report-empty.ts +158 -0
- package/api/src/libs/subscription.ts +67 -0
- package/api/src/libs/util.ts +30 -0
- package/api/src/locales/en.ts +13 -0
- package/api/src/locales/zh.ts +13 -0
- package/api/src/queues/invoice.ts +18 -0
- package/api/src/queues/notification.ts +43 -1
- package/api/src/queues/subscription.ts +21 -2
- package/api/src/routes/checkout-sessions.ts +26 -0
- package/api/src/routes/subscriptions.ts +5 -3
- package/api/src/store/models/checkout-session.ts +2 -0
- package/api/src/store/models/types.ts +22 -4
- package/api/src/store/models/usage-record.ts +5 -1
- package/api/tests/libs/subscription.spec.ts +58 -1
- package/api/tests/libs/util.spec.ts +135 -0
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/scripts/sdk.js +37 -3
- package/src/components/invoice/list.tsx +0 -1
- package/src/components/invoice/table.tsx +7 -2
- package/src/components/subscription/items/index.tsx +26 -7
- package/src/components/subscription/items/usage-records.tsx +21 -10
- package/src/components/subscription/portal/actions.tsx +16 -14
- package/src/libs/util.ts +51 -0
- package/src/locales/en.tsx +2 -0
- package/src/locales/zh.tsx +2 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +1 -1
- package/src/pages/customer/subscription/embed.tsx +16 -14
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import Empty from '@arcblock/ux/lib/Empty';
|
|
3
3
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
4
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
5
|
-
import { ConfirmDialog, api, formatError, usePaymentContext } from '@blocklet/payment-react';
|
|
5
|
+
import { ConfirmDialog, api, formatError, formatTime, usePaymentContext } from '@blocklet/payment-react';
|
|
6
6
|
import type { TUsageRecord } from '@blocklet/payment-types';
|
|
7
|
-
import { Alert, Box, Button, CircularProgress, TextField } from '@mui/material';
|
|
7
|
+
import { Alert, Box, Button, CircularProgress, TextField, Typography } from '@mui/material';
|
|
8
8
|
import { useRequest } from 'ahooks';
|
|
9
9
|
import { useState } from 'react';
|
|
10
10
|
import { Bar, BarChart, Rectangle, Tooltip, XAxis, YAxis } from 'recharts';
|
|
@@ -47,17 +47,21 @@ function addUsageQuantity({
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function UsageRecordDialog({
|
|
50
|
+
title,
|
|
50
51
|
subscriptionId,
|
|
51
52
|
id,
|
|
52
53
|
onConfirm,
|
|
53
54
|
start = 0,
|
|
54
55
|
end = 0,
|
|
56
|
+
disableAddUsageQuantity = false,
|
|
55
57
|
}: {
|
|
58
|
+
title?: string;
|
|
56
59
|
subscriptionId: string;
|
|
57
60
|
id: string;
|
|
58
61
|
onConfirm: any;
|
|
59
62
|
start?: number;
|
|
60
63
|
end?: number;
|
|
64
|
+
disableAddUsageQuantity?: boolean;
|
|
61
65
|
}) {
|
|
62
66
|
const { t } = useLocaleContext();
|
|
63
67
|
const { loading, error, data } = useRequest(() => fetchData(subscriptionId, id, start, end), {
|
|
@@ -69,7 +73,7 @@ export function UsageRecordDialog({
|
|
|
69
73
|
if (error) {
|
|
70
74
|
return (
|
|
71
75
|
<ConfirmDialog
|
|
72
|
-
title={t('admin.subscription.usage.current')}
|
|
76
|
+
title={title || t('admin.subscription.usage.current')}
|
|
73
77
|
message={<Alert severity="error">{error.message}</Alert>}
|
|
74
78
|
onConfirm={onConfirm}
|
|
75
79
|
onCancel={onConfirm}
|
|
@@ -82,7 +86,7 @@ export function UsageRecordDialog({
|
|
|
82
86
|
if (loading || !data) {
|
|
83
87
|
return (
|
|
84
88
|
<ConfirmDialog
|
|
85
|
-
title={t('admin.subscription.usage.current')}
|
|
89
|
+
title={title || t('admin.subscription.usage.current')}
|
|
86
90
|
message={<CircularProgress />}
|
|
87
91
|
onConfirm={onConfirm}
|
|
88
92
|
onCancel={onConfirm}
|
|
@@ -109,12 +113,17 @@ export function UsageRecordDialog({
|
|
|
109
113
|
};
|
|
110
114
|
return (
|
|
111
115
|
<ConfirmDialog
|
|
112
|
-
title={t('admin.subscription.usage.current')}
|
|
116
|
+
title={title || t('admin.subscription.usage.current')}
|
|
113
117
|
message={
|
|
114
118
|
<>
|
|
119
|
+
{start && end ? (
|
|
120
|
+
<Typography variant="h6" mb={2}>
|
|
121
|
+
{t('admin.subscription.usage.cycle')}: {formatTime(start * 1000)} - {formatTime(end * 1000)}
|
|
122
|
+
</Typography>
|
|
123
|
+
) : null}
|
|
115
124
|
{data.list.length > 0 ? (
|
|
116
125
|
<BarChart
|
|
117
|
-
width={
|
|
126
|
+
width={540}
|
|
118
127
|
height={240}
|
|
119
128
|
data={data.list.map((item) => ({
|
|
120
129
|
...item,
|
|
@@ -123,7 +132,7 @@ export function UsageRecordDialog({
|
|
|
123
132
|
margin={{
|
|
124
133
|
top: 5,
|
|
125
134
|
right: 5,
|
|
126
|
-
left:
|
|
135
|
+
left: 5,
|
|
127
136
|
bottom: 5,
|
|
128
137
|
}}>
|
|
129
138
|
<Tooltip />
|
|
@@ -138,8 +147,8 @@ export function UsageRecordDialog({
|
|
|
138
147
|
) : (
|
|
139
148
|
<Empty>{t('admin.usageRecord.empty')}</Empty>
|
|
140
149
|
)}
|
|
141
|
-
{!settings.livemode && window.location.pathname.includes('/admin/billing') && (
|
|
142
|
-
<Box sx={{ display: 'flex', justifyContent: 'center' }} pt={1} pb={1}>
|
|
150
|
+
{!settings.livemode && window.location.pathname.includes('/admin/billing') && !disableAddUsageQuantity && (
|
|
151
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }} pt={1} pb={1} gap={1}>
|
|
143
152
|
<TextField
|
|
144
153
|
id="add-usage-record"
|
|
145
154
|
label={t('admin.usageRecord.add.quantity')}
|
|
@@ -151,7 +160,9 @@ export function UsageRecordDialog({
|
|
|
151
160
|
value={usageQuantity}
|
|
152
161
|
onChange={(e) => setUsageQuantity(+e.target.value)}
|
|
153
162
|
/>
|
|
154
|
-
<Button onClick={handAddUsageQuantity}
|
|
163
|
+
<Button onClick={handAddUsageQuantity} sx={{ color: 'text.link' }}>
|
|
164
|
+
{t('admin.usageRecord.add.label')}
|
|
165
|
+
</Button>
|
|
155
166
|
</Box>
|
|
156
167
|
)}
|
|
157
168
|
</>
|
|
@@ -167,20 +167,22 @@ export function SubscriptionActionsInner({ subscription, showExtra, onChange, ac
|
|
|
167
167
|
{action?.text || t('admin.subscription.batchPay.button')}
|
|
168
168
|
</Button>
|
|
169
169
|
)}
|
|
170
|
-
{subscription.service_actions
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
170
|
+
{subscription.service_actions
|
|
171
|
+
?.filter((x: any) => x?.type !== 'notification')
|
|
172
|
+
.map((x) => (
|
|
173
|
+
// @ts-ignore
|
|
174
|
+
<Button
|
|
175
|
+
component={Link}
|
|
176
|
+
key={x.name}
|
|
177
|
+
variant={x?.variant || 'contained'}
|
|
178
|
+
color={x?.color || 'primary'}
|
|
179
|
+
href={x.link}
|
|
180
|
+
size="small"
|
|
181
|
+
target="_blank"
|
|
182
|
+
sx={{ textDecoration: 'none !important' }}>
|
|
183
|
+
{x.text[locale] || x.text.en || x.name}
|
|
184
|
+
</Button>
|
|
185
|
+
))}
|
|
184
186
|
{state.action === 'cancel' && state.subscription && (
|
|
185
187
|
<ConfirmDialog
|
|
186
188
|
onConfirm={handleCancel}
|
package/src/libs/util.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { formatCheckoutHeadlines, formatPrice, getPrefix, getPriceCurrencyOptions } from '@blocklet/payment-react';
|
|
5
5
|
import type {
|
|
6
6
|
LineItem,
|
|
7
|
+
PriceRecurring,
|
|
8
|
+
TInvoiceExpanded,
|
|
7
9
|
TLineItemExpanded,
|
|
8
10
|
TPaymentCurrency,
|
|
9
11
|
TPaymentLinkExpanded,
|
|
@@ -271,3 +273,52 @@ export function isEmptyExceptNumber(value: any): boolean {
|
|
|
271
273
|
}
|
|
272
274
|
return isEmpty(value);
|
|
273
275
|
}
|
|
276
|
+
|
|
277
|
+
export function getRecurringPeriod(recurring: PriceRecurring) {
|
|
278
|
+
const { interval } = recurring;
|
|
279
|
+
const count = +recurring.interval_count || 1;
|
|
280
|
+
const dayInMs = 24 * 60 * 60 * 1000;
|
|
281
|
+
|
|
282
|
+
switch (interval) {
|
|
283
|
+
case 'hour':
|
|
284
|
+
return 60 * 60 * 1000;
|
|
285
|
+
case 'day':
|
|
286
|
+
return count * dayInMs;
|
|
287
|
+
case 'week':
|
|
288
|
+
return count * 7 * dayInMs;
|
|
289
|
+
case 'month':
|
|
290
|
+
return count * 30 * dayInMs;
|
|
291
|
+
case 'year':
|
|
292
|
+
return count * 365 * dayInMs;
|
|
293
|
+
default:
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function getInvoiceUsageReportStartEnd(invoice: TInvoiceExpanded, showPreviousPeriod: boolean = false) {
|
|
299
|
+
const { subscription, paymentMethod } = invoice;
|
|
300
|
+
const usageReportRange = {
|
|
301
|
+
start: invoice.metadata?.usage_start || invoice.period_start,
|
|
302
|
+
end: invoice.metadata?.usage_end || invoice.period_end,
|
|
303
|
+
};
|
|
304
|
+
if (!subscription || !showPreviousPeriod) {
|
|
305
|
+
return usageReportRange;
|
|
306
|
+
}
|
|
307
|
+
if (invoice?.billing_reason === 'subscription_cancel') {
|
|
308
|
+
return usageReportRange;
|
|
309
|
+
}
|
|
310
|
+
const cycle = getRecurringPeriod(subscription.pending_invoice_item_interval);
|
|
311
|
+
let offset = 0;
|
|
312
|
+
if (['arcblock', 'ethereum'].includes(paymentMethod.type)) {
|
|
313
|
+
switch (invoice?.billing_reason) {
|
|
314
|
+
case 'subscription_cycle':
|
|
315
|
+
offset = cycle / 1000;
|
|
316
|
+
break;
|
|
317
|
+
default:
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
usageReportRange.start = invoice.period_start - offset;
|
|
322
|
+
usageReportRange.end = invoice.period_end - offset;
|
|
323
|
+
return usageReportRange;
|
|
324
|
+
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -500,9 +500,11 @@ export default flat({
|
|
|
500
500
|
usage: {
|
|
501
501
|
title: 'Usage records',
|
|
502
502
|
current: 'Usage records for current period',
|
|
503
|
+
range: 'Usage records for {start} - {end}',
|
|
503
504
|
view: 'View usage',
|
|
504
505
|
vary: 'Varies with usage',
|
|
505
506
|
used: 'Unit used',
|
|
507
|
+
cycle: 'Usage cycle',
|
|
506
508
|
},
|
|
507
509
|
batchPay: {
|
|
508
510
|
button: 'Pay due invoices',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -489,9 +489,11 @@ export default flat({
|
|
|
489
489
|
usage: {
|
|
490
490
|
title: '使用记录',
|
|
491
491
|
current: '当前周期的使用记录',
|
|
492
|
+
range: '周期使用记录({start} - {end})',
|
|
492
493
|
view: '查看使用情况',
|
|
493
494
|
vary: '随使用情况变化',
|
|
494
495
|
used: '已使用单位',
|
|
496
|
+
cycle: '统计周期',
|
|
495
497
|
},
|
|
496
498
|
batchPay: {
|
|
497
499
|
button: '批量付款',
|
|
@@ -315,7 +315,7 @@ export default function SubscriptionDetail(props: { id: string }) {
|
|
|
315
315
|
<Box className="section">
|
|
316
316
|
<SectionHeader title={t('admin.product.pricing')} />
|
|
317
317
|
<Box className="section-body">
|
|
318
|
-
<SubscriptionItemList data={data.items} currency={data.paymentCurrency} />
|
|
318
|
+
<SubscriptionItemList data={data.items} currency={data.paymentCurrency} mode="admin" />
|
|
319
319
|
</Box>
|
|
320
320
|
</Box>
|
|
321
321
|
<Divider />
|
|
@@ -228,20 +228,22 @@ export default function SubscriptionEmbed() {
|
|
|
228
228
|
</List>
|
|
229
229
|
</Box>
|
|
230
230
|
<Stack direction="row" justifyContent="center" spacing={2} sx={{ mt: 2 }}>
|
|
231
|
-
{subscription.service_actions
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
231
|
+
{subscription.service_actions
|
|
232
|
+
?.filter((x: any) => x?.type !== 'notification')
|
|
233
|
+
?.map((x) => (
|
|
234
|
+
// @ts-ignore
|
|
235
|
+
<Button
|
|
236
|
+
component={Link}
|
|
237
|
+
key={x.name}
|
|
238
|
+
variant={x?.variant || 'contained'}
|
|
239
|
+
color={x.color || 'primary'}
|
|
240
|
+
href={x.link}
|
|
241
|
+
size="small"
|
|
242
|
+
target="_blank"
|
|
243
|
+
sx={{ textDecoration: 'none !important' }}>
|
|
244
|
+
{x.text[locale] || x.text.en || x.name}
|
|
245
|
+
</Button>
|
|
246
|
+
))}
|
|
245
247
|
<Button
|
|
246
248
|
variant="contained"
|
|
247
249
|
sx={{ color: '#fff!important', width: subscription.service_actions?.length ? 'auto' : '100%' }}
|