payment-kit 1.13.65 → 1.13.67

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 (49) hide show
  1. package/api/src/index.ts +4 -0
  2. package/api/src/integrations/blocklet/notification.ts +5 -3
  3. package/api/src/jobs/notification.ts +142 -0
  4. package/api/src/jobs/payment.ts +14 -0
  5. package/api/src/jobs/subscription.ts +2 -2
  6. package/api/src/libs/audit.ts +3 -1
  7. package/api/src/libs/env.ts +3 -0
  8. package/api/src/libs/event.ts +10 -1
  9. package/api/src/libs/invoice.ts +5 -0
  10. package/api/src/libs/notification/index.ts +23 -0
  11. package/api/src/libs/notification/template/base.ts +12 -0
  12. package/api/src/libs/notification/template/subscription-renew-failed.ts +286 -0
  13. package/api/src/libs/notification/template/subscription-renewed.ts +259 -0
  14. package/api/src/libs/notification/template/subscription-succeeded.ts +279 -0
  15. package/api/src/libs/notification/template/subscription-trial-start.ts +267 -0
  16. package/api/src/libs/notification/template/subscription-trial-will-end.ts +250 -0
  17. package/api/src/libs/notification/template/subscription-will-renew.ts +232 -0
  18. package/api/src/libs/payment.ts +100 -3
  19. package/api/src/libs/product.ts +19 -0
  20. package/api/src/libs/queue/index.ts +13 -0
  21. package/api/src/libs/subscription.ts +5 -0
  22. package/api/src/libs/time.ts +17 -0
  23. package/api/src/libs/util.ts +39 -0
  24. package/api/src/locales/en.ts +67 -0
  25. package/api/src/locales/zh.ts +64 -0
  26. package/api/src/routes/connect/collect.ts +6 -0
  27. package/api/src/schedule/index.ts +28 -0
  28. package/api/src/schedule/interface/diff.ts +9 -0
  29. package/api/src/schedule/subscription-trail-will-end.ts +197 -0
  30. package/api/src/schedule/subscription-will-renew.ts +195 -0
  31. package/api/src/store/models/subscription.ts +30 -12
  32. package/api/src/store/models/types.ts +13 -12
  33. package/api/third.d.ts +2 -0
  34. package/blocklet.yml +1 -1
  35. package/package.json +9 -7
  36. package/src/app.tsx +2 -0
  37. package/src/components/invoice/action.tsx +25 -7
  38. package/src/components/invoice/list.tsx +19 -4
  39. package/src/components/portal/invoice/list.tsx +1 -1
  40. package/src/components/portal/subscription/list.tsx +6 -5
  41. package/src/components/subscription/items/index.tsx +8 -4
  42. package/src/libs/util.ts +2 -2
  43. package/src/locales/en.tsx +5 -1
  44. package/src/locales/zh.tsx +5 -1
  45. package/src/pages/checkout/pricing-table.tsx +1 -1
  46. package/src/pages/customer/index.tsx +13 -2
  47. package/src/pages/customer/invoice.tsx +5 -4
  48. package/src/pages/customer/subscription/index.tsx +163 -0
  49. package/tsconfig.api.json +6 -1
@@ -39,6 +39,12 @@ type ListProps = {
39
39
  customer_id?: string;
40
40
  subscription_id?: string;
41
41
  status?: string;
42
+
43
+ invoiceProps?: {
44
+ onClick?: (invoice: TInvoiceExpanded) => void | Promise<void>;
45
+ };
46
+
47
+ mode?: 'admin' | 'customer';
42
48
  };
43
49
 
