payment-kit 1.18.56 → 1.19.1

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 (214) hide show
  1. package/.eslintrc.js +6 -0
  2. package/api/src/crons/index.ts +8 -0
  3. package/api/src/index.ts +4 -0
  4. package/api/src/libs/credit-grant.ts +146 -0
  5. package/api/src/libs/env.ts +1 -0
  6. package/api/src/libs/invoice.ts +4 -3
  7. package/api/src/libs/notification/template/base.ts +388 -2
  8. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +149 -0
  9. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +151 -0
  10. package/api/src/libs/notification/template/customer-credit-insufficient.ts +254 -0
  11. package/api/src/libs/notification/template/subscription-canceled.ts +193 -202
  12. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +215 -237
  13. package/api/src/libs/notification/template/subscription-renewed.ts +130 -200
  14. package/api/src/libs/notification/template/subscription-succeeded.ts +100 -202
  15. package/api/src/libs/notification/template/subscription-trial-start.ts +142 -188
  16. package/api/src/libs/notification/template/subscription-trial-will-end.ts +146 -174
  17. package/api/src/libs/notification/template/subscription-upgraded.ts +96 -192
  18. package/api/src/libs/notification/template/subscription-will-canceled.ts +94 -135
  19. package/api/src/libs/notification/template/subscription-will-renew.ts +220 -245
  20. package/api/src/libs/payment.ts +69 -0
  21. package/api/src/libs/queue/index.ts +3 -2
  22. package/api/src/libs/session.ts +8 -0
  23. package/api/src/libs/subscription.ts +74 -3
  24. package/api/src/libs/ws.ts +23 -1
  25. package/api/src/locales/en.ts +33 -0
  26. package/api/src/locales/zh.ts +31 -0
  27. package/api/src/queues/credit-consume.ts +715 -0
  28. package/api/src/queues/credit-grant.ts +572 -0
  29. package/api/src/queues/notification.ts +173 -128
  30. package/api/src/queues/payment.ts +210 -122
  31. package/api/src/queues/subscription.ts +179 -0
  32. package/api/src/routes/checkout-sessions.ts +157 -9
  33. package/api/src/routes/connect/shared.ts +3 -2
  34. package/api/src/routes/credit-grants.ts +241 -0
  35. package/api/src/routes/credit-transactions.ts +208 -0
  36. package/api/src/routes/index.ts +8 -0
  37. package/api/src/routes/meter-events.ts +347 -0
  38. package/api/src/routes/meters.ts +219 -0
  39. package/api/src/routes/payment-currencies.ts +14 -2
  40. package/api/src/routes/payment-links.ts +1 -1
  41. package/api/src/routes/payment-methods.ts +14 -2
  42. package/api/src/routes/prices.ts +43 -0
  43. package/api/src/routes/pricing-table.ts +13 -7
  44. package/api/src/routes/products.ts +63 -4
  45. package/api/src/routes/settings.ts +1 -1
  46. package/api/src/routes/subscriptions.ts +4 -0
  47. package/api/src/store/migrations/20250610-billing-credit.ts +43 -0
  48. package/api/src/store/models/credit-grant.ts +486 -0
  49. package/api/src/store/models/credit-transaction.ts +268 -0
  50. package/api/src/store/models/customer.ts +8 -0
  51. package/api/src/store/models/index.ts +52 -1
  52. package/api/src/store/models/meter-event.ts +423 -0
  53. package/api/src/store/models/meter.ts +176 -0
  54. package/api/src/store/models/payment-currency.ts +66 -14
  55. package/api/src/store/models/price.ts +6 -0
  56. package/api/src/store/models/product.ts +2 -2
  57. package/api/src/store/models/subscription.ts +24 -0
  58. package/api/src/store/models/types.ts +28 -2
  59. package/api/tests/libs/subscription.spec.ts +53 -0
  60. package/blocklet.yml +9 -1
  61. package/package.json +57 -58
  62. package/scripts/sdk.js +233 -1
  63. package/src/app.tsx +10 -0
  64. package/src/components/actions.tsx +22 -9
  65. package/src/components/balance-list.tsx +40 -12
  66. package/src/components/collapse.tsx +33 -15
  67. package/src/components/copyable.tsx +8 -7
  68. package/src/components/currency.tsx +15 -7
  69. package/src/components/customer/actions.tsx +1 -5
  70. package/src/components/customer/credit-grant-item-list.tsx +99 -0
  71. package/src/components/customer/credit-overview.tsx +233 -0
  72. package/src/components/customer/form.tsx +7 -2
  73. package/src/components/customer/link.tsx +4 -12
  74. package/src/components/customer/notification-preference.tsx +18 -9
  75. package/src/components/customer/overdraft-protection.tsx +112 -41
  76. package/src/components/drawer-form.tsx +42 -18
  77. package/src/components/error.tsx +1 -5
  78. package/src/components/event/list.tsx +9 -10
  79. package/src/components/filter-toolbar.tsx +20 -19
  80. package/src/components/info-card.tsx +32 -18
  81. package/src/components/info-metric.tsx +16 -6
  82. package/src/components/info-row-group.tsx +1 -7
  83. package/src/components/info-row.tsx +30 -24
  84. package/src/components/invoice/action.tsx +1 -7
  85. package/src/components/invoice/list.tsx +34 -26
  86. package/src/components/invoice/recharge.tsx +5 -7
  87. package/src/components/invoice/table.tsx +17 -12
  88. package/src/components/layout/user.tsx +1 -1
  89. package/src/components/metadata/form.tsx +290 -94
  90. package/src/components/metadata/list.tsx +11 -3
  91. package/src/components/meter/actions.tsx +101 -0
  92. package/src/components/meter/add-usage-dialog.tsx +239 -0
  93. package/src/components/meter/events-list.tsx +657 -0
  94. package/src/components/meter/form.tsx +245 -0
  95. package/src/components/meter/products.tsx +264 -0
  96. package/src/components/meter/usage-guide.tsx +174 -0
  97. package/src/components/passport/actions.tsx +9 -4
  98. package/src/components/payment-currency/add.tsx +16 -3
  99. package/src/components/payment-currency/form.tsx +14 -6
  100. package/src/components/payment-intent/actions.tsx +24 -16
  101. package/src/components/payment-intent/list.tsx +30 -9
  102. package/src/components/payment-link/actions.tsx +1 -5
  103. package/src/components/payment-link/after-pay.tsx +4 -2
  104. package/src/components/payment-link/before-pay.tsx +14 -4
  105. package/src/components/payment-link/item.tsx +27 -6
  106. package/src/components/payment-link/preview.tsx +9 -9
  107. package/src/components/payment-link/product-select.tsx +69 -15
  108. package/src/components/payment-method/arcblock.tsx +8 -1
  109. package/src/components/payment-method/base.tsx +8 -1
  110. package/src/components/payment-method/bitcoin.tsx +8 -1
  111. package/src/components/payment-method/ethereum.tsx +8 -1
  112. package/src/components/payment-method/evm-rpc-input.tsx +11 -7
  113. package/src/components/payment-method/form.tsx +2 -7
  114. package/src/components/payment-method/stripe.tsx +2 -0
  115. package/src/components/payouts/actions.tsx +1 -5
  116. package/src/components/payouts/list.tsx +30 -10
  117. package/src/components/payouts/portal/list.tsx +11 -9
  118. package/src/components/price/currency-select.tsx +63 -32
  119. package/src/components/price/form.tsx +895 -370
  120. package/src/components/price/upsell-select.tsx +10 -2
  121. package/src/components/price/upsell.tsx +7 -2
  122. package/src/components/pricing-table/actions.tsx +1 -5
  123. package/src/components/pricing-table/customer-settings.tsx +5 -1
  124. package/src/components/pricing-table/payment-settings.tsx +14 -4
  125. package/src/components/pricing-table/preview.tsx +9 -9
  126. package/src/components/pricing-table/price-item.tsx +6 -1
  127. package/src/components/pricing-table/product-item.tsx +6 -1
  128. package/src/components/pricing-table/product-settings.tsx +17 -4
  129. package/src/components/product/actions.tsx +1 -5
  130. package/src/components/product/add-price.tsx +9 -7
  131. package/src/components/product/create.tsx +8 -9
  132. package/src/components/product/cross-sell-select.tsx +5 -1
  133. package/src/components/product/cross-sell.tsx +7 -2
  134. package/src/components/product/edit-price.tsx +21 -12
  135. package/src/components/product/features.tsx +26 -6
  136. package/src/components/product/form.tsx +115 -72
  137. package/src/components/progress-bar.tsx +1 -1
  138. package/src/components/refund/actions.tsx +1 -7
  139. package/src/components/refund/list.tsx +31 -18
  140. package/src/components/section/header.tsx +12 -14
  141. package/src/components/subscription/actions/cancel.tsx +22 -5
  142. package/src/components/subscription/actions/index.tsx +9 -10
  143. package/src/components/subscription/actions/pause.tsx +32 -6
  144. package/src/components/subscription/actions/slash-stake.tsx +5 -3
  145. package/src/components/subscription/description.tsx +12 -8
  146. package/src/components/subscription/items/index.tsx +31 -16
  147. package/src/components/subscription/items/usage-records.tsx +19 -5
  148. package/src/components/subscription/list.tsx +5 -7
  149. package/src/components/subscription/metrics.tsx +62 -15
  150. package/src/components/subscription/portal/actions.tsx +78 -71
  151. package/src/components/subscription/portal/cancel.tsx +10 -3
  152. package/src/components/subscription/portal/list.tsx +48 -26
  153. package/src/components/uploader.tsx +5 -13
  154. package/src/components/webhook/attempts.tsx +51 -16
  155. package/src/components/webhook/request-info.tsx +8 -6
  156. package/src/contexts/products.tsx +27 -10
  157. package/src/hooks/subscription.ts +34 -0
  158. package/src/libs/meter-utils.ts +196 -0
  159. package/src/libs/util.ts +4 -0
  160. package/src/locales/en.tsx +385 -4
  161. package/src/locales/zh.tsx +364 -0
  162. package/src/pages/admin/billing/index.tsx +61 -33
  163. package/src/pages/admin/billing/invoices/detail.tsx +49 -13
  164. package/src/pages/admin/billing/meters/create.tsx +60 -0
  165. package/src/pages/admin/billing/meters/detail.tsx +435 -0
  166. package/src/pages/admin/billing/meters/index.tsx +210 -0
  167. package/src/pages/admin/billing/meters/meter-event.tsx +346 -0
  168. package/src/pages/admin/billing/subscriptions/detail.tsx +90 -25
  169. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +391 -0
  170. package/src/pages/admin/customers/customers/detail.tsx +67 -14
  171. package/src/pages/admin/customers/customers/index.tsx +6 -1
  172. package/src/pages/admin/customers/index.tsx +5 -0
  173. package/src/pages/admin/developers/events/detail.tsx +37 -11
  174. package/src/pages/admin/developers/index.tsx +1 -1
  175. package/src/pages/admin/developers/webhooks/detail.tsx +41 -11
  176. package/src/pages/admin/index.tsx +15 -2
  177. package/src/pages/admin/overview.tsx +107 -19
  178. package/src/pages/admin/payments/intents/detail.tsx +58 -14
  179. package/src/pages/admin/payments/payouts/detail.tsx +63 -15
  180. package/src/pages/admin/payments/refunds/detail.tsx +58 -14
  181. package/src/pages/admin/products/index.tsx +11 -4
  182. package/src/pages/admin/products/links/create.tsx +22 -4
  183. package/src/pages/admin/products/links/detail.tsx +43 -14
  184. package/src/pages/admin/products/passports/index.tsx +23 -4
  185. package/src/pages/admin/products/prices/actions.tsx +16 -9
  186. package/src/pages/admin/products/prices/detail.tsx +73 -14
  187. package/src/pages/admin/products/prices/list.tsx +15 -3
  188. package/src/pages/admin/products/pricing-tables/create.tsx +45 -12
  189. package/src/pages/admin/products/pricing-tables/detail.tsx +45 -14
  190. package/src/pages/admin/products/products/create.tsx +233 -54
  191. package/src/pages/admin/products/products/detail.tsx +74 -18
  192. package/src/pages/admin/settings/index.tsx +8 -1
  193. package/src/pages/admin/settings/payment-methods/index.tsx +87 -19
  194. package/src/pages/admin/settings/vault-config/edit-form.tsx +42 -28
  195. package/src/pages/admin/settings/vault-config/index.tsx +57 -10
  196. package/src/pages/customer/credit-grant/detail.tsx +308 -0
  197. package/src/pages/customer/index.tsx +76 -17
  198. package/src/pages/customer/invoice/detail.tsx +63 -14
  199. package/src/pages/customer/invoice/past-due.tsx +11 -3
  200. package/src/pages/customer/payout/detail.tsx +56 -13
  201. package/src/pages/customer/recharge/account.tsx +78 -18
  202. package/src/pages/customer/recharge/subscription.tsx +86 -25
  203. package/src/pages/customer/refund/list.tsx +60 -24
  204. package/src/pages/customer/subscription/change-payment.tsx +17 -6
  205. package/src/pages/customer/subscription/change-plan.tsx +34 -7
  206. package/src/pages/customer/subscription/detail.tsx +134 -34
  207. package/src/pages/customer/subscription/embed.tsx +25 -5
  208. package/src/pages/home.tsx +26 -4
  209. package/src/pages/integrations/donations/edit-form.tsx +25 -9
  210. package/src/pages/integrations/donations/index.tsx +26 -9
  211. package/src/pages/integrations/donations/preview.tsx +59 -15
  212. package/src/pages/integrations/index.tsx +10 -1
  213. package/src/pages/integrations/overview.tsx +78 -17
  214. package/vite.config.ts +60 -30
