payment-kit 1.13.95 → 1.13.97

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.
@@ -1,6 +1,15 @@
1
- import { CheckoutSession, Price, Product } from '../store/models';
1
+ import isEmpty from 'lodash/isEmpty';
2
+
3
+ import { CheckoutSession, Price, Product, Subscription } from '../store/models';
2
4
 
3
5
  export async function getMainProductName(subscriptionId: string): Promise<string> {
6
+ const subscription = await Subscription.findByPk(subscriptionId);
7
+
8
+ // @see: https://github.com/ArcBlock/did-spaces/issues/962#issuecomment-1880549421
9
+ if (subscription && !isEmpty(subscription.description)) {
10
+ return subscription.description!;
11
+ }
12
+
4
13
  const checkoutSession = await CheckoutSession.findOne({
5
14
  where: {
6
15
  subscription_id: subscriptionId,
@@ -37,6 +37,7 @@ import type { LineItem, SubscriptionUpdateItem } from '../store/models/types';
37
37
  import { UsageRecord } from '../store/models/usage-record';
38
38
  import { sequelize } from '../store/sequelize';
39
39
  import { ensureInvoiceAndItems } from './connect/shared';
40
+ import { createUsageRecordQueryFn } from './usage-records';
40
41
 
41
42
  const router = Router();
42
43
  const auth = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
@@ -1068,4 +1069,9 @@ router.delete('/:id', auth, async (req, res) => {
1068
1069
  return res.json(doc);
1069
1070
  });
1070
1071
 
1072
+ // Get usage records
1073
+ router.get('/:id/usage-records', authPortal, (req, res) => {
1074
+ createUsageRecordQueryFn(req.doc)(req, res);
1075
+ });
1076
+
1071
1077
  export default router;
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable consistent-return */
2
- import { Router } from 'express';
2
+ import { Request, Response, Router } from 'express';
3
3
  import Joi from 'joi';
4
4
  import pick from 'lodash/pick';
5
5
  import { Op } from 'sequelize';
@@ -121,34 +121,38 @@ router.get('/summary', auth, async (req, res) => {
121
121
  }
122
122
  });
123
123
 
124
- router.get('/', auth, async (req, res) => {
125
- const { page, pageSize, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
124
+ export function createUsageRecordQueryFn(doc?: Subscription) {
125
+ return async (req: Request, res: Response) => {
126
+ const { page, pageSize, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
126
127
 
127
- try {
128
- const item = await SubscriptionItem.findByPk(query.subscription_item_id);
129
- if (!item) {
130
- return res.status(400).json({ error: `SubscriptionItem not found: ${query.subscription_item_id}` });
131
- }
132
- const subscription = await Subscription.findByPk(item.subscription_id);
133
- if (!subscription) {
134
- return res.status(400).json({ error: `Subscription not found: ${item.subscription_id}` });
135
- }
128
+ try {
129
+ const item = await SubscriptionItem.findByPk(query.subscription_item_id);
130
+ if (!item) {
131
+ return res.status(400).json({ error: `SubscriptionItem not found: ${query.subscription_item_id}` });
132
+ }
133
+ const subscription = doc || (await Subscription.findByPk(item.subscription_id));
134
+ if (!subscription) {
135
+ return res.status(400).json({ error: `Subscription not found: ${item.subscription_id}` });
136
+ }
136
137
 
137
- const { rows: list, count } = await UsageRecord.findAndCountAll({
138
- where: {
139
- subscription_item_id: query.subscription_item_id,
140
- timestamp: { [Op.gte]: subscription.current_period_start, [Op.lt]: subscription.current_period_end },
141
- },
142
- order: [['created_at', 'DESC']],
143
- offset: (page - 1) * pageSize,
144
- limit: pageSize,
145
- });
138
+ const { rows: list, count } = await UsageRecord.findAndCountAll({
139
+ where: {
140
+ subscription_item_id: query.subscription_item_id,
141
+ timestamp: { [Op.gte]: subscription.current_period_start, [Op.lt]: subscription.current_period_end },
142
+ },
143
+ order: [['created_at', 'DESC']],
144
+ offset: (page - 1) * pageSize,
145
+ limit: pageSize,
146
+ });
147
+
148
+ res.json({ count, list });
149
+ } catch (err) {
150
+ console.error(err);
151
+ res.json({ count: 0, list: [] });
152
+ }
153
+ };
154
+ }
146
155
 
147
- res.json({ count, list });
148
- } catch (err) {
149
- console.error(err);
150
- res.json({ count: 0, list: [] });
151
- }
152
- });
156
+ router.get('/', auth, createUsageRecordQueryFn());
153
157
 
154
158
  export default router;
@@ -1,8 +1,10 @@
1
+ import dayjs from '../../src/libs/dayjs';
1
2
  import {
2
3
  getCheckoutMode,
3
4
  getPriceCurrencyOptions,
4
5
  getPriceUintAmountByCurrency,
5
6
  getRecurringPeriod,
7
+ getSubscriptionCreateSetup,
6
8
  } from '../../src/libs/session';
7
9
  import type { TLineItemExpanded } from '../../src/store/models';
8
10
 
@@ -60,6 +62,7 @@ describe('getPriceCurrencyOptions', () => {
60
62
  expect(result).toEqual([{ currency_id: 'usd', unit_amount: 1000, tiers: null, custom_unit_amount: null }]);
61
63
  });
62
64
  });
65
+
63
66
  describe('getPriceUintAmountByCurrency', () => {
64
67
  it('should return the unit_amount of the matching currency_id in currency_options', () => {
65
68
  const price = {
@@ -96,6 +99,7 @@ describe('getPriceUintAmountByCurrency', () => {
96
99
  expect(result).toBe(1000);
97
100
  });
98
101
  });
102
+
99
103
  describe('getRecurringPeriod', () => {
100
104
  it('should return the correct period in milliseconds when interval is "hour"', () => {
101
105
  const recurring = { interval: 'hour', interval_count: '1' };
@@ -133,3 +137,78 @@ describe('getRecurringPeriod', () => {
133
137
  expect(result).toBe(0);
134
138
  });
135
139
  });
140
+
141
+ describe('getSubscriptionCreateSetup', () => {
142
+ const currencies = [
143
+ {
144
+ currency_id: 'usd',
145
+ unit_amount: '1',
146
+ },
147
+ ];
148
+
149
+ it('should calculate setup for recurring licensed price type', () => {
150
+ const items = [
151
+ {
152
+ price: { type: 'one_time', currency_options: currencies },
153
+ quantity: 1,
154
+ },
155
+ {
156
+ price: {
157
+ type: 'recurring',
158
+ currency_options: currencies,
159
+ recurring: { interval: 'day', interval_count: '1', usage_type: 'licensed' },
160
+ },
161
+ quantity: 2,
162
+ },
163
+ ];
164
+ const result = getSubscriptionCreateSetup(items as any, 'usd');
165
+ expect(result.amount.setup).toBe('3');
166
+ });
167
+
168
+ it('should not calculate setup for recurring metered price type', () => {
169
+ const items = [
170
+ {
171
+ price: { type: 'one_time', currency_options: currencies },
172
+ quantity: 1,
173
+ },
174
+ {
175
+ price: {
176
+ type: 'recurring',
177
+ currency_options: currencies,
178
+ recurring: { interval: 'day', interval_count: '1', usage_type: 'metered' },
179
+ },
180
+ quantity: 2,
181
+ },
182
+ ];
183
+ const result = getSubscriptionCreateSetup(items as any, 'usd');
184
+ expect(result.amount.setup).toBe('1');
185
+ });
186
+
187
+ it('should calculate cycle duration for recurring price type', () => {
188
+ const items = [
189
+ {
190
+ price: { type: 'recurring', currency_options: currencies, recurring: { interval: 'day', interval_count: '1' } },
191
+ quantity: 2,
192
+ },
193
+ ];
194
+ const result = getSubscriptionCreateSetup(items as any, 'usd');
195
+ expect(result.cycle.duration).toBe(24 * 60 * 60 * 1000);
196
+ });
197
+
198
+ it('should calculate trial period when trialInDays is provided', () => {
199
+ const items = [
200
+ {
201
+ price: { type: 'recurring', currency_options: currencies, recurring: { interval: 'day', interval_count: '1' } },
202
+ quantity: 2,
203
+ },
204
+ ];
205
+ const result = getSubscriptionCreateSetup(items as any, 'usd', 7);
206
+ const now = dayjs().unix();
207
+ expect(result.trail.start).toBe(now);
208
+ expect(result.trail.end).toBe(
209
+ dayjs()
210
+ .add(7 * 24 * 60 * 60 * 1000, 'millisecond')
211
+ .unix()
212
+ );
213
+ });
214
+ });
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.13.95
17
+ version: 1.13.97
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.13.95",
3
+ "version": "1.13.97",
4
4
  "scripts": {
5
5
  "dev": "COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev",
6
6
  "eject": "vite eject",
@@ -110,7 +110,7 @@
110
110
  "@abtnode/types": "1.16.21",
111
111
  "@arcblock/eslint-config": "^0.2.4",
112
112
  "@arcblock/eslint-config-ts": "^0.2.4",
113
- "@did-pay/types": "1.13.95",
113
+ "@did-pay/types": "1.13.97",
114
114
  "@types/cookie-parser": "^1.4.6",
115
115
  "@types/cors": "^2.8.17",
116
116
  "@types/dotenv-flow": "^3.3.3",
@@ -149,5 +149,5 @@
149
149
  "parser": "typescript"
150
150
  }
151
151
  },
152
- "gitHead": "7cdc672350689926b95a75edc7db8e08007ab6dd"
152
+ "gitHead": "f9ca9bf2030d0a2552b71c66470cbaf3964df0f8"
153
153
  }
@@ -19,8 +19,8 @@ InvoiceTable.defaultProps = {
19
19
 
20
20
  export function getAppliedBalance(invoice: TInvoiceExpanded) {
21
21
  if (invoice.paymentMethod.type === 'stripe') {
22
- const starting = toBN(invoice.starting_balance);
23
- const ending = toBN(invoice.ending_balance);
22
+ const starting = toBN(invoice.starting_balance || '0');
23
+ const ending = toBN(invoice.ending_balance || '0');
24
24
  return ending.sub(starting).toString();
25
25
  }
26
26
 
@@ -48,7 +48,7 @@ export default function CurrentSubscriptions({ id, onChange, onClickSubscription
48
48
  }
49
49
 
50
50
  if (data && data.list.length === 0) {
51
- return <Typography color="text.secondary">{t('customer.subscription.empty')}</Typography>;
51
+ return <Typography color="text.secondary">{t('customer.subscriptions.empty')}</Typography>;
52
52
  }
53
53
 
54
54
  const hasMore = data && data.list.length < data.count;
@@ -32,7 +32,8 @@ export default function SubscriptionItemList({ data, currency, mode }: ListProps
32
32
  return (
33
33
  <Stack>
34
34
  <Typography color="text.primary" fontWeight={600}>
35
- {item?.price.product.name} - {item?.price_id}
35
+ {item?.price.product.name}
36
+ {mode === 'customer' ? '' : ` - ${item?.price_id}`}
36
37
  </Typography>
37
38
  <Typography color="text.secondary">
38
39
  {formatPrice(item.price, currency, item?.price.product.unit_label)}
@@ -58,7 +59,11 @@ export default function SubscriptionItemList({ data, currency, mode }: ListProps
58
59
  options: {
59
60
  customBodyRenderLite: (_: string, index: number) => {
60
61
  const item = data[index] as TSubscriptionItemExpanded;
61
- return item.price.recurring?.usage_type === 'metered' ? <UsageRecords id={item.id} /> : item.quantity;
62
+ return item.price.recurring?.usage_type === 'metered' ? (
63
+ <UsageRecords subscriptionId={item.subscription_id} id={item.id} />
64
+ ) : (
65
+ item.quantity
66
+ );
62
67
  },
63
68
  },
64
69
  },
@@ -9,13 +9,17 @@ import { formatToDatetime } from '../../../libs/util';
9
9
  import ConfirmDialog from '../../confirm';
10
10
  import Table from '../../table';
11
11
 
12
- const fetchData = (id: string): Promise<{ list: TUsageRecord[]; count: number }> => {
13
- return api.get(`/api/usage-records?subscription_item_id=${id}&pageSize=100`).then((res) => res.data);
12
+ const fetchData = (subscriptionId: string, id: string): Promise<{ list: TUsageRecord[]; count: number }> => {
13
+ return api
14
+ .get(`/api/subscriptions/${subscriptionId}/usage-records?subscription_item_id=${id}&pageSize=100`)
15
+ .then((res) => res.data);
14
16
  };
15
17
 
16
- export function UsageRecordDialog(props: { id: string; onConfirm: any }) {
18
+ export function UsageRecordDialog(props: { subscriptionId: string; id: string; onConfirm: any }) {
17
19
  const { t } = useLocaleContext();
18
- const { loading, error, data } = useRequest(() => fetchData(props.id), { refreshDeps: [props.id] });
20
+ const { loading, error, data } = useRequest(() => fetchData(props.subscriptionId, props.id), {
21
+ refreshDeps: [props.subscriptionId, props.id],
22
+ });
19
23
 
20
24
  if (error) {
21
25
  return (
@@ -84,7 +88,7 @@ export function UsageRecordDialog(props: { id: string; onConfirm: any }) {
84
88
  );
85
89
  }
86
90
 
87
- export default function UsageRecords({ id }: { id: string }) {
91
+ export default function UsageRecords({ subscriptionId, id }: { subscriptionId: string; id: string }) {
88
92
  const { t } = useLocaleContext();
89
93
  const [open, setOpen] = useState(false);
90
94
  return (
@@ -92,7 +96,7 @@ export default function UsageRecords({ id }: { id: string }) {
92
96
  <Button size="small" variant="text" color="info" onClick={() => setOpen(true)}>
93
97
  {t('admin.subscription.usage.view')}
94
98
  </Button>
95
- {open && <UsageRecordDialog id={id} onConfirm={() => setOpen(false)} />}
99
+ {open && <UsageRecordDialog subscriptionId={subscriptionId} id={id} onConfirm={() => setOpen(false)} />}
96
100
  </Box>
97
101
  );
98
102
  }
@@ -13,6 +13,7 @@ import InfoRow from '../../components/info-row';
13
13
  import CustomerInvoiceList from '../../components/portal/invoice/list';
14
14
  import CurrentSubscriptions from '../../components/portal/subscription/list';
15
15
  import SectionHeader from '../../components/section/header';
16
+ import { useSessionContext } from '../../contexts/session';
16
17
  import api from '../../libs/api';
17
18
  import { formatError } from '../../libs/util';
18
19
 
@@ -22,14 +23,16 @@ const fetchData = (): Promise<TCustomerExpanded> => {
22
23
 
23
24
  export default function CustomerHome() {
24
25
  const { t } = useLocaleContext();
25
- const [state, setState] = useSetState({
26
- editing: false,
27
- loading: false,
28
- });
26
+ const { events } = useSessionContext();
27
+ const [state, setState] = useSetState({ editing: false, loading: false });
29
28
  const navigate = useNavigate();
30
29
 
31
30
  const { loading, error, data, runAsync } = useRequest(fetchData);
32
31
 
32
+ events.once('switch-did', () => {
33
+ runAsync().catch(console.error);
34
+ });
35
+
33
36
  if (error) {
34
37
  return <Alert severity="error">{formatError(error)}</Alert>;
35
38
  }
@@ -117,7 +117,7 @@ export default function CustomerSubscriptionDetail() {
117
117
  </Box>
118
118
 
119
119
  <Box className="section">
120
- <SectionHeader title={t('admin.product.pricing')} mb={0} />
120
+ <SectionHeader title={t('admin.products')} mb={0} />
121
121
  <Box className="section-body">
122
122
  <SubscriptionItemList data={data.items} currency={data.paymentCurrency} mode="customer" />
123
123
  </Box>