payment-kit 1.20.20 → 1.20.22

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 (37) hide show
  1. package/api/src/integrations/stripe/resource.ts +1 -1
  2. package/api/src/libs/discount/coupon.ts +41 -73
  3. package/api/src/libs/invoice.ts +17 -0
  4. package/api/src/libs/notification/template/subscription-renew-failed.ts +22 -1
  5. package/api/src/libs/notification/template/subscription-will-renew.ts +22 -0
  6. package/api/src/locales/en.ts +1 -0
  7. package/api/src/locales/zh.ts +1 -0
  8. package/api/src/queues/checkout-session.ts +2 -2
  9. package/api/src/queues/vendors/fulfillment-coordinator.ts +11 -2
  10. package/api/src/queues/vendors/status-check.ts +1 -3
  11. package/api/src/routes/checkout-sessions.ts +84 -0
  12. package/api/src/routes/connect/collect-batch.ts +2 -2
  13. package/api/src/routes/connect/pay.ts +1 -1
  14. package/api/src/routes/vendor.ts +93 -47
  15. package/api/src/store/migrations/20250926-change-customer-did-unique.ts +49 -0
  16. package/api/src/store/models/checkout-session.ts +1 -0
  17. package/api/src/store/models/customer.ts +1 -0
  18. package/api/tests/libs/coupon.spec.ts +219 -0
  19. package/api/tests/libs/discount.spec.ts +250 -0
  20. package/blocklet.yml +1 -1
  21. package/package.json +7 -7
  22. package/src/components/discount/discount-info.tsx +0 -1
  23. package/src/components/invoice/action.tsx +26 -0
  24. package/src/components/invoice/table.tsx +2 -9
  25. package/src/components/invoice-pdf/styles.ts +2 -0
  26. package/src/components/invoice-pdf/template.tsx +44 -12
  27. package/src/components/metadata/list.tsx +1 -0
  28. package/src/components/subscription/metrics.tsx +7 -3
  29. package/src/components/subscription/vendor-service-list.tsx +56 -58
  30. package/src/locales/en.tsx +9 -2
  31. package/src/locales/zh.tsx +9 -2
  32. package/src/pages/admin/billing/subscriptions/detail.tsx +43 -4
  33. package/src/pages/admin/products/coupons/applicable-products.tsx +20 -37
  34. package/src/pages/admin/products/products/detail.tsx +4 -14
  35. package/src/pages/admin/products/vendors/index.tsx +57 -48
  36. package/src/pages/customer/invoice/detail.tsx +1 -1
  37. package/src/pages/customer/subscription/detail.tsx +17 -4