@@ -10,6 +10,7 @@ import {
10
10
  Grid,
11
11
  List,
12
12
  ListItem,
13
+ ListItemButton,
13
14
  ListItemIcon,
14
15
  ListItemText,
15
16
  ListSubheader,
@@ -49,13 +50,7 @@ type Props = {
49
50
  event?: TEvent & { requestInfo?: RequestInfo };
50
51
  };
51
52
 
52
- WebhookAttempts.defaultProps = {
53
- event_id: '',
54
- webhook_endpoint_id: '',
55
- event: null,
56
- };
57
-
58
- export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }: Props) {
53
+ export default function WebhookAttempts({ event_id = '', webhook_endpoint_id = '', event = undefined }: Props) {
59
54
  const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TWebhookAttemptExpanded>>(
60
55
  (d) => {
61
56
  const size = 15;
@@ -93,9 +88,14 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
93
88
 
94
89
  return (
95
90
  <Grid container spacing={2.5}>
96
- <Grid item xs={12} md={4} className="attempt-list">
91
+ <Grid size={{ xs: 12, md: 4 }} className="attempt-list">
97
92
  {isEmpty(Object.keys(groupedAttempts)) ? (
98
- <Typography color="text.secondary">No Attempt</Typography>
93
+ <Typography
94
+ sx={{
95
+ color: 'text.secondary',
96
+ }}>
97
+ No Attempt
98
+ </Typography>
99
99
  ) : (
100
100
  <>
101
101
  <List sx={{ maxHeight: '64vh', overflowY: 'scroll' }}>
@@ -104,12 +104,17 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
104
104
  <ListSubheader>{date}</ListSubheader>
105
105
  {items.map((attempt) => (
106
106
  <ListItem
107
- button
108
107
  key={attempt.id}
108
+ component={ListItemButton}
109
109
  onClick={() => handleAttemptClick(attempt)}
110
110
  selected={selected === attempt}
111
111
  secondaryAction={
112
- <Typography color="text.secondary">{formatTime(attempt.created_at, 'HH:mm:ss A')}</Typography>
112
+ <Typography
113
+ sx={{
114
+ color: 'text.secondary',
115
+ }}>
116
+ {formatTime(attempt.created_at, 'HH:mm:ss A')}
117
+ </Typography>
113
118
  }>
114
119
  <ListItemIcon>
115
120
  {isSuccessAttempt(attempt.response_status) === false ? (
@@ -118,7 +123,16 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
118
123
  <CheckCircleOutlined color="success" />
119
124
  )}
120
125
  </ListItemIcon>
121
- <ListItemText primary={<Typography color="text.primary">{attempt.event.type}</Typography>} />
126
+ <ListItemText
127
+ primary={
128
+ <Typography
129
+ sx={{
130
+ color: 'text.primary',
131
+ }}>
132
+ {attempt.event.type}
133
+ </Typography>
134
+ }
135
+ />
122
136
  </ListItem>
123
137
  ))}
124
138
  </React.Fragment>
@@ -129,11 +143,22 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
129
143
  {loadingMore ? 'Loading more...' : 'Load more'}
130
144
  </Button>
131
145
  )}
132
- {!hasMore && <Typography color="text.secondary">No more data</Typography>}
146
+ {!hasMore && (
147
+ <Typography
148
+ sx={{
149
+ color: 'text.secondary',
150
+ }}>
151
+ No more data
152
+ </Typography>
153
+ )}
133
154
  </>
134
155
  )}
135
156
  </Grid>
136
- <Grid item xs={12} md={8}>
157
+ <Grid
158
+ size={{
159
+ xs: 12,
160
+ md: 8,
161
+ }}>
137
162
  {selected && (
138
163
  <Stack direction="column" spacing={2} sx={{ pt: 3, pl: 3, borderLeft: '1px solid', borderColor: 'grey.100' }}>
139
164
  <Typography variant="h6">{event_id ? selected.endpoint.url : selected.event.type}</Typography>
@@ -143,7 +168,12 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
143
168
  <CodeBlock language="json">{JSON.stringify(selected.response_body, null, 2)}</CodeBlock>
144
169
  </Box>
145
170
  <Box>
146
- <Stack direction="row" alignItems="center" spacing={1}>
171
+ <Stack
172
+ direction="row"
173
+ spacing={1}
174
+ sx={{
175
+ alignItems: 'center',
176
+ }}>
147
177
  <Typography variant="h6">Request</Typography>
148
178
  <RequestInfoPopper
149
179
  // @ts-ignore
@@ -158,7 +188,12 @@ export default function WebhookAttempts({ event_id, webhook_endpoint_id, event }
158
188
  )}
159
189
  {data?.list.length === 0 && event && (
160
190
  <Box>
161
- <Stack direction="row" alignItems="center" spacing={1}>
191
+ <Stack
192
+ direction="row"
193
+ spacing={1}
194
+ sx={{
195
+ alignItems: 'center',
196
+ }}>
162
197
  <Typography variant="h6">Event Data</Typography>
163
198
  <RequestInfoPopper
164
199
  // @ts-ignore
@@ -16,11 +16,7 @@ type Props = {
16
16
  requestInfo?: RequestInfo;
17
17
  request?: RequestType;
18
18
  };
19
- RequestInfoPopper.defaultProps = {
20
- requestInfo: null,
21
- request: null,
22
- };
23
- export default function RequestInfoPopper({ requestInfo, request }: Props) {
19
+ export default function RequestInfoPopper({ requestInfo = undefined, request = undefined }: Props) {
24
20
  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
25
21
 
26
22
  const handleClick = (e: React.MouseEvent<HTMLElement>) => {
@@ -119,7 +115,13 @@ export default function RequestInfoPopper({ requestInfo, request }: Props) {
119
115
  }}>
120
116
  {requestInfo ? (
121
117
  <>
122
- <Typography variant="caption" color="text.secondary" sx={{ mb: 1, display: 'block' }}>
118
+ <Typography
119
+ variant="caption"
120
+ sx={{
121
+ color: 'text.secondary',
122
+ mb: 1,
123
+ display: 'block',
124
+ }}>
123
125
  Requested by:
124
126
  </Typography>
125
127
  <InfoCard
@@ -1,28 +1,39 @@
1
- import { api } from '@blocklet/payment-react';
1
+ import { api, CachedRequest } from '@blocklet/payment-react';
2
2
  import type { TProductExpanded } from '@blocklet/payment-types';
3
3
  import { Alert, CircularProgress } from '@mui/material';
4
4
  import { useRequest } from 'ahooks';
5
- import { createContext, useContext } from 'react';
5
+ import { createContext, useContext, type JSX } from 'react';
6
+ import useBus from 'use-bus';
6
7
 
7
8
  type ProductsContextType = {
8
9
  products: TProductExpanded[];
9
- refresh: () => void;
10
+ refresh: (forceRefresh?: boolean) => void;
10
11
  };
11
12
 
12
13
  // @ts-ignore
13
14
  const ProductsContext = createContext<ProductsContextType>({ api });
14
15
  const { Provider, Consumer } = ProductsContext;
15
16
 
16
- const getProducts = async (): Promise<TProductExpanded[]> => {
17
- // FIXME: pagination here
18
- const { data } = await api.get('/api/products?active=true&page=1&pageSize=100&donation=hide');
19
- return data.list || [];
17
+ const fetchProducts = (forceRefresh = false): Promise<{ list: TProductExpanded[] }> => {
18
+ const livemode = localStorage.getItem('livemode') !== 'false';
19
+ const cacheKey = `products-${livemode}`;
20
+
21
+ const cachedRequest = new CachedRequest(
22
+ cacheKey,
23
+ () => api.get('/api/products?active=true&page=1&pageSize=100&donation=hide'),
24
+ {
25
+ ttl: 1000 * 60 * 10, // 10分钟缓存
26
+ strategy: 'session',
27
+ }
28
+ );
29
+
30
+ return cachedRequest.fetch(forceRefresh);
20
31
  };
21
32
 
22
33
  // eslint-disable-next-line react/prop-types
23
34
  function ProductsProvider({ children }: { children: any }): JSX.Element {
24
- const { data, error, run, loading } = useRequest(getProducts);
25
-
35
+ const { data, error, run, loading } = useRequest((forceRefresh = false) => fetchProducts(forceRefresh));
36
+ useBus('project.created', () => run(true), []);
26
37
  if (error) {
27
38
  return <Alert severity="error">{error.message}</Alert>;
28
39
  }
@@ -31,7 +42,7 @@ function ProductsProvider({ children }: { children: any }): JSX.Element {
31
42
  return <CircularProgress />;
32
43
  }
33
44
 
34
- return <Provider value={{ products: data, refresh: run }}>{children}</Provider>;
45
+ return <Provider value={{ products: data?.list ?? [], refresh: () => run(true) }}>{children}</Provider>;
35
46
  }
36
47
 
37
48
  function useProductsContext() {
@@ -39,4 +50,10 @@ function useProductsContext() {
39
50
  return context;
40
51
  }
41
52
 
53
+ export const clearProductsCache = () => {
54
+ const livemode = localStorage.getItem('livemode') !== 'false';
55
+ const cacheKey = `products-${livemode}`;
56
+ sessionStorage.removeItem(cacheKey);
57
+ };
58
+
42
59
  export { ProductsContext, ProductsProvider, Consumer as ProductsConsumer, useProductsContext };
@@ -3,6 +3,7 @@ import Toast from '@arcblock/ux/lib/Toast';
3
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
4
  import { useRequest } from 'ahooks';
5
5
  import { api } from '@blocklet/payment-react';
6
+ import type { TPaymentCurrency } from '@blocklet/payment-types';
6
7
 
7
8
  export function useUnpaidInvoicesCheckForSubscription(subscriptionId: string, manualCheck: boolean = false) {
8
9
  const { t } = useLocaleContext();
@@ -28,3 +29,36 @@ export function useUnpaidInvoicesCheckForSubscription(subscriptionId: string, ma
28
29
  hasUnpaid: data?.count > 0,
29
30
  };
30
31
  }
32
+
33
+ export function usePendingAmountForSubscription(subscriptionId: string, paymentCurrency?: TPaymentCurrency) {
34
+ const { data: pendingAmountSummary, run: checkPendingAmount } = useRequest(
35
+ async () => {
36
+ if (!subscriptionId || !paymentCurrency?.id) return null;
37
+
38
+ const response = await api.get('/api/meter-events/pending-amount', {
39
+ params: {
40
+ subscription_id: subscriptionId,
41
+ currency_id: paymentCurrency.id,
42
+ },
43
+ });
44
+
45
+ return response.data;
46
+ },
47
+ {
48
+ ready: !!subscriptionId && !!paymentCurrency?.id,
49
+ refreshDeps: [subscriptionId, paymentCurrency?.id],
50
+ pollingInterval: 30000, // 每30秒检查一次
51
+ }
52
+ );
53
+
54
+ // pendingAmountSummary 是 GroupedBN 格式: { [currencyId: string]: string }
55
+ const pendingAmount = pendingAmountSummary && paymentCurrency?.id ? pendingAmountSummary[paymentCurrency.id] : null;
56
+
57
+ const hasPendingAmount = pendingAmount && pendingAmount !== '0';
58
+
59
+ return {
60
+ pendingAmount,
61
+ hasPendingAmount,
62
+ checkPendingAmount,
63
+ };
64
+ }
@@ -0,0 +1,196 @@
1
+ import { api } from '@blocklet/payment-react';
2
+ import type { TMeter, TCustomer, TPaymentCurrency, TSubscription } from '@blocklet/payment-types';
3
+
4
+ export interface MeterInfo {
5
+ meter?: TMeter & { paymentCurrency?: TPaymentCurrency };
6
+ customer?: TCustomer;
7
+ paymentCurrency?: TPaymentCurrency;
8
+ subscription?: TSubscription;
9
+ }
10
+
11
+ export interface GetMeterInfoOptions {
12
+ meterId?: string;
13
+ customerId?: string;
14
+ eventName?: string;
15
+ includeCustomer?: boolean;
16
+ includePaymentCurrency?: boolean;
17
+ includeSubscription?: boolean;
18
+ subscriptionId?: string;
19
+ }
20
+
21
+ /**
22
+ * 获取meter相关信息的通用方法
23
+ * @param options 配置选项
24
+ * @returns Promise<MeterInfo>
25
+ */
26
+ export async function getMeterInfo(options: GetMeterInfoOptions = {}): Promise<MeterInfo> {
27
+ const {
28
+ meterId,
29
+ customerId,
30
+ eventName,
31
+ includeCustomer = false,
32
+ includePaymentCurrency = false,
33
+ includeSubscription = false,
34
+ subscriptionId,
35
+ } = options;
36
+
37
+ const result: MeterInfo = {};
38
+
39
+ try {
40
+ // 获取meter信息
41
+ if (meterId) {
42
+ const { data: meter } = await api.get(`/api/meters/${meterId}`);
43
+ result.meter = meter;
44
+
45
+ // 如果meter包含paymentCurrency且需要包含
46
+ if (includePaymentCurrency && meter.paymentCurrency) {
47
+ result.paymentCurrency = meter.paymentCurrency;
48
+ }
49
+ } else if (eventName) {
50
+ // 通过event_name查找meter
51
+ const { data: meters } = await api.get(`/api/meters?event_name=${encodeURIComponent(eventName)}`);
52
+ if (meters.list && meters.list.length > 0) {
53
+ const [firstMeter] = meters.list;
54
+ result.meter = firstMeter;
55
+
56
+ if (includePaymentCurrency && firstMeter.paymentCurrency) {
57
+ result.paymentCurrency = firstMeter.paymentCurrency;
58
+ }
59
+ }
60
+ }
61
+
62
+ // 获取customer信息
63
+ if (includeCustomer && customerId) {
64
+ const { data: customer } = await api.get(`/api/customers/${customerId}`);
65
+ result.customer = customer;
66
+ }
67
+
68
+ // 获取subscription信息
69
+ if (includeSubscription && subscriptionId) {
70
+ const { data: subscription } = await api.get(`/api/subscriptions/${subscriptionId}`);
71
+ result.subscription = subscription;
72
+ }
73
+
74
+ // 如果需要paymentCurrency但还没有获取到,尝试从meter的currency_id获取
75
+ if (includePaymentCurrency && !result.paymentCurrency && result.meter?.currency_id) {
76
+ try {
77
+ const { data: paymentCurrency } = await api.get(`/api/payment-currencies/${result.meter.currency_id}`);
78
+ result.paymentCurrency = paymentCurrency;
79
+ } catch (err) {
80
+ console.warn('Failed to fetch payment currency:', err);
81
+ }
82
+ }
83
+
84
+ return result;
85
+ } catch (error) {
86
+ console.error('Error fetching meter info:', error);
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * 获取meter的完整信息(包含所有关联数据)
93
+ * @param meterId meter ID
94
+ * @returns Promise<MeterInfo>
95
+ */
96
+ export function getMeterFullInfo(meterId: string): Promise<MeterInfo> {
97
+ return getMeterInfo({
98
+ meterId,
99
+ includePaymentCurrency: true,
100
+ });
101
+ }
102
+
103
+ /**
104
+ * 根据事件名称获取meter信息
105
+ * @param eventName 事件名称
106
+ * @param includePaymentCurrency 是否包含支付货币信息
107
+ * @returns Promise<MeterInfo>
108
+ */
109
+ export function getMeterByEventName(eventName: string, includePaymentCurrency = true): Promise<MeterInfo> {
110
+ return getMeterInfo({
111
+ eventName,
112
+ includePaymentCurrency,
113
+ });
114
+ }
115
+
116
+ /**
117
+ * 获取meter事件的完整上下文信息
118
+ * @param options 配置选项
119
+ * @returns Promise<MeterInfo>
120
+ */
121
+ export function getMeterEventContext(options: {
122
+ meterId?: string;
123
+ eventName?: string;
124
+ customerId?: string;
125
+ subscriptionId?: string;
126
+ }): Promise<MeterInfo> {
127
+ const { meterId, eventName, customerId, subscriptionId } = options;
128
+
129
+ return getMeterInfo({
130
+ meterId,
131
+ eventName,
132
+ customerId,
133
+ subscriptionId,
134
+ includeCustomer: !!customerId,
135
+ includePaymentCurrency: true,
136
+ includeSubscription: !!subscriptionId,
137
+ });
138
+ }
139
+
140
+ /**
141
+ * 批量获取多个meter的信息
142
+ * @param meterIds meter ID数组
143
+ * @param includePaymentCurrency 是否包含支付货币信息
144
+ * @returns Promise<MeterInfo[]>
145
+ */
146
+ export function getBatchMeterInfo(meterIds: string[], includePaymentCurrency = true): Promise<MeterInfo[]> {
147
+ const promises = meterIds.map((meterId) =>
148
+ getMeterInfo({
149
+ meterId,
150
+ includePaymentCurrency,
151
+ }).catch((error) => {
152
+ console.warn(`Failed to fetch meter ${meterId}:`, error);
153
+ return { meter: undefined };
154
+ })
155
+ );
156
+
157
+ return Promise.all(promises);
158
+ }
159
+
160
+ /**
161
+ * 获取客户的meter使用情况
162
+ * @param customerId 客户ID
163
+ * @param meterId 可选的meter ID,如果不提供则获取所有meter
164
+ * @returns Promise<MeterInfo[]>
165
+ */
166
+ export async function getCustomerMeterUsage(customerId: string, meterId?: string): Promise<MeterInfo[]> {
167
+ try {
168
+ // 获取客户的credit transactions来找到相关的meters
169
+ const params = new URLSearchParams();
170
+ params.append('customer_id', customerId);
171
+ if (meterId) {
172
+ params.append('meter_id', meterId);
173
+ }
174
+
175
+ const { data: transactions } = await api.get(`/api/credit-transactions?${params.toString()}`);
176
+
177
+ // 提取唯一的meter IDs
178
+ const uniqueMeterIds = [
179
+ ...new Set(
180
+ transactions.list
181
+ ?.map((t: any) => t.meter?.id)
182
+ .filter((id: any): id is string => Boolean(id) && typeof id === 'string')
183
+ ),
184
+ ] as string[];
185
+
186
+ if (uniqueMeterIds.length === 0) {
187
+ return [];
188
+ }
189
+
190
+ // 批量获取meter信息
191
+ return await getBatchMeterInfo(uniqueMeterIds);
192
+ } catch (error) {
193
+ console.error('Error fetching customer meter usage:', error);
194
+ return [];
195
+ }
196
+ }
package/src/libs/util.ts CHANGED
@@ -50,6 +50,10 @@ export function getPricingModel(price: TPrice) {
50
50
  return 'package';
51
51
  }
52
52
 
53
+ if (price.type === 'recurring' && price.recurring?.usage_type === 'metered' && price.recurring?.meter_id) {
54
+ return 'credit_metered';
55
+ }
56
+
53
57
  return 'standard';
54
58
  }
55
59