payment-kit 1.21.13 → 1.21.14

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 (55) hide show
  1. package/api/src/crons/payment-stat.ts +31 -23
  2. package/api/src/libs/invoice.ts +29 -4
  3. package/api/src/libs/product.ts +28 -4
  4. package/api/src/routes/checkout-sessions.ts +46 -1
  5. package/api/src/routes/index.ts +2 -0
  6. package/api/src/routes/invoices.ts +63 -2
  7. package/api/src/routes/payment-stats.ts +244 -22
  8. package/api/src/routes/products.ts +3 -0
  9. package/api/src/routes/tax-rates.ts +220 -0
  10. package/api/src/store/migrations/20251001-add-tax-code-to-products.ts +20 -0
  11. package/api/src/store/migrations/20251001-create-tax-rates.ts +17 -0
  12. package/api/src/store/migrations/20251007-relate-tax-rate-to-invoice.ts +24 -0
  13. package/api/src/store/migrations/20251009-add-tax-behavior.ts +21 -0
  14. package/api/src/store/models/index.ts +3 -0
  15. package/api/src/store/models/invoice-item.ts +10 -0
  16. package/api/src/store/models/price.ts +7 -0
  17. package/api/src/store/models/product.ts +7 -0
  18. package/api/src/store/models/tax-rate.ts +352 -0
  19. package/api/tests/models/tax-rate.spec.ts +777 -0
  20. package/blocklet.yml +2 -2
  21. package/package.json +6 -6
  22. package/public/currencies/dollar.png +0 -0
  23. package/src/components/collapse.tsx +3 -2
  24. package/src/components/drawer-form.tsx +2 -1
  25. package/src/components/invoice/list.tsx +38 -1
  26. package/src/components/invoice/table.tsx +48 -2
  27. package/src/components/metadata/form.tsx +2 -2
  28. package/src/components/payment-intent/list.tsx +19 -1
  29. package/src/components/payouts/list.tsx +19 -1
  30. package/src/components/price/currency-select.tsx +105 -48
  31. package/src/components/price/form.tsx +3 -1
  32. package/src/components/product/form.tsx +79 -5
  33. package/src/components/refund/list.tsx +20 -1
  34. package/src/components/subscription/items/actions.tsx +25 -15
  35. package/src/components/subscription/list.tsx +16 -1
  36. package/src/components/tax/actions.tsx +140 -0
  37. package/src/components/tax/filter-toolbar.tsx +230 -0
  38. package/src/components/tax/tax-code-select.tsx +633 -0
  39. package/src/components/tax/tax-rate-form.tsx +177 -0
  40. package/src/components/tax/tax-utils.ts +38 -0
  41. package/src/components/tax/taxCodes.json +10882 -0
  42. package/src/components/uploader.tsx +3 -0
  43. package/src/locales/en.tsx +152 -0
  44. package/src/locales/zh.tsx +149 -0
  45. package/src/pages/admin/billing/invoices/detail.tsx +1 -1
  46. package/src/pages/admin/index.tsx +2 -0
  47. package/src/pages/admin/overview.tsx +1114 -322
  48. package/src/pages/admin/products/vendors/index.tsx +4 -2
  49. package/src/pages/admin/tax/create.tsx +104 -0
  50. package/src/pages/admin/tax/detail.tsx +476 -0
  51. package/src/pages/admin/tax/edit.tsx +126 -0
  52. package/src/pages/admin/tax/index.tsx +86 -0
  53. package/src/pages/admin/tax/list.tsx +334 -0
  54. package/src/pages/customer/subscription/change-payment.tsx +1 -1
  55. package/src/pages/home.tsx +6 -3