44
50
  const getListKey = (props: ListProps) => {
@@ -60,9 +66,13 @@ InvoiceList.defaultProps = {
60
66
  customer_id: '',
61
67
  subscription_id: '',
62
68
  status: '',
69
+
70
+ invoiceProps: {},
71
+
72
+ mode: 'admin',
63
73
  };
64
74
 
65
- export default function InvoiceList({ customer_id, subscription_id, features, status }: ListProps) {
75
+ export default function InvoiceList({ customer_id, subscription_id, features, status, invoiceProps, mode }: ListProps) {
66
76
  const listKey = getListKey({ customer_id, subscription_id });
67
77
  const persisted = getDurableData(listKey);
68
78
 
@@ -159,7 +169,7 @@ export default function InvoiceList({ customer_id, subscription_id, features, st
159
169
  sort: false,
160
170
  customBodyRenderLite: (_: string, index: number) => {
161
171
  const item = data.list[index] as TInvoiceExpanded;
162
- return <InvoiceActions data={item} onChange={refresh} />;
172
+ return <InvoiceActions data={item} onChange={refresh} mode={mode} />;
163
173
  },
164
174
  },
165
175
  },
@@ -200,9 +210,14 @@ export default function InvoiceList({ customer_id, subscription_id, features, st
200
210
  count: data.count,
201
211
  page: search.page - 1,
202
212
  rowsPerPage: search.pageSize,
203
- onRowClick: (_: any, { dataIndex }: any) => {
213
+ onRowClick: async (_: any, { dataIndex }: any) => {
204
214
  const item = data.list[dataIndex] as TInvoiceExpanded;
205
- navigate(`/admin/billing/${item.id}`);
215
+
216
+ if (invoiceProps?.onClick) {
217
+ await invoiceProps.onClick(item);
218
+ } else {
219
+ navigate(`/admin/billing/${item.id}`);
220
+ }
206
221
  },
207
222
  }}
208
223
  toolbar={features?.toolbar}
@@ -85,7 +85,7 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
85
85
  <Typography component="span">{invoice.number}</Typography>
86
86
  </Link>
87
87
  </Box>
88
- <Box flex={1}>
88
+ <Box flex={2}>
89
89
  <Typography>{formatToDate(invoice.created_at)}</Typography>
90
90
  </Box>
91
91
  <Box flex={1}>
@@ -3,7 +3,7 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
4
  import type { Paginated, TSubscriptionExpanded } from '@did-pay/types';
5
5
  import { ScheduleOutlined } from '@mui/icons-material';
6
- import { Avatar, AvatarGroup, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
6
+ import { Avatar, AvatarGroup, Box, Button, CircularProgress, Stack, StackProps, Typography } from '@mui/material';
7
7
  import { useInfiniteScroll, useSetState } from 'ahooks';
8
8
  import { FormProvider, useForm, useFormContext } from 'react-hook-form';
9
9
 
@@ -29,12 +29,13 @@ const fetchData = (params: Record<string, any> = {}): Promise<Paginated<TSubscri
29
29
 
30
30
  type Props = {
31
31
  id: string;
32
- onChange: Function;
33
- };
32
+ onChange: (action?: string) => any | Promise<any>;
33
+ onClickSubscription: (subscription: TSubscriptionExpanded) => void | Promise<void>;
34
+ } & Omit<StackProps, 'onChange'>;
34
35
 
35
36
  const pageSize = 4;
36
37
 
37
- export function CurrentSubscriptionsInner({ id, onChange }: Props) {
38
+ export function CurrentSubscriptionsInner({ id, onChange, onClickSubscription, ...rest }: Props) {
38
39
  const { t } = useLocaleContext();
39
40
  const { reset, getValues } = useFormContext();
40
41
 
@@ -108,7 +109,7 @@ export function CurrentSubscriptionsInner({ id, onChange }: Props) {
108
109
  sm: 3,
109
110
  }}
110
111
  flexWrap="wrap">
111
- <Stack direction="column" spacing={0.5}>
112
+ <Stack direction="column" spacing={0.5} onClick={() => onClickSubscription(subscription)} {...rest}>
112
113
  <Stack direction="row" spacing={1} alignItems="center">
113
114
  <AvatarGroup max={3}>
114
115
  {subscription.items.map((item) =>
@@ -12,11 +12,14 @@ import UsageRecords from './usage-records';
12
12
  type ListProps = {
13
13
  data: TSubscriptionItemExpanded[];
14
14
  currency: TPaymentCurrency;
15
+ mode?: 'admin' | 'customer';
15
16
  };
16
17
 
17
- SubscriptionItemList.defaultProps = {};
18
+ SubscriptionItemList.defaultProps = {
19
+ mode: 'customer',
20
+ };
18
21
 
19
- export default function SubscriptionItemList({ data, currency }: ListProps) {
22
+ export default function SubscriptionItemList({ data, currency, mode }: ListProps) {
20
23
  const { t } = useLocaleContext();
21
24
 
22
25
  const columns = [
@@ -73,7 +76,7 @@ export default function SubscriptionItemList({ data, currency }: ListProps) {
73
76
  },
74
77
  },
75
78
  },
76
- {
79
+ mode === 'admin' && {
77
80
  label: '',
78
81
  name: '',
79
82
  align: 'center',
@@ -81,11 +84,12 @@ export default function SubscriptionItemList({ data, currency }: ListProps) {
81
84
  sort: false,
82
85
  customBodyRenderLite: (_: string, index: number) => {
83
86
  const item = data[index] as TSubscriptionItemExpanded;
87
+
84
88
  return <LineItemActions data={item as any} />;
85
89
  },
86
90
  },
87
91
  },
88
- ];
92
+ ].filter(Boolean);
89
93
 
90
94
  return (
91
95
  <Table
package/src/libs/util.ts CHANGED
@@ -59,7 +59,7 @@ export function formatToDate(date: Date | string | number, locale = 'en') {
59
59
  return '-';
60
60
  }
61
61
 
62
- return dayjs(date).locale(formatLocale(locale)).format('ll');
62
+ return dayjs(date).locale(formatLocale(locale)).format('YYYY-MM-DD HH:mm:ss');
63
63
  }
64
64
 
65
65
  export function formatToDatetime(date: Date | string | number, locale = 'en') {
@@ -70,7 +70,7 @@ export function formatToDatetime(date: Date | string | number, locale = 'en') {
70
70
  return dayjs(date).locale(formatLocale(locale)).format('lll');
71
71
  }
72
72
 
73
- export function formatTime(date: Date | string | number, format = 'lll', locale = 'en') {
73
+ export function formatTime(date: Date | string | number, format = 'YYYY-MM-DD HH:mm:ss', locale = 'en') {
74
74
  if (!date) {
75
75
  return '-';
76
76
  }
@@ -570,7 +570,6 @@ export default flat({
570
570
  },
571
571
  },
572
572
  customer: {
573
- subscriptions: 'Current Subscriptions',
574
573
  invoices: 'Invoice History',
575
574
  details: 'Billing Details',
576
575
  update: 'Update Information',
@@ -603,5 +602,10 @@ export default flat({
603
602
  download: 'Download PDF',
604
603
  pay: 'Pay this invoice',
605
604
  },
605
+
606
+ subscriptions: {
607
+ title: 'Manage subscriptions',
608
+ current: 'Current subscriptions',
609
+ },
606
610
  },
607
611
  });
@@ -557,7 +557,6 @@ export default flat({
557
557
  },
558
558
  },
559
559
  customer: {
560
- subscriptions: '当前订阅',
561
560
  invoices: '发票历史',
562
561
  details: '计费详情',
563
562
  update: '更新客户信息',
@@ -589,5 +588,10 @@ export default flat({
589
588
  download: '下载PDF',
590
589
  pay: '支付此发票',
591
590
  },
591
+
592
+ subscriptions: {
593
+ title: '订阅管理',
594
+ current: '当前订阅',
595
+ },
592
596
  },
593
597
  });
@@ -258,7 +258,7 @@ export default function PricingTable({ id }: Props) {
258
258
  <Box>
259
259
  <Typography>{t('checkout.include')}</Typography>
260
260
  <List dense>
261
- {x.product.features.map((f) => (
261
+ {x.product.features.map((f: any) => (
262
262
  <ListItem key={f.name} disableGutters disablePadding>
263
263
  <ListItemIcon sx={{ minWidth: 25 }}>
264
264
  <CheckOutlined color="success" fontSize="small" />
@@ -6,6 +6,7 @@ import { Alert, Box, Button, CircularProgress, Grid, Stack } from '@mui/material
6
6
  import { styled } from '@mui/system';
7
7
  import { useRequest, useSetState } from 'ahooks';
8
8
  import { FlagEmoji } from 'react-international-phone';
9
+ import { useNavigate } from 'react-router-dom';
9
10
 
10
11
  import EditCustomer from '../../components/customer/edit';
11
12
  import InfoRow from '../../components/info-row';
@@ -26,6 +27,7 @@ export default function CustomerHome() {
26
27
  editing: false,
27
28
  loading: false,
28
29
  });
30
+ const navigate = useNavigate();
29
31
 
30
32
  const { loading, error, data, runAsync } = useRequest(fetchData);
31
33
 
@@ -67,9 +69,18 @@ export default function CustomerHome() {
67
69
  <Grid item xs={12} md={8}>
68
70
  <Root direction="column" spacing={3} sx={{ my: 2 }}>
69
71
  <Box className="section">
70
- <SectionHeader title={t('customer.subscriptions')} mb={0} />
72
+ <SectionHeader title={t('customer.subscriptions.current')} mb={0} />
71
73
  <Box className="section-body">
72
- <CurrentSubscriptions id={data.id} onChange={runAsync} />
74
+ <CurrentSubscriptions
75
+ id={data.id}
76
+ onChange={runAsync}
77
+ style={{
78
+ cursor: 'pointer',
79
+ }}
80
+ onClickSubscription={(subscription) => {
81
+ navigate(`/customer/subscription/${subscription.id}`);
82
+ }}
83
+ />
73
84
  </Box>
74
85
  </Box>
75
86
  <Box className="section">
@@ -1,10 +1,11 @@
1
+ /* eslint-disable jsx-a11y/anchor-is-valid */
1
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
3
  import Toast from '@arcblock/ux/lib/Toast';
3
4
  import type { TInvoiceExpanded } from '@did-pay/types';
4
5
  import { ArrowBackOutlined } from '@mui/icons-material';
5
- import { Alert, Box, Button, CircularProgress, Grid, Stack, Typography } from '@mui/material';
6
+ import { Alert, Box, Button, CircularProgress, Grid, Link, Stack, Typography } from '@mui/material';
6
7
  import { useRequest, useSetState } from 'ahooks';
7
- import { Link, useParams } from 'react-router-dom';
8
+ import { useParams } from 'react-router-dom';
8
9
 
9
10
  import TxLink from '../../components/blockchain/tx';
10
11
  import Currency from '../../components/currency';
@@ -72,11 +73,11 @@ export default function CustomerHome() {
72
73
  <Layout>
73
74
  <Grid container spacing={3} sx={{ mt: 1 }}>
74
75
  <Grid item xs={12} md={12}>
75
- <Link to="/customer">
76
+ <Link onClick={() => window.history.back()}>
76
77
  <Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal' }}>
77
78
  <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
78
79
  <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
79
- {t('admin.invoices')}
80
+ {t('common.previous')}
80
81
  </Typography>
81
82
  </Stack>
82
83
  </Link>
@@ -0,0 +1,163 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import type { TSubscriptionExpanded } from '@did-pay/types';
4
+ import { ArrowBackOutlined } from '@mui/icons-material';
5
+ import { Alert, Box, CircularProgress, Stack, Typography } from '@mui/material';
6
+ import { styled } from '@mui/system';
7
+ import { useRequest } from 'ahooks';
8
+ import { Link, useNavigate, useParams } from 'react-router-dom';
9
+
10
+ import TxLink from '../../../components/blockchain/tx';
11
+ import Currency from '../../../components/currency';
12
+ import InfoMetric from '../../../components/info-metric';
13
+ import InfoRow from '../../../components/info-row';
14
+ import InvoiceList from '../../../components/invoice/list';
15
+ import Layout from '../../../components/layout';
16
+ import SectionHeader from '../../../components/section/header';
17
+ import SubscriptionItemList from '../../../components/subscription/items';
18
+ import SubscriptionStatus from '../../../components/subscription/status';
19
+ import api from '../../../libs/api';
20
+ import { formatSubscriptionProduct, formatTime } from '../../../libs/util';
21
+
22
+ const fetchData = (id: string | undefined): Promise<TSubscriptionExpanded> => {
23
+ return api.get(`/api/subscriptions/${id}`).then((res) => res.data);
24
+ };
25
+
26
+ export default function CustomerSubscription() {
27
+ const { id } = useParams() as { id: string };
28
+ const { t } = useLocaleContext();
29
+ const { loading, error, data } = useRequest(() => fetchData(id));
30
+ const navigate = useNavigate();
31
+
32
+ if (error) {
33
+ return <Alert severity="error">{error.message}</Alert>;
34
+ }
35
+
36
+ if (loading || !data) {
37
+ return <CircularProgress />;
38
+ }
39
+
40
+ return (
41
+ <Layout>
42
+ <Root direction="column" spacing={4} sx={{ mb: 4 }}>
43
+ <Box>
44
+ <Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
45
+ <Link to="/customer">
46
+ <Stack direction="row" alignItems="center" sx={{ fontWeight: 'normal', mt: '16px' }}>
47
+ <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
48
+ <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
49
+ {t('customer.subscriptions.title')}
50
+ </Typography>
51
+ </Stack>
52
+ </Link>
53
+ </Stack>
54
+ <Box mt={2}>
55
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
56
+ <Stack direction="row" alignItems="center">
57
+ <Typography variant="h5" sx={{ fontWeight: 600 }}>
58
+ {formatSubscriptionProduct(data.items)}
59
+ </Typography>
60
+ <SubscriptionStatus subscription={data} sx={{ ml: 1 }} />
61
+ </Stack>
62
+ </Stack>
63
+ <Stack
64
+ className="section-body"
65
+ direction="row"
66
+ spacing={3}
67
+ justifyContent="flex-start"
68
+ flexWrap="wrap"
69
+ sx={{ pt: 2, mt: 2, borderTop: '1px solid #eee' }}>
70
+ <InfoMetric
71
+ label={t('admin.subscription.startedAt')}
72
+ value={formatTime(data.start_date ? data.start_date * 1000 : data.created_at)}
73
+ divider
74
+ />
75
+ {!data.cancel_at && (
76
+ <InfoMetric
77
+ label={t('admin.subscription.nextInvoice')}
78
+ value={formatTime(data.current_period_end * 1000)}
79
+ divider
80
+ />
81
+ )}
82
+ {data.cancel_at && (
83
+ <InfoMetric
84
+ label={t('admin.subscription.cancel.schedule')}
85
+ value={formatTime(data.cancel_at * 1000)}
86
+ divider
87
+ />
88
+ )}
89
+ </Stack>
90
+ </Box>
91
+ </Box>
92
+
93
+ <Box className="section">
94
+ <SectionHeader title={t('admin.details')} />
95
+ <Stack>
96
+ <InfoRow
97
+ label={t('common.customer')}
98
+ value={<Link to={`/admin/customers/${data.customer.id}`}>{data.customer.name}</Link>}
99
+ />
100
+ <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
101
+ {data.status === 'paused' && !!data.pause_collection?.resumes_at && (
102
+ <InfoRow label={t('common.resumesAt')} value={formatTime(data.pause_collection.resumes_at * 1000)} />
103
+ )}
104
+ <InfoRow
105
+ label={t('admin.subscription.currentPeriod')}
106
+ value={[formatTime(data.current_period_start * 1000), formatTime(data.current_period_end * 1000)].join(
107
+ ' ~ '
108
+ )}
109
+ />
110
+ <InfoRow
111
+ label={t('admin.subscription.trialingPeriod')}
112
+ value={
113
+ data.trail_end && data.trail_start
114
+ ? [formatTime(data.trail_start * 1000), formatTime(data.trail_end * 1000)].join(' ~ ')
115
+ : ''
116
+ }
117
+ />
118
+ <InfoRow label={t('admin.subscription.discount')} value={data.discount_id ? data.discount_id : ''} />
119
+ <InfoRow label={t('admin.subscription.collectionMethod')} value={data.collection_method} />
120
+ <InfoRow
121
+ label={t('admin.paymentMethod._name')}
122
+ value={<Currency logo={data.paymentMethod.logo} name={data.paymentMethod.name} />}
123
+ />
124
+ <InfoRow
125
+ label={t('admin.paymentCurrency.name')}
126
+ value={<Currency logo={data.paymentCurrency.logo} name={data.paymentCurrency.symbol} />}
127
+ />
128
+ {data.payment_details?.arcblock?.tx_hash && (
129
+ <InfoRow
130
+ label={t('common.txHash')}
131
+ value={<TxLink details={data.payment_details} method={data.paymentMethod} />}
132
+ />
133
+ )}
134
+ </Stack>
135
+ </Box>
136
+
137
+ <Box className="section">
138
+ <SectionHeader title={t('admin.product.pricing')} mb={0} />
139
+ <Box className="section-body">
140
+ <SubscriptionItemList data={data.items} currency={data.paymentCurrency} mode="customer" />
141
+ </Box>
142
+ </Box>
143
+ <Box className="section">
144
+ <SectionHeader title={t('admin.invoices')} mb={0} />
145
+ <Box className="section-body">
146
+ <InvoiceList
147
+ features={{ customer: false, toolbar: false }}
148
+ subscription_id={data.id}
149
+ invoiceProps={{
150
+ onClick: (invoice) => {
151
+ navigate(`/customer/invoice/${invoice.id}`);
152
+ },
153
+ }}
154
+ mode="customer"
155
+ />
156
+ </Box>
157
+ </Box>
158
+ </Root>
159
+ </Layout>
160
+ );
161
+ }
162
+
163
+ const Root = styled(Stack)``;
package/tsconfig.api.json CHANGED
@@ -3,7 +3,12 @@
3
3
  "compilerOptions": {
4
4
  "outDir": "api/dist",
5
5
  "noEmit": false,
6
- "noEmitOnError": true
6
+ "noEmitOnError": true,
7
+ "typeRoots": [
8
+ "./node_modules/@types",
9
+ "env.d.ts",
10
+ "pretty-ms-i18n.d.ts"
11
+ ],
7
12
  },
8
13
  "include": ["api/*.d.ts", "api/src"]
9
14
  }