payment-kit 1.15.17 → 1.15.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/api/src/integrations/stripe/handlers/invoice.ts +20 -0
  2. package/api/src/libs/audit.ts +1 -1
  3. package/api/src/libs/invoice.ts +81 -1
  4. package/api/src/libs/notification/template/billing-discrepancy.ts +223 -0
  5. package/api/src/libs/notification/template/subscription-canceled.ts +11 -0
  6. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +10 -2
  7. package/api/src/libs/notification/template/subscription-renew-failed.ts +10 -2
  8. package/api/src/libs/notification/template/subscription-renewed.ts +10 -2
  9. package/api/src/libs/notification/template/subscription-stake-slash-succeeded.ts +11 -1
  10. package/api/src/libs/notification/template/subscription-succeeded.ts +11 -1
  11. package/api/src/libs/notification/template/subscription-trial-start.ts +11 -0
  12. package/api/src/libs/notification/template/subscription-trial-will-end.ts +11 -0
  13. package/api/src/libs/notification/template/subscription-upgraded.ts +11 -1
  14. package/api/src/libs/notification/template/subscription-will-canceled.ts +10 -0
  15. package/api/src/libs/notification/template/subscription-will-renew.ts +10 -1
  16. package/api/src/libs/notification/template/usage-report-empty.ts +158 -0
  17. package/api/src/libs/subscription.ts +67 -0
  18. package/api/src/libs/util.ts +30 -0
  19. package/api/src/locales/en.ts +13 -0
  20. package/api/src/locales/zh.ts +13 -0
  21. package/api/src/queues/invoice.ts +18 -0
  22. package/api/src/queues/notification.ts +43 -1
  23. package/api/src/queues/subscription.ts +21 -2
  24. package/api/src/routes/checkout-sessions.ts +26 -0
  25. package/api/src/routes/subscriptions.ts +5 -3
  26. package/api/src/store/models/checkout-session.ts +2 -0
  27. package/api/src/store/models/types.ts +22 -4
  28. package/api/src/store/models/usage-record.ts +5 -1
  29. package/api/tests/libs/subscription.spec.ts +58 -1
  30. package/api/tests/libs/util.spec.ts +135 -0
  31. package/blocklet.yml +1 -1
  32. package/package.json +4 -4
  33. package/scripts/sdk.js +37 -3
  34. package/src/components/invoice/list.tsx +0 -1
  35. package/src/components/invoice/table.tsx +7 -2
  36. package/src/components/subscription/items/index.tsx +26 -7
  37. package/src/components/subscription/items/usage-records.tsx +21 -10
  38. package/src/components/subscription/portal/actions.tsx +16 -14
  39. package/src/libs/util.ts +51 -0
  40. package/src/locales/en.tsx +2 -0
  41. package/src/locales/zh.tsx +2 -0
  42. package/src/pages/admin/billing/subscriptions/detail.tsx +1 -1
  43. 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={480}
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: 0,
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}>{t('admin.usageRecord.add.label')}</Button>
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?.map((x) => (
171
- // @ts-ignore
172
- <Button
173
- component={Link}
174
- key={x.name}
175
- variant={x.variant}
176
- color={x.color}
177
- href={x.link}
178
- size="small"
179
- target="_blank"
180
- sx={{ textDecoration: 'none !important' }}>
181
- {x.text[locale] || x.text.en || x.name}
182
- </Button>
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
+ }
@@ -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',
@@ -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?.map((x) => (
232
- // @ts-ignore
233
- <Button
234
- component={Link}
235
- key={x.name}
236
- variant={x.variant}
237
- color={x.color}
238
- href={x.link}
239
- size="small"
240
- target="_blank"
241
- sx={{ textDecoration: 'none !important' }}>
242
- {x.text[locale] || x.text.en || x.name}
243
- </Button>
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%' }}