@@ -0,0 +1,126 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import { api, formatError } from '@blocklet/payment-react';
4
+ import { CircularProgress, Stack, Button } from '@mui/material';
5
+ import { FormProvider, useForm } from 'react-hook-form';
6
+ import { useNavigate } from 'react-router-dom';
7
+ import { dispatch } from 'use-bus';
8
+
9
+ import { EditOutlined } from '@mui/icons-material';
10
+ import DrawerForm from '../../../components/drawer-form';
11
+ import TaxRateForm, { TaxRateFormValues, generateTaxRateDisplayName } from '../../../components/tax/tax-rate-form';
12
+
13
+ type FormData = TaxRateFormValues & {
14
+ metadata: Array<{ key: string; value: string }>;
15
+ };
16
+
17
+ type TaxRateData = {
18
+ id: string;
19
+ display_name: string;
20
+ description?: string;
21
+ country: string;
22
+ state?: string;
23
+ postal_code?: string;
24
+ tax_code?: string;
25
+ percentage: number;
26
+ metadata?: Record<string, any>;
27
+ };
28
+
29
+ export default function TaxRateEdit({
30
+ id,
31
+ initialData,
32
+ onClose = undefined,
33
+ onChange = () => {},
34
+ }: {
35
+ id: string;
36
+ initialData: TaxRateData;
37
+ onClose?: () => void;
38
+ onChange?: () => void;
39
+ }) {
40
+ const { t, locale } = useLocaleContext();
41
+ const navigate = useNavigate();
42
+
43
+ const methods = useForm<FormData>({
44
+ mode: 'onChange',
45
+ defaultValues: {
46
+ display_name: initialData.display_name,
47
+ description: initialData.description || '',
48
+ country: (initialData.country || '').toLowerCase(),
49
+ state: initialData.state || '',
50
+ postal_code: initialData.postal_code || '',
51
+ tax_code: initialData.tax_code || '',
52
+ percentage: initialData.percentage,
53
+ metadata: initialData.metadata || {},
54
+ },
55
+ });
56
+
57
+ const handleClose = () => {
58
+ if (onClose) {
59
+ onClose();
60
+ } else {
61
+ navigate(`/admin/tax/${id}`);
62
+ }
63
+ };
64
+
65
+ const onSubmit = async (values: FormData) => {
66
+ try {
67
+ const displayName =
68
+ values.display_name ||
69
+ generateTaxRateDisplayName(
70
+ values.country,
71
+ values.state,
72
+ values.postal_code,
73
+ Number(values.percentage),
74
+ values.tax_code,
75
+ locale
76
+ );
77
+
78
+ await api.put(`/api/tax-rates/${id}`, {
79
+ display_name: displayName,
80
+ description: values.description,
81
+ country: values.country,
82
+ state: values.state || null,
83
+ postal_code: values.postal_code || null,
84
+ tax_code: values.tax_code || null,
85
+ percentage: Number(values.percentage),
86
+ metadata: values.metadata,
87
+ });
88
+ Toast.success(t('admin.taxRate.updated'));
89
+ dispatch('tax-rate.updated');
90
+ onChange();
91
+ handleClose();
92
+ } catch (err) {
93
+ console.error(err);
94
+ Toast.error(formatError(err));
95
+ }
96
+ };
97
+
98
+ return (
99
+ <DrawerForm
100
+ icon={<EditOutlined />}
101
+ open
102
+ onClose={handleClose}
103
+ text={t('admin.taxRate.editTitle')}
104
+ hideLiveMode
105
+ width={720}
106
+ addons={
107
+ <Stack direction="row" spacing={1}>
108
+ <Button variant="text" color="inherit" onClick={handleClose}>
109
+ {t('common.cancel')}
110
+ </Button>
111
+ <Button
112
+ variant="contained"
113
+ color="primary"
114
+ onClick={methods.handleSubmit(onSubmit)}
115
+ disabled={methods.formState.isSubmitting}>
116
+ {methods.formState.isSubmitting && <CircularProgress size={16} sx={{ mr: 1 }} />}
117
+ {t('common.save')}
118
+ </Button>
119
+ </Stack>
120
+ }>
121
+ <FormProvider {...methods}>
122
+ <TaxRateForm isEdit />
123
+ </FormProvider>
124
+ </DrawerForm>
125
+ );
126
+ }
@@ -0,0 +1,86 @@
1
+ import Tabs from '@arcblock/ux/lib/Tabs';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { Box, Stack } from '@mui/material';
4
+ import React, { isValidElement } from 'react';
5
+ import { useNavigate, useParams } from 'react-router-dom';
6
+
7
+ import { useTransitionContext } from '../../../components/progress-bar';
8
+ import TaxRateCreate from './create';
9
+
10
+ const TaxRatesList = React.lazy(() => import('./list'));
11
+ const TaxRateDetail = React.lazy(() => import('./detail'));
12
+
13
+ const pages = {
14
+ rates: TaxRatesList,
15
+ };
16
+
17
+ export default function TaxIndex() {
18
+ const navigate = useNavigate();
19
+ const { t } = useLocaleContext();
20
+ const { page = 'rates' } = useParams();
21
+ const { startTransition } = useTransitionContext();
22
+
23
+ if (page.startsWith('txr_')) {
24
+ return <TaxRateDetail id={page} />;
25
+ }
26
+
27
+ // @ts-ignore
28
+ const TabComponent = pages[page] || pages.rates;
29
+
30
+ const tabs = [{ label: t('admin.taxRates'), value: 'rates' }];
31
+
32
+ const onTabChange = (newTab: string) => {
33
+ startTransition(() => {
34
+ navigate(`/admin/tax/${newTab}`);
35
+ });
36
+ };
37
+
38
+ return (
39
+ <>
40
+ <Stack
41
+ direction="row"
42
+ spacing={1}
43
+ sx={{
44
+ alignItems: 'flex-start',
45
+ justifyContent: 'end',
46
+ flexWrap: 'wrap',
47
+ }}>
48
+ {/* @ts-ignore */}
49
+ <Tabs
50
+ // @ts-ignore
51
+ tabs={tabs}
52
+ // @ts-ignore
53
+ current={page}
54
+ // @ts-ignore
55
+ onChange={onTabChange}
56
+ scrollButtons="auto"
57
+ variant="scrollable"
58
+ sx={{
59
+ flex: '1 0 auto',
60
+ maxWidth: '100%',
61
+ '.MuiTab-root': {
62
+ color: 'text.lighter',
63
+ },
64
+ '.MuiTabs-indicator': {
65
+ display: 'none',
66
+ },
67
+ '.Mui-selected': {
68
+ fontSize: 24,
69
+ color: 'text.primary',
70
+ },
71
+ '.MuiTabs-hideScrollbar': {
72
+ border: 'none !important',
73
+ },
74
+ '.MuiTouchRipple-root': {
75
+ display: 'none',
76
+ },
77
+ }}
78
+ />
79
+ <Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
80
+ <TaxRateCreate />
81
+ </Box>
82
+ </Stack>
83
+ {isValidElement(TabComponent) ? TabComponent : <TabComponent />}
84
+ </>
85
+ );
86
+ }
@@ -0,0 +1,334 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { getDurableData } from '@arcblock/ux/lib/Datatable';
3
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
+ import { Status, Table, api, formatTime } from '@blocklet/payment-react';
5
+ import { Box, Button, Tooltip, Typography } from '@mui/material';
6
+ import { useLocalStorageState, useRequest, useUpdateEffect } from 'ahooks';
7
+ import { useMemo } from 'react';
8
+ import { useNavigate } from 'react-router-dom';
9
+ import useBus from 'use-bus';
10
+ import { FlagEmoji } from 'react-international-phone';
11
+
12
+ import Copyable from '../../../components/copyable';
13
+ import TaxRateActions from '../../../components/tax/actions';
14
+ import TaxFilterToolbar from '../../../components/tax/filter-toolbar';
15
+ import { getTaxCodeInfo, getCountryInfo } from '../../../components/tax/tax-utils';
16
+
17
+ type SearchProps = {
18
+ page: number;
19
+ pageSize: number;
20
+ active?: string;
21
+ country?: string;
22
+ q?: string;
23
+ };
24
+
25
+ const fetchTaxRates = (params: Record<string, any> = {}): Promise<{ list: any[]; count: number }> => {
26
+ const search = new URLSearchParams();
27
+ Object.keys(params).forEach((key) => {
28
+ const value = params[key];
29
+ if (value === undefined || value === null || value === '') {
30
+ return;
31
+ }
32
+ if (key === 'q' && value) {
33
+ search.set('q', value);
34
+ return;
35
+ }
36
+ search.set(key, String(value));
37
+ });
38
+
39
+ return api.get(`/api/tax-rates?${search.toString()}`).then((res) => res.data);
40
+ };
41
+
42
+ export default function TaxRatesList() {
43
+ const listKey = 'tax-rates';
44
+ const persisted = getDurableData(listKey);
45
+ const { t } = useLocaleContext();
46
+ const navigate = useNavigate();
47
+
48
+ const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
49
+ defaultValue: {
50
+ page: persisted.page ? persisted.page + 1 : 1,
51
+ pageSize: persisted.rowsPerPage || 20,
52
+ active: '',
53
+ country: '',
54
+ q: '',
55
+ },
56
+ });
57
+
58
+ const query = useMemo(() => {
59
+ const params: Record<string, any> = {
60
+ page: search?.page,
61
+ pageSize: search?.pageSize,
62
+ sort: '-createdAt', // 按创建时间倒序
63
+ };
64
+
65
+ if (search?.active) {
66
+ params.active = search.active === 'active';
67
+ }
68
+ if (search?.country) {
69
+ params.country = search.country;
70
+ }
71
+ if (search?.q) {
72
+ const term = encodeURIComponent(search.q);
73
+ const parts = [
74
+ `like-display_name:${term}`,
75
+ `like-description:${term}`,
76
+ `like-state:${term}`,
77
+ `like-postal_code:${term}`,
78
+ `like-tax_code:${term}`,
79
+ `like-country:${term}`,
80
+ `like-percentage:${term}`,
81
+ ];
82
+ params.q = parts.join(' ');
83
+ }
84
+
85
+ return params;
86
+ }, [search]);
87
+
88
+ const {
89
+ data = {
90
+ list: [],
91
+ count: 0,
92
+ },
93
+ refresh,
94
+ loading,
95
+ } = useRequest(() => fetchTaxRates(query), {
96
+ refreshDeps: [query],
97
+ });
98
+
99
+ useBus('tax-rate.created', () => refresh(), []);
100
+ useBus('tax-rate.updated', () => refresh(), []);
101
+ useBus('tax-rate.deleted', () => refresh(), []);
102
+
103
+ useUpdateEffect(() => {
104
+ refresh();
105
+ }, [search?.page, search?.pageSize]);
106
+
107
+ const taxRates = data.list || [];
108
+ const { locale } = useLocaleContext();
109
+
110
+ const columns = [
111
+ {
112
+ label: t('common.id'),
113
+ name: 'id',
114
+ options: {
115
+ filter: false,
116
+ customBodyRenderLite: (_: string, index: number) => {
117
+ const item = taxRates[index];
118
+ return <Copyable text={item.id} style={{ fontSize: '0.875rem' }} />;
119
+ },
120
+ },
121
+ },
122
+ {
123
+ label: t('admin.taxRate.displayName'),
124
+ name: 'display_name',
125
+ options: {
126
+ filter: false,
127
+ customBodyRenderLite: (_: string, index: number) => {
128
+ const item = taxRates[index];
129
+ return (
130
+ <Button
131
+ variant="text"
132
+ sx={{ p: 0, minWidth: 'unset', textTransform: 'none', textAlign: 'left' }}
133
+ onClick={() => navigate(`/admin/tax/${item.id}`)}>
134
+ {item.display_name}
135
+ </Button>
136
+ );
137
+ },
138
+ },
139
+ },
140
+ {
141
+ label: t('admin.taxRate.location'),
142
+ name: 'location',
143
+ options: {
144
+ filter: false,
145
+ customBodyRenderLite: (_: string, index: number) => {
146
+ const item = taxRates[index];
147
+ if (!item.country) {
148
+ return '—';
149
+ }
150
+
151
+ const countryInfo = getCountryInfo(item.country);
152
+ const parts = [];
153
+ if (item.state) {
154
+ parts.push(item.state);
155
+ }
156
+ if (item.postal_code) {
157
+ parts.push(item.postal_code);
158
+ }
159
+
160
+ return (
161
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
162
+ <FlagEmoji iso2={item.country.toLowerCase()} style={{ width: 24 }} />
163
+ <Typography variant="body2">
164
+ {countryInfo?.name}
165
+ {parts.length > 0 && ` · ${parts.join(' · ')}`}
166
+ </Typography>
167
+ </Box>
168
+ );
169
+ },
170
+ },
171
+ },
172
+ {
173
+ label: t('admin.taxRate.percentage'),
174
+ name: 'percentage',
175
+ options: {
176
+ filter: false,
177
+ customBodyRenderLite: (_: string, index: number) => {
178
+ const item = taxRates[index];
179
+ return `${item.percentage}%`;
180
+ },
181
+ },
182
+ },
183
+ {
184
+ label: t('admin.taxRate.taxCode'),
185
+ name: 'tax_code',
186
+ options: {
187
+ filter: false,
188
+ customBodyRenderLite: (_: string, index: number) => {
189
+ const item = taxRates[index];
190
+ if (!item.tax_code) {
191
+ return '—';
192
+ }
193
+
194
+ const taxCodeInfo = getTaxCodeInfo(item.tax_code, locale);
195
+ if (!taxCodeInfo) {
196
+ return item.tax_code;
197
+ }
198
+
199
+ return (
200
+ <Tooltip title={taxCodeInfo.description} arrow>
201
+ <Box>
202
+ <Typography variant="body2" sx={{ fontWeight: 500 }}>
203
+ {taxCodeInfo.name}
204
+ </Typography>
205
+ <Typography
206
+ variant="caption"
207
+ sx={{
208
+ color: 'text.secondary',
209
+ }}>
210
+ {taxCodeInfo.id}
211
+ </Typography>
212
+ </Box>
213
+ </Tooltip>
214
+ );
215
+ },
216
+ },
217
+ },
218
+ {
219
+ label: t('admin.taxRate.relatedInvoiceNum'),
220
+ name: 'invoice_count',
221
+ options: {
222
+ filter: false,
223
+ customBodyRenderLite: (_: string, index: number) => {
224
+ const item = taxRates[index];
225
+ const count = Number(item.invoice_count) || 0;
226
+
227
+ return (
228
+ <Typography
229
+ variant="body2"
230
+ sx={{
231
+ color: count > 0 ? 'text.primary' : 'text.secondary',
232
+ }}>
233
+ {count > 0 ? count : t('admin.taxRate.notUsed')}
234
+ </Typography>
235
+ );
236
+ },
237
+ },
238
+ },
239
+ {
240
+ label: t('common.status'),
241
+ name: 'active',
242
+ options: {
243
+ filter: false,
244
+ customBodyRenderLite: (_: string, index: number) => {
245
+ const item = taxRates[index];
246
+ return (
247
+ <Status
248
+ label={item.active ? t('common.active') : t('common.inactive')}
249
+ color={item.active ? 'success' : 'default'}
250
+ />
251
+ );
252
+ },
253
+ },
254
+ },
255
+ {
256
+ label: t('common.createdAt'),
257
+ name: 'created_at',
258
+ options: {
259
+ filter: false,
260
+ customBodyRenderLite: (_: string, index: number) => {
261
+ const item = taxRates[index];
262
+ return formatTime(item.created_at);
263
+ },
264
+ },
265
+ },
266
+ {
267
+ label: t('common.actions'),
268
+ name: 'actions',
269
+ width: 80,
270
+ options: {
271
+ sort: false,
272
+ filter: false,
273
+ customBodyRenderLite: (_: string, index: number) => {
274
+ const item = taxRates[index];
275
+ return <TaxRateActions data={item} onChange={() => refresh()} />;
276
+ },
277
+ },
278
+ },
279
+ ];
280
+
281
+ const handleTableChange = ({ page, rowsPerPage }: any) => {
282
+ if (search!.pageSize !== rowsPerPage) {
283
+ setSearch((prev) => ({ ...(prev || {}), page: 1, pageSize: rowsPerPage }));
284
+ } else if (search!.page !== page + 1) {
285
+ // @ts-ignore
286
+ setSearch((prev) => ({ ...(prev || {}), page: page + 1 }));
287
+ }
288
+ };
289
+
290
+ const handleSearchChange = (value: string) => {
291
+ // @ts-ignore
292
+ setSearch((prev) => ({
293
+ ...(prev || {}),
294
+ q: value,
295
+ page: 1,
296
+ }));
297
+ };
298
+
299
+ const handleFilterChange = (key: keyof SearchProps, value: string) => {
300
+ // @ts-ignore
301
+ setSearch((prev) => ({
302
+ ...(prev || {}),
303
+ [key]: value,
304
+ page: 1,
305
+ }));
306
+ };
307
+
308
+ const title = (
309
+ <TaxFilterToolbar
310
+ search={search}
311
+ onFilterChange={(key, value) => handleFilterChange(key as keyof SearchProps, value)}
312
+ />
313
+ );
314
+
315
+ return (
316
+ <Table
317
+ hasRowLink={false}
318
+ durable={`__${listKey}__`}
319
+ durableKeys={['page', 'rowsPerPage', 'searchText']}
320
+ title={title}
321
+ data={taxRates}
322
+ columns={columns}
323
+ loading={loading}
324
+ options={{
325
+ count: data.count,
326
+ page: (search?.page || 1) - 1,
327
+ rowsPerPage: search?.pageSize || 20,
328
+ onSearchChange: handleSearchChange,
329
+ }}
330
+ onChange={handleTableChange}
331
+ mobileTDFlexDirection="row"
332
+ />
333
+ );
334
+ }
@@ -276,7 +276,7 @@ function CustomerSubscriptionChangePayment({ subscription, customer, onComplete
276
276
  />
277
277
  {subscription.paymentMethod.type !== 'stripe' && (
278
278
  <Stack direction="column" className="cko-payment-form" spacing={0}>
279
- <AddressForm mode="auto" stripe={method?.type === 'stripe'} />
279
+ <AddressForm mode="auto" />
280
280
  </Stack>
281
281
  )}
282
282
  </Stack>
@@ -3,7 +3,10 @@ import { Avatar, Button, Stack, Typography } from '@mui/material';
3
3
  import { Link } from 'react-router-dom';
4
4
  import Typewriter from 'typewriter-effect';
5
5
 
6
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
7
+
6
8
  function Home() {
9
+ const { t } = useLocaleContext();
7
10
  return (
8
11
  <div>
9
12
  <Header
@@ -50,7 +53,7 @@ function Home() {
50
53
  }}>
51
54
  <Typewriter
52
55
  options={{
53
- strings: ['The decentralized stripe alike payment solution for blocklets'],
56
+ strings: [t('common.homeTagline')],
54
57
  autoStart: true,
55
58
  loop: true,
56
59
  }}
@@ -59,7 +62,7 @@ function Home() {
59
62
  </Stack>
60
63
  <Stack direction="row" spacing={3}>
61
64
  <Button variant="outlined" color="secondary" size="large" component={Link} to="/integrations">
62
- Admin Dashboard
65
+ {t('common.adminDashboard')}
63
66
  </Button>
64
67
  <Button
65
68
  variant="outlined"
@@ -68,7 +71,7 @@ function Home() {
68
71
  component={Link}
69
72
  to="/customer"
70
73
  sx={{ background: 'transparent' }}>
71
- Customer Portal
74
+ {t('common.customerPortal')}
72
75
  </Button>
73
76
  </Stack>
74
77
  </Stack>