@@ -13,9 +13,16 @@ interface VendorConfig {
13
13
  interface VendorServiceListProps {
14
14
  vendorServices: VendorConfig[];
15
15
  subscriptionId: string;
16
+ isOwner?: boolean;
17
+ isCanceled: boolean;
16
18
  }
17
19
 
18
- export default function VendorServiceList({ vendorServices, subscriptionId }: VendorServiceListProps) {
20
+ export default function VendorServiceList({
21
+ vendorServices,
22
+ subscriptionId,
23
+ isOwner = true,
24
+ isCanceled,
25
+ }: VendorServiceListProps) {
19
26
  const { t } = useLocaleContext();
20
27
 
21
28
  if (!vendorServices || vendorServices.length === 0) {
@@ -31,7 +38,6 @@ export default function VendorServiceList({ vendorServices, subscriptionId }: Ve
31
38
  </Typography>
32
39
  <Box className="section-body">
33
40
  <Stack
34
- spacing={2}
35
41
  sx={{
36
42
  display: 'grid',
37
43
  gridTemplateColumns: { xs: '1fr', md: '1fr 1fr', lg: '1fr 1fr 1fr' },
@@ -39,12 +45,14 @@ export default function VendorServiceList({ vendorServices, subscriptionId }: Ve
39
45
  }}>
40
46
  {vendorServices.map((vendor, index) => {
41
47
  const isLauncher = vendor.vendor_type === 'launcher';
42
-
43
48
  return (
44
49
  <Box
45
50
  key={vendor.vendor_key || index}
51
+ className="vendor-service-item"
46
52
  sx={{
47
53
  p: 2,
54
+ display: 'flex',
55
+ alignItems: 'center',
48
56
  border: '1px solid',
49
57
  borderColor: 'divider',
50
58
  borderRadius: 2,
@@ -54,40 +62,28 @@ export default function VendorServiceList({ vendorServices, subscriptionId }: Ve
54
62
  },
55
63
  transition: 'background-color 0.2s ease',
56
64
  }}>
57
- <Stack
58
- direction="row"
59
- sx={{
60
- justifyContent: 'space-between',
61
- alignItems: 'flex-start',
62
- }}>
63
- <Stack
64
- direction="row"
65
- spacing={1}
65
+ <Stack direction="row" spacing={1} sx={{ alignItems: 'center', flex: 1 }}>
66
+ <Box
67
+ sx={{
68
+ width: 8,
69
+ height: 8,
70
+ borderRadius: '50%',
71
+ bgcolor: isCanceled ? 'error.main' : 'success.main',
72
+ flexShrink: 0,
73
+ }}
74
+ />
75
+ <Typography
76
+ variant="body1"
66
77
  sx={{
67
- alignItems: 'center',
68
- flex: 1,
78
+ fontWeight: 600,
79
+ fontSize: '1rem',
80
+ color: 'text.primary',
69
81
  }}>
70
- <Box
71
- sx={{
72
- width: 8,
73
- height: 8,
74
- borderRadius: '50%',
75
- bgcolor: 'success.main',
76
- flexShrink: 0,
77
- }}
78
- />
79
- <Typography
80
- variant="body1"
81
- sx={{
82
- fontWeight: 600,
83
- fontSize: '1rem',
84
- color: 'text.primary',
85
- }}>
86
- {vendor.name || vendor.vendor_key}
87
- </Typography>
88
- </Stack>
89
- {/* Launcher 类型的链接 */}
90
- {isLauncher && (
82
+ {vendor.name || vendor.vendor_key}
83
+ </Typography>
84
+ </Stack>
85
+ {isLauncher && (
86
+ <Box>
91
87
  <Stack direction="row" spacing={0.5}>
92
88
  <Tooltip title={t('admin.subscription.serviceHome')} placement="top">
93
89
  <IconButton
@@ -105,30 +101,32 @@ export default function VendorServiceList({ vendorServices, subscriptionId }: Ve
105
101
  <Home fontSize="small" />
106
102
  </IconButton>
107
103
  </Tooltip>
108
- <Tooltip title={t('admin.subscription.serviceDashboard')} placement="top">
109
- <IconButton
110
- size="small"
111
- component="a"
112
- href={joinURL(
113
- prefix,
114
- '/api/vendors/open/',
115
- subscriptionId,
116
- `?vendorId=${vendor.vendor_id}&target=dashboard`
117
- )}
118
- target="_blank"
119
- rel="noopener noreferrer"
120
- sx={{
121
- color: 'primary.main',
122
- '&:hover': {
123
- backgroundColor: 'primary.lighter',
124
- },
125
- }}>
126
- <Dashboard fontSize="small" />
127
- </IconButton>
128
- </Tooltip>
104
+ {isOwner && (
105
+ <Tooltip title={t('admin.subscription.serviceDashboard')} placement="top">
106
+ <IconButton
107
+ size="small"
108
+ component="a"
109
+ href={joinURL(
110
+ prefix,
111
+ '/api/vendors/open/',
112
+ subscriptionId,
113
+ `?vendorId=${vendor.vendor_id}&target=dashboard`
114
+ )}
115
+ target="_blank"
116
+ rel="noopener noreferrer"
117
+ sx={{
118
+ color: 'primary.main',
119
+ '&:hover': {
120
+ backgroundColor: 'primary.lighter',
121
+ },
122
+ }}>
123
+ <Dashboard fontSize="small" />
124
+ </IconButton>
125
+ </Tooltip>
126
+ )}
129
127
  </Stack>
130
- )}
131
- </Stack>
128
+ </Box>
129
+ )}
132
130
  </Box>
133
131
  );
134
132
  })}
@@ -132,6 +132,7 @@ export default flat({
132
132
  paymentMethods: 'Payment methods',
133
133
  customers: 'Customers',
134
134
  products: 'Products',
135
+ invoiceItems: 'Invoice Items',
135
136
  pricing: 'Pricing',
136
137
  coupons: 'Coupons',
137
138
  pricingTables: 'Pricing tables',
@@ -1086,6 +1087,11 @@ export default flat({
1086
1087
  download: 'Download PDF',
1087
1088
  edit: 'Edit Invoice',
1088
1089
  duplicate: 'Duplicate Invoice',
1090
+ retryUncollectible: {
1091
+ title: 'Retry collection',
1092
+ tip: 'Are you sure you want to retry collecting this invoice? This will attempt to charge the customer again.',
1093
+ success: 'Retry request submitted',
1094
+ },
1089
1095
  returnStake: {
1090
1096
  title: 'Return Stake',
1091
1097
  tip: 'Are you sure you want to return the stake? This action will return the stake to the customer immediately.',
@@ -1164,8 +1170,8 @@ export default flat({
1164
1170
  apiConfig: 'API Configuration',
1165
1171
  commissionConfig: 'Commission Configuration',
1166
1172
  status: 'Status',
1167
- servicePublicKey: 'Service Public Key',
1168
- servicePublicKeyDescription: 'Used for vendor communication authentication',
1173
+ brokerDID: 'Broker DID',
1174
+ brokerPublicKey: 'Broker Public Key',
1169
1175
  },
1170
1176
  subscription: {
1171
1177
  view: 'View subscription',
@@ -1546,6 +1552,7 @@ export default flat({
1546
1552
  subscriptions: 'No Subscriptions',
1547
1553
  customers: 'No Customers',
1548
1554
  products: 'No Products',
1555
+ invoiceItems: 'No Invoice Items',
1549
1556
  payouts: 'No Payouts',
1550
1557
  paymentLinks: 'No Payment Links',
1551
1558
  paymentMethods: 'No Payment Methods',
@@ -131,6 +131,7 @@ export default flat({
131
131
  paymentMethods: '支付方式',
132
132
  customers: '客户管理',
133
133
  products: '产品定价',
134
+ invoiceItems: '账单明细',
134
135
  coupons: '优惠券',
135
136
  pricing: '定价',
136
137
  pricingTables: '定价表',
@@ -1057,6 +1058,11 @@ export default flat({
1057
1058
  download: '下载PDF',
1058
1059
  edit: '编辑账单',
1059
1060
  duplicate: '复制账单',
1061
+ retryUncollectible: {
1062
+ title: '重新收款',
1063
+ tip: '确定要重新尝试收取该笔账单吗?系统将再次尝试向客户发起扣款。',
1064
+ success: '重新收款请求已提交',
1065
+ },
1060
1066
  attention: '未完成的账单',
1061
1067
  returnStake: {
1062
1068
  title: '退还质押',
@@ -1136,8 +1142,8 @@ export default flat({
1136
1142
  apiConfig: 'API配置',
1137
1143
  commissionConfig: '分成配置',
1138
1144
  status: '状态',
1139
- servicePublicKey: '服务公钥',
1140
- servicePublicKeyDescription: '用于供应商通信认证',
1145
+ brokerDID: '平台 DID',
1146
+ brokerPublicKey: '平台公钥',
1141
1147
  },
1142
1148
  subscription: {
1143
1149
  view: '查看订阅',
@@ -1496,6 +1502,7 @@ export default flat({
1496
1502
  image: '无图片',
1497
1503
  refunds: '没有退款记录',
1498
1504
  invoices: '没有账单',
1505
+ invoiceItems: '没有账单明细',
1499
1506
  subscriptions: '没有订阅记录',
1500
1507
  customers: '没有客户',
1501
1508
  products: '没有产品',
@@ -33,6 +33,8 @@ import SubscriptionMetrics from '../../../../components/subscription/metrics';
33
33
  import DiscountInfo from '../../../../components/discount/discount-info';
34
34
  import { goBackOrFallback } from '../../../../libs/util';
35
35
  import InfoRowGroup from '../../../../components/info-row-group';
36
+ import VendorServiceList from '../../../../components/subscription/vendor-service-list';
37
+ import { useSessionContext } from '../../../../contexts/session';
36
38
 
37
39
  const fetchData = (id: string): Promise<TSubscriptionExpanded> => {
38
40
  return api.get(`/api/subscriptions/${id}`).then((res) => res.data);
@@ -40,6 +42,7 @@ const fetchData = (id: string): Promise<TSubscriptionExpanded> => {
40
42
 
41
43
  export default function SubscriptionDetail(props: { id: string }) {
42
44
  const { t } = useLocaleContext();
45
+ const { session } = useSessionContext();
43
46
  const { isMobile } = useMobile();
44
47
  const [state, setState] = useSetState({
45
48
  adding: {
@@ -178,7 +181,7 @@ export default function SubscriptionDetail(props: { id: string }) {
178
181
  md: 3,
179
182
  },
180
183
  }}>
181
- <SubscriptionMetrics subscription={data} showBalance={false} />
184
+ <SubscriptionMetrics subscription={data} mode="admin" />
182
185
  </Stack>
183
186
  </Box>
184
187
  <Divider />
@@ -273,7 +276,6 @@ export default function SubscriptionDetail(props: { id: string }) {
273
276
  <InfoRow label={t('common.resumesAt')} value={formatTime(data.pause_collection.resumes_at * 1000)} />
274
277
  )}
275
278
  <InfoRow label={t('admin.subscription.collectionMethod')} value={data.collection_method} />
276
- <InfoRow label={t('admin.subscription.discount')} value={data.discount_id ? data.discount_id : ''} />
277
279
  <InfoRow
278
280
  label={t('admin.paymentCurrency.name')}
279
281
  value={
@@ -314,9 +316,17 @@ export default function SubscriptionDetail(props: { id: string }) {
314
316
  </InfoRowGroup>
315
317
  </Box>
316
318
  <Divider />
317
-
318
319
  {/* Discount Information */}
319
- {(data as any).discountStats && <DiscountInfo discountStats={(data as any).discountStats} />}
320
+ {(data as any).discountStats && (
321
+ <Box className="section">
322
+ <Typography variant="h3" className="section-header" sx={{ mb: 1.5 }}>
323
+ {t('admin.subscription.discount')}
324
+ </Typography>
325
+ <Box className="section-body">
326
+ <DiscountInfo discountStats={(data as any).discountStats} />
327
+ </Box>
328
+ </Box>
329
+ )}
320
330
 
321
331
  <Box className="section">
322
332
  <SectionHeader title={t('admin.product.pricing')} />
@@ -324,6 +334,35 @@ export default function SubscriptionDetail(props: { id: string }) {
324
334
  <SubscriptionItemList data={data.items} currency={data.paymentCurrency} mode="admin" />
325
335
  </Box>
326
336
  </Box>
337
+ {(() => {
338
+ const vendorServices = data.items?.map((item) => item.price?.product?.vendor_config || []).flat();
339
+ if (!vendorServices || vendorServices.length === 0) return null;
340
+ return (
341
+ <>
342
+ <Divider />
343
+ <Box
344
+ className="section"
345
+ sx={{
346
+ '.section-header': {
347
+ fontSize: {
348
+ xs: '18px',
349
+ md: '1.09375rem',
350
+ },
351
+ },
352
+ '.vendor-service-item': {
353
+ maxWidth: '400px',
354
+ },
355
+ }}>
356
+ <VendorServiceList
357
+ vendorServices={vendorServices}
358
+ subscriptionId={data.id}
359
+ isOwner={session?.user?.did === data.customer?.did}
360
+ isCanceled={data.status === 'canceled'}
361
+ />
362
+ </Box>
363
+ </>
364
+ );
365
+ })()}
327
366
  <Divider />
328
367
  {isCredit ? (
329
368
  <Box className="section">
@@ -11,7 +11,6 @@ import {
11
11
  } from '@blocklet/payment-react';
12
12
  import type { TProductExpanded } from '@blocklet/payment-types';
13
13
  import { Avatar, Stack, Typography, Box } from '@mui/material';
14
- import { styled } from '@mui/system';
15
14
  import { Link } from 'react-router-dom';
16
15
 
17
16
  interface Props {
@@ -126,41 +125,25 @@ export default function ApplicableProductsList({ products }: Props) {
126
125
  ].filter(Boolean);
127
126
 
128
127
  return (
129
- <ApplicableProductsTableRoot>
130
- <Table
131
- data={products}
132
- columns={columns}
133
- loading={false}
134
- footer={false}
135
- toolbar={false}
136
- components={{
137
- TableToolbar: () => null,
138
- TableFooter: () => null,
139
- }}
140
- mobileTDFlexDirection="row"
141
- options={{
142
- count: products.length,
143
- page: 0,
144
- rowsPerPage: 100,
145
- selectableRows: 'none',
146
- pagination: false,
147
- }}
148
- emptyNodeText={t('admin.coupon.noApplicableProducts')}
149
- />
150
- </ApplicableProductsTableRoot>
128
+ <Table
129
+ data={products}
130
+ columns={columns}
131
+ loading={false}
132
+ footer={false}
133
+ toolbar={false}
134
+ components={{
135
+ TableToolbar: () => null,
136
+ TableFooter: () => null,
137
+ }}
138
+ mobileTDFlexDirection="row"
139
+ options={{
140
+ count: products.length,
141
+ page: 0,
142
+ rowsPerPage: 100,
143
+ selectableRows: 'none',
144
+ pagination: false,
145
+ }}
146
+ emptyNodeText={t('admin.coupon.noApplicableProducts')}
147
+ />
151
148
  );
152
149
  }
153
-
154
- const ApplicableProductsTableRoot = styled(Box)`
155
- @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
156
- .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > td.MuiTableCell-root {
157
- align-items: center;
158
- padding: 4px 0;
159
- > div {
160
- width: fit-content;
161
- flex: inherit;
162
- font-size: 14px;
163
- }
164
- }
165
- }
166
- `;
@@ -389,7 +389,6 @@ export default function ProductDetail(props: { id: string }) {
389
389
  </Box>
390
390
  </Box>
391
391
  <Divider />
392
- {/* 供应商配置展示 */}
393
392
  {data.type === 'service' && (
394
393
  <>
395
394
  <Box className="section">
@@ -429,13 +428,9 @@ export default function ProductDetail(props: { id: string }) {
429
428
  borderColor: 'divider',
430
429
  borderRadius: 1,
431
430
  backgroundColor: 'background.paper',
431
+ maxWidth: '600px',
432
432
  }}>
433
- <Stack
434
- direction="row"
435
- sx={{
436
- justifyContent: 'space-between',
437
- alignItems: 'center',
438
- }}>
433
+ <Stack direction="row" sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
439
434
  <Box>
440
435
  <Typography variant="body1" sx={{ color: 'text.primary', fontWeight: 500 }}>
441
436
  {vendor.name || vendor.vendor_key || vendor.vendor_id}
@@ -446,15 +441,10 @@ export default function ProductDetail(props: { id: string }) {
446
441
  </Typography>
447
442
  )}
448
443
  </Box>
449
- <Stack
450
- direction="row"
451
- spacing={3}
452
- sx={{
453
- alignItems: 'center',
454
- }}>
444
+ <Stack direction="row" spacing={3} sx={{ alignItems: 'center' }}>
455
445
  <Typography variant="body2" sx={{ color: 'text.secondary' }}>
456
446
  {vendor.commission_type === 'percentage'
457
- ? t('admin.vendor.percentage')
447
+ ? t('admin.vendor.commission')
458
448
  : t('admin.vendor.fixedAmount')}
459
449
  </Typography>
460
450
  <Typography variant="body1" sx={{ color: 'text.primary', fontWeight: 600 }}>
@@ -185,69 +185,78 @@ export default function VendorsList() {
185
185
  setDetailOpen(true);
186
186
  };
187
187
 
188
- const handleCopyPublicKey = async () => {
188
+ const handleCopyValue = async (value: string) => {
189
189
  try {
190
- await navigator.clipboard.writeText(window.blocklet.appPk);
190
+ await navigator.clipboard.writeText(value);
191
191
  setCopySuccess(true);
192
192
  setTimeout(() => setCopySuccess(false), 2000);
193
193
  } catch (err) {
194
- console.error('Failed to copy public key:', err);
194
+ console.error('Failed to copy value:', err);
195
195
  }
196
196
  };
197
197
 
198
+ const brokerInfo = [
199
+ ...(window.blocklet.appId ? [{ label: t('admin.vendor.brokerDID'), value: window.blocklet.appId }] : []),
200
+ ...(window.blocklet.appPk ? [{ label: t('admin.vendor.brokerPublicKey'), value: window.blocklet.appPk }] : []),
201
+ ];
202
+
198
203
  return (
199
204
  <>
200
- {/* 供应商服务公钥展示 */}
201
- {window.blocklet.appPk && (
205
+ {/* Broker Information */}
206
+ {brokerInfo.length > 0 && (
202
207
  <Box
203
208
  sx={{
204
- display: 'flex',
205
- flexDirection: { xs: 'column', md: 'row' },
206
- alignItems: { xs: 'flex-start', md: 'center' },
207
- gap: { xs: 1, md: 0 },
208
- p: { xs: 1.5, md: 2 },
209
- mt: { xs: 1.5, md: 1 },
209
+ px: 2,
210
+ py: 1.5,
211
+ my: 2,
210
212
  borderRadius: 1,
211
213
  border: '1px solid',
212
214
  borderColor: 'divider',
215
+ backgroundColor: 'transparent',
216
+ display: 'grid',
217
+ gridTemplateColumns: 'max-content 1fr',
218
+ gap: 1,
219
+ alignItems: 'center',
213
220
  }}>
214
- <Typography
215
- variant="body2"
216
- sx={{
217
- color: 'text.secondary',
218
- minWidth: 'fit-content',
219
- }}>
220
- {t('admin.vendor.servicePublicKey')}:
221
- <Tooltip title={copySuccess ? t('common.copied') : t('common.copy')}>
222
- <IconButton
223
- size="small"
224
- onClick={handleCopyPublicKey}
225
- sx={{
226
- color: copySuccess ? 'success.main' : 'text.secondary',
227
- minWidth: { xs: '24px', md: '32px' },
228
- width: { xs: '24px', md: '32px' },
229
- height: { xs: '24px', md: '32px' },
230
- '&:hover': { backgroundColor: 'grey.100' },
231
- }}>
232
- <ContentCopy sx={{ fontSize: { xs: 16, md: 18 } }} />
233
- </IconButton>
234
- </Tooltip>
235
- </Typography>
236
- <Box
237
- sx={{
238
- display: 'flex',
239
- alignItems: 'center',
240
- gap: 1,
241
- width: { xs: '100%', md: 'auto' },
242
- flex: { xs: 'none', md: 1 },
243
- }}>
244
- <Chip
245
- sx={{ backgroundColor: 'grey.200', color: 'text.secondary' }}
246
- label={window.blocklet.appPk}
247
- variant="outlined"
248
- size="small"
249
- />
250
- </Box>
221
+ {brokerInfo.map((info) => (
222
+ <>
223
+ <Typography
224
+ key={`${info.label}-label`}
225
+ variant="body2"
226
+ color="text.secondary"
227
+ sx={{ justifySelf: 'start' }}>
228
+ {info.label}:
229
+ </Typography>
230
+ <Box sx={{ display: 'flex', alignItems: 'center', overflow: 'hidden' }}>
231
+ <Chip
232
+ key={`${info.label}-chip`}
233
+ label={info.value}
234
+ size="small"
235
+ sx={{
236
+ flexShrink: 1,
237
+ overflow: 'hidden',
238
+ '& .MuiChip-label': {
239
+ overflow: 'hidden',
240
+ textOverflow: 'ellipsis',
241
+ },
242
+ }}
243
+ />
244
+ <Tooltip key={`${info.label}-tooltip`} title={copySuccess ? t('common.copied') : t('common.copy')}>
245
+ <IconButton
246
+ size="small"
247
+ onClick={() => handleCopyValue(info.value)}
248
+ sx={{
249
+ color: copySuccess ? 'success.main' : 'text.secondary',
250
+ '&:hover': {
251
+ color: 'primary.main',
252
+ },
253
+ }}>
254
+ <ContentCopy sx={{ fontSize: 16 }} />
255
+ </IconButton>
256
+ </Tooltip>
257
+ </Box>
258
+ </>
259
+ ))}
251
260
  </Box>
252
261
  )}
253
262
  <Table
@@ -447,7 +447,7 @@ export default function CustomerInvoiceDetail() {
447
447
  sx={{
448
448
  mb: 1.5,
449
449
  }}>
450
- {t('payment.customer.products')}
450
+ {t('admin.invoiceItems')}
451
451
  </Typography>
452
452
  <InvoiceTable invoice={data} simple />
453
453
  </Box>
@@ -609,7 +609,6 @@ export default function CustomerSubscriptionDetail() {
609
609
  )}
610
610
 
611
611
  <InfoRow label={t('admin.subscription.collectionMethod')} value={data.collection_method} />
612
- <InfoRow label={t('admin.subscription.discount')} value={data.discount_id ? data.discount_id : ''} />
613
612
 
614
613
  <InfoRow
615
614
  label={t('admin.paymentMethod._name')}
@@ -682,7 +681,17 @@ export default function CustomerSubscriptionDetail() {
682
681
  <Divider />
683
682
 
684
683
  {/* Discount Information */}
685
- {(data as any).discountStats && <DiscountInfo discountStats={(data as any).discountStats} />}
684
+ {(data as any).discountStats && (
685
+ <Box className="section">
686
+ <Typography variant="h3" className="section-header" sx={{ mb: 1.5 }}>
687
+ {t('admin.subscription.discount')}
688
+ </Typography>
689
+ <Box className="section-body">
690
+ <DiscountInfo discountStats={(data as any).discountStats} />
691
+ </Box>
692
+ </Box>
693
+ )}
694
+ <Box className="divider" />
686
695
 
687
696
  <Box className="section">
688
697
  <Typography
@@ -706,12 +715,16 @@ export default function CustomerSubscriptionDetail() {
706
715
  <>
707
716
  <Divider />
708
717
  <Box className="section">
709
- <VendorServiceList vendorServices={vendorServices} subscriptionId={id} />
718
+ <VendorServiceList
719
+ vendorServices={vendorServices}
720
+ subscriptionId={id}
721
+ isCanceled={data.status === 'canceled'}
722
+ />
710
723
  </Box>
711
724
  </>
712
725
  );
713
726
  })()}
714
- <Divider />
727
+ <Box className="divider" />
715
728
  {isCredit ? (
716
729
  <Box className="section">
717
730
  <Typography variant="h3" className="section-header">