payment-kit 1.13.33 → 1.13.35

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.
@@ -80,7 +80,7 @@ export function getCheckoutAmount(items: TLineItemExpanded[], currency: TPayment
80
80
 
81
81
  export function getRecurringPeriod(recurring: PriceRecurring) {
82
82
  const { interval } = recurring;
83
- const count = recurring.interval_count || 1;
83
+ const count = +recurring.interval_count || 1;
84
84
  const dayInMs = 24 * 60 * 60 * 1000;
85
85
 
86
86
  switch (interval) {
@@ -1,6 +1,8 @@
1
1
  import { fromTokenToUnit } from '@ocap/util';
2
2
  import { Router } from 'express';
3
+ import Joi from 'joi';
3
4
  import pick from 'lodash/pick';
5
+ import type { WhereOptions } from 'sequelize';
4
6
 
5
7
  import { authenticate } from '../libs/security';
6
8
  import { canUpsell } from '../libs/session';
@@ -12,7 +14,33 @@ const router = Router();
12
14
 
13
15
  const auth = authenticate<Price>({ component: true, roles: ['owner', 'admin'] });
14
16
 
15
- // FIXME: @wangshijun use schema validation, validate product exist
17
+ export async function getExpandedPrice(id: string) {
18
+ const price = await Price.findByPkOrLookupKey(id, {
19
+ include: [
20
+ { model: Product, as: 'product' },
21
+ { model: PaymentCurrency, as: 'currency' },
22
+ ],
23
+ });
24
+
25
+ if (price) {
26
+ const currencies = await PaymentCurrency.findAll();
27
+ const doc = Price.formatAfterRead(price.toJSON(), currencies);
28
+
29
+ if (doc.upsell?.upsells_to_id) {
30
+ const to = await Price.findByPk(doc.upsell.upsells_to_id);
31
+ if (to) {
32
+ // @ts-ignore
33
+ doc.upsell.upsells_to = Price.formatAfterRead(to.toJSON(), currencies);
34
+ }
35
+ }
36
+
37
+ return doc;
38
+ }
39
+
40
+ return null;
41
+ }
42
+
43
+ // FIXME: @wangshijun use schema validation
16
44
  // create price
17
45
  // eslint-disable-next-line consistent-return
18
46
  router.post('/', auth, async (req, res) => {
@@ -23,14 +51,17 @@ router.post('/', auth, async (req, res) => {
23
51
  raw.currency_id = raw.currency_id || req.currency.id;
24
52
  raw.created_via = req.user?.via as string;
25
53
 
54
+ if (!raw.product_id) {
55
+ return res.status(400).json({ error: 'product_id is required to create a price' });
56
+ }
57
+
26
58
  if (!raw.unit_amount) {
27
59
  return res.status(400).json({ error: 'price unit_amount is required' });
28
60
  }
29
61
 
30
- if (raw.currency_options?.length === 0) {
31
- raw.currency_options = [
32
- { currency_id: raw.currency_id, unit_amount: raw.unit_amount, tiers: null, custom_unit_amount: null },
33
- ];
62
+ const product = await Product.findByPk(raw.product_id);
63
+ if (!product) {
64
+ return res.status(400).json({ error: `product ${raw.product_id} not found for price` });
34
65
  }
35
66
 
36
67
  const currencies = await PaymentCurrency.findAll({ where: { active: true } });
@@ -38,39 +69,29 @@ router.post('/', auth, async (req, res) => {
38
69
  if (!currency) {
39
70
  return res.status(400).json({ error: `currency used in price or not active: ${raw.currency_id}` });
40
71
  }
41
- raw.unit_amount = fromTokenToUnit(raw.unit_amount, currency.decimal).toString();
72
+
73
+ if (Array.isArray(raw.currency_options) === false) {
74
+ raw.currency_options = [];
75
+ }
76
+ if (raw.currency_options.some((x) => x.currency_id === raw.currency_id) === false) {
77
+ raw.currency_options.unshift({
78
+ currency_id: raw.currency_id,
79
+ unit_amount: raw.unit_amount,
80
+ tiers: null,
81
+ custom_unit_amount: null,
82
+ });
83
+ }
84
+
42
85
  raw.currency_options = Price.formatCurrencies(raw.currency_options, currencies);
86
+ raw.unit_amount = fromTokenToUnit(raw.unit_amount, currency.decimal).toString();
43
87
 
44
88
  const price = await Price.insert(raw);
45
-
46
- res.json(price);
89
+ res.json(await getExpandedPrice(price.id as string));
47
90
  });
48
91
 
49
92
  // get price detail
50
93
  router.get('/:id', auth, async (req, res) => {
51
- const price = await Price.findByPkOrLookupKey(req.params.id as string, {
52
- include: [
53
- { model: Product, as: 'product' },
54
- { model: PaymentCurrency, as: 'currency' },
55
- ],
56
- });
57
-
58
- if (price) {
59
- const currencies = await PaymentCurrency.findAll();
60
- const doc = Price.formatAfterRead(price.toJSON(), currencies);
61
-
62
- if (doc.upsell?.upsells_to_id) {
63
- const to = await Price.findByPk(doc.upsell.upsells_to_id);
64
- if (to) {
65
- // @ts-ignore
66
- doc.upsell.upsells_to = Price.formatAfterRead(to.toJSON(), currencies);
67
- }
68
- }
69
-
70
- res.json(doc);
71
- } else {
72
- res.json(null);
73
- }
94
+ res.json(await getExpandedPrice(req.params.id as string));
74
95
  });
75
96
 
76
97
  router.get('/:id/upsell', auth, async (req, res) => {
@@ -107,7 +128,7 @@ router.put('/:id', auth, async (req, res) => {
107
128
  pick(
108
129
  req.body,
109
130
  price.locked
110
- ? ['nickname', 'description', 'metadata', 'upsell']
131
+ ? ['nickname', 'description', 'metadata', 'currency_options', 'upsell']
111
132
  : ['type', 'model', 'active', 'livemode', 'nickname', 'recurring', 'description', 'tiers', 'unit_amount', 'transform_quantity', 'metadata', 'lookup_key', 'currency_options', 'upsell'] // prettier-ignore
112
133
  )
113
134
  );
@@ -120,17 +141,16 @@ router.put('/:id', auth, async (req, res) => {
120
141
  }
121
142
 
122
143
  const currencies = await PaymentCurrency.findAll({ where: { active: true } });
144
+ const currency = currencies.find((x) => x.id === price.currency_id);
145
+ if (!currency) {
146
+ return res.status(400).json({ error: `currency used in price not found or not active: ${price.currency_id}` });
147
+ }
123
148
  if (updates.unit_amount) {
124
- const currency = currencies.find((x) => x.id === price.currency_id);
125
- if (!currency) {
126
- return res.status(400).json({ error: `currency used in price not found or not active: ${price.currency_id}` });
127
- }
128
149
  updates.unit_amount = fromTokenToUnit(updates.unit_amount, currency.decimal).toString();
129
150
  }
130
151
  if (updates.currency_options) {
131
152
  updates.currency_options = Price.formatCurrencies(updates.currency_options, currencies);
132
- const base = updates.currency_options.find((x) => x.currency_id === price.currency_id);
133
- if (!base) {
153
+ if (updates.currency_options.some((x) => x.currency_id === price.currency_id) === false) {
134
154
  updates.currency_options.unshift({
135
155
  currency_id: price.currency_id,
136
156
  unit_amount: price.unit_amount,
@@ -139,21 +159,16 @@ router.put('/:id', auth, async (req, res) => {
139
159
  });
140
160
  }
141
161
  if (updates.unit_amount) {
142
- const exist = updates.currency_options.find((x) => x.currency_id === price.currency_id);
143
- if (exist) {
144
- exist.unit_amount = updates.unit_amount;
162
+ const base = price.currency_options.find((x) => x.currency_id === price.currency_id);
163
+ if (base) {
164
+ base.unit_amount = updates.unit_amount;
145
165
  }
146
166
  }
147
- } else if (updates.unit_amount) {
148
- const exist = price.currency_options.find((x) => x.currency_id === price.currency_id);
149
- if (exist) {
150
- exist.unit_amount = updates.unit_amount;
151
- }
152
167
  }
153
168
 
154
169
  await price.update(Price.formatBeforeSave(updates));
155
170
 
156
- return res.json(price);
171
+ return res.json(await getExpandedPrice(req.params.id as string));
157
172
  });
158
173
 
159
174
  // archive
@@ -173,7 +188,8 @@ router.put('/:id/archive', auth, async (req, res) => {
173
188
  }
174
189
 
175
190
  await price.update({ active: false });
176
- return res.json(price);
191
+
192
+ return res.json(await getExpandedPrice(req.params.id as string));
177
193
  });
178
194
 
179
195
  // delete price
@@ -199,4 +215,63 @@ router.delete('/:id', auth, async (req, res) => {
199
215
  return res.json(price);
200
216
  });
201
217
 
218
+ // list products and prices
219
+ const paginationSchema = Joi.object<{
220
+ page: number;
221
+ pageSize: number;
222
+ livemode?: boolean;
223
+ active?: boolean;
224
+ type?: string;
225
+ currency_id?: string;
226
+ product_id?: string;
227
+ lookup_key?: string;
228
+ }>({
229
+ page: Joi.number().integer().min(1).default(1),
230
+ pageSize: Joi.number().integer().min(1).max(100).default(20),
231
+ livemode: Joi.boolean().empty(''),
232
+ active: Joi.boolean().empty(''),
233
+ type: Joi.string().empty(''),
234
+ currency_id: Joi.string().empty(''),
235
+ product_id: Joi.string().empty(''),
236
+ lookup_key: Joi.string().empty(''),
237
+ });
238
+ router.get('/', auth, async (req, res) => {
239
+ const { page, pageSize, active, livemode, ...query } = await paginationSchema.validateAsync(req.query, {
240
+ stripUnknown: false,
241
+ allowUnknown: true,
242
+ });
243
+ const where: WhereOptions<Price> = {};
244
+
245
+ if (typeof active === 'boolean') {
246
+ where.active = active;
247
+ }
248
+ if (typeof livemode === 'boolean') {
249
+ where.livemode = livemode;
250
+ }
251
+ ['type', 'currency_id', 'product_id', 'lookup_key'].forEach((key: string) => {
252
+ // @ts-ignore
253
+ if (query[key]) {
254
+ // @ts-ignore
255
+ where[key] = query[key].split(',').map((x: string) => x.trim()).filter(Boolean); // prettier-ignore
256
+ }
257
+ });
258
+
259
+ Object.keys(query)
260
+ .filter((x) => x.startsWith('recurring.'))
261
+ .forEach((key: string) => {
262
+ // @ts-ignore
263
+ where[key] = query[key];
264
+ });
265
+
266
+ const { rows, count } = await Price.findAndCountAll({
267
+ where,
268
+ attributes: ['id'],
269
+ order: [['created_at', 'DESC']],
270
+ offset: (page - 1) * pageSize,
271
+ limit: pageSize,
272
+ });
273
+
274
+ res.json({ count, list: await Promise.all(rows.map((x) => getExpandedPrice(x.id))) });
275
+ });
276
+
202
277
  export default router;
@@ -63,12 +63,14 @@ router.post('/', auth, async (req, res) => {
63
63
  } else {
64
64
  price.currency_options = [];
65
65
  }
66
- price.currency_options.unshift({
67
- currency_id: price.currency_id,
68
- unit_amount: price.unit_amount,
69
- tiers: null,
70
- custom_unit_amount: null,
71
- });
66
+ if (price.currency_options.some((x) => x.currency_id === price.currency_id) === false) {
67
+ price.currency_options.unshift({
68
+ currency_id: price.currency_id,
69
+ unit_amount: price.unit_amount,
70
+ tiers: null,
71
+ custom_unit_amount: null,
72
+ });
73
+ }
72
74
 
73
75
  return price;
74
76
  });
@@ -138,10 +140,9 @@ router.get('/', auth, async (req, res) => {
138
140
  res.json({ count, list });
139
141
  });
140
142
 
141
- // get product detail
142
- router.get('/:id', auth, async (req, res) => {
143
+ export async function getExpandedProduct(id: string) {
143
144
  const product = await Product.findOne({
144
- where: { id: req.params.id },
145
+ where: { id },
145
146
  include: [
146
147
  { model: Price, as: 'prices', include: [{ model: PaymentCurrency, as: 'currency' }] },
147
148
  { model: Price, as: 'default_price' },
@@ -169,10 +170,16 @@ router.get('/:id', auth, async (req, res) => {
169
170
  ];
170
171
  }
171
172
  }
172
- res.json(doc);
173
- } else {
174
- res.json(null);
173
+
174
+ return doc;
175
175
  }
176
+
177
+ return null;
178
+ }
179
+
180
+ // get product detail
181
+ router.get('/:id', auth, async (req, res) => {
182
+ res.json(await getExpandedProduct(req.params.id as string));
176
183
  });
177
184
 
178
185
  // update product
@@ -204,7 +211,7 @@ router.put('/:id', auth, async (req, res) => {
204
211
  }
205
212
  await product.update(updates);
206
213
 
207
- return res.json(product);
214
+ return res.json(await getExpandedProduct(req.params.id as string));
208
215
  });
209
216
 
210
217
  // archive
@@ -221,8 +228,7 @@ router.put('/:id/archive', auth, async (req, res) => {
221
228
  await product.update({ active: !product.active });
222
229
 
223
230
  // FIXME: deactivate payment-links, pricing-tables
224
-
225
- return res.json(product);
231
+ return res.json(await getExpandedProduct(req.params.id as string));
226
232
  });
227
233
 
228
234
  // delete product
@@ -355,7 +355,7 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
355
355
  }
356
356
 
357
357
  // @ts-ignore
358
- return this.create(this.formatBeforeSave(price));
358
+ return this.create(this.formatBeforeSave({ model: 'standard', ...price }));
359
359
  }
360
360
 
361
361
  public static findByPkOrLookupKey(id: string, options: FindOptions<Price> = {}) {
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.33
17
+ version: 1.13.35
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.33",
3
+ "version": "1.13.35",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev",
6
6
  "eject": "vite eject",
@@ -103,7 +103,7 @@
103
103
  "@abtnode/types": "1.16.17-beta-952ef53d",
104
104
  "@arcblock/eslint-config": "^0.2.4",
105
105
  "@arcblock/eslint-config-ts": "^0.2.4",
106
- "@did-pay/types": "1.13.33",
106
+ "@did-pay/types": "1.13.35",
107
107
  "@types/cookie-parser": "^1.4.4",
108
108
  "@types/cors": "^2.8.14",
109
109
  "@types/dotenv-flow": "^3.3.1",
@@ -140,5 +140,5 @@
140
140
  "parser": "typescript"
141
141
  }
142
142
  },
143
- "gitHead": "d22dad06dbe2d08c06279f3d4a66c666f2542255"
143
+ "gitHead": "25a3e75f4894d8aefb016605548c58645c0ca194"
144
144
  }
@@ -61,12 +61,12 @@ export default function ProductItem({ item, session, currency, onUpsell, onDowns
61
61
  onChange={() => onUpsell(item.price_id, item.price.upsell?.upsells_to_id)}
62
62
  />
63
63
  {t('checkout.upsell.save', {
64
- recurring: formatRecurring(item.price.upsell.upsells_to.recurring as PriceRecurring),
64
+ recurring: t(`common.${formatRecurring(item.price.upsell.upsells_to.recurring as PriceRecurring)}`),
65
65
  })}
66
- <Status label={t('checkout.upsell.off', { saving })} color="primary" variant="outlined" sx={{ ml: 0.5 }} />
66
+ <Status label={t('checkout.upsell.off', { saving })} color="primary" variant="outlined" sx={{ ml: 1 }} />
67
67
  </Typography>
68
68
  <Typography component="span" sx={{ fontSize: 12 }}>
69
- {formatPrice(item.price.upsell.upsells_to, currency)}
69
+ {formatPrice(item.price.upsell.upsells_to, currency, item.price.product?.unit_label)}
70
70
  </Typography>
71
71
  </Stack>
72
72
  )}
@@ -88,11 +88,11 @@ export default function ProductItem({ item, session, currency, onUpsell, onDowns
88
88
  onChange={() => onDownsell(item.upsell_price_id)}
89
89
  />
90
90
  {t('checkout.upsell.revert', {
91
- recurring: formatRecurring(item.price.recurring as PriceRecurring),
91
+ recurring: t(`common.${formatRecurring(item.price.recurring as PriceRecurring)}`),
92
92
  })}
93
93
  </Typography>
94
94
  <Typography component="span" sx={{ fontSize: 12 }}>
95
- {formatPrice(item.price, currency)}
95
+ {formatPrice(item.price, currency, item.price.product?.unit_label)}
96
96
  </Typography>
97
97
  </Stack>
98
98
  )}
@@ -20,8 +20,6 @@ export default function MetadataEditor({
20
20
  const metadata = data.metadata || {};
21
21
  const methods = useForm<any>({
22
22
  defaultValues: {
23
- ...data,
24
- // @ts-ignore
25
23
  metadata: Object.keys(metadata).map((key: string) => ({ key, value: metadata[key] })),
26
24
  },
27
25
  });
@@ -12,13 +12,14 @@ import AddPrice from '../product/add-price';
12
12
  type Props = {
13
13
  price: TPriceExpanded;
14
14
  onSelect: Function;
15
+ onAdd: Function;
15
16
  };
16
17
 
17
18
  const fetchData = (id: string): Promise<TPriceExpanded[]> => {
18
19
  return api.get(`/api/prices/${id}/upsell`).then((res) => res.data);
19
20
  };
20
21
 
21
- export default function UpsellSelect({ price, onSelect }: Props) {
22
+ export default function UpsellSelect({ price, onSelect, onAdd }: Props) {
22
23
  const { t } = useLocaleContext();
23
24
  const [state, setState] = useSetState({ loading: false, adding: false, action: '' });
24
25
 
@@ -47,6 +48,7 @@ export default function UpsellSelect({ price, onSelect }: Props) {
47
48
  setState({ adding: true });
48
49
  await api.post('/api/prices', { ...formData, product_id: price.product_id });
49
50
  Toast.success(t('common.saved'));
51
+ onAdd();
50
52
  } catch (err) {
51
53
  console.error(err);
52
54
  Toast.error(formatError(err));
@@ -58,7 +58,7 @@ export function UpsellForm({ data, onChange }: { data: TPriceExpanded; onChange:
58
58
  );
59
59
  }
60
60
 
61
- return <UpsellSelect price={data} onSelect={onSelectUpsell} />;
61
+ return <UpsellSelect price={data} onSelect={onSelectUpsell} onAdd={onChange} />;
62
62
  }
63
63
 
64
64
  export default function PriceUpsell({ data, onChange }: { data: TPriceExpanded; onChange: Function }) {
package/src/libs/util.ts CHANGED
@@ -332,9 +332,36 @@ export function formatPaymentLinkPricing(link: TPaymentLinkExpanded, currency: T
332
332
  currency
333
333
  );
334
334
  }
335
+ export function getRecurringPeriod(recurring: PriceRecurring) {
336
+ const { interval } = recurring;
337
+ const count = +recurring.interval_count || 1;
338
+ const dayInMs = 24 * 60 * 60 * 1000;
339
+
340
+ switch (interval) {
341
+ case 'hour':
342
+ return 60 * 60 * 1000;
343
+ case 'day':
344
+ return count * dayInMs;
345
+ case 'week':
346
+ return count * 7 * dayInMs;
347
+ case 'month':
348
+ return count * 30 * dayInMs;
349
+ case 'year':
350
+ return count * 365 * dayInMs;
351
+ default:
352
+ throw new Error(`Unsupported recurring interval: ${interval}`);
353
+ }
354
+ }
335
355
 
336
356
  export function formatUpsellSaving(session: TCheckoutSessionExpanded, currency: TPaymentCurrency) {
337
357
  const items = session.line_items as TLineItemExpanded[];
358
+ if (items[0]?.upsell_price_id) {
359
+ return '0';
360
+ }
361
+ if (!items[0]?.price.upsell?.upsells_to) {
362
+ return '0';
363
+ }
364
+
338
365
  const from = getCheckoutAmount(items, currency, false, false);
339
366
  const to = getCheckoutAmount(
340
367
  items.map((x) => ({
@@ -347,11 +374,18 @@ export function formatUpsellSaving(session: TCheckoutSessionExpanded, currency:
347
374
  true
348
375
  );
349
376
 
350
- const factor = 12; // FIXME: interval
377
+ const fromRecurring = items[0].price?.recurring as PriceRecurring;
378
+ const toRecurring = items[0].price?.upsell?.upsells_to?.recurring as PriceRecurring;
379
+ if (!fromRecurring || !toRecurring) {
380
+ return '0';
381
+ }
382
+
383
+ const factor = Math.floor(getRecurringPeriod(toRecurring) / getRecurringPeriod(fromRecurring));
384
+
351
385
  const before = new BN(from.total).mul(new BN(factor));
352
386
  const after = new BN(to.total);
353
387
 
354
- return Number(before.sub(after).mul(new BN(100)).div(after).toString()).toFixed(0);
388
+ return Number(before.sub(after).mul(new BN(100)).div(before).toString()).toFixed(0);
355
389
  }
356
390
 
357
391
  export function formatCheckoutHeadlines(
@@ -501,15 +501,15 @@ export default flat({
501
501
  },
502
502
  customer: {
503
503
  name: '姓名',
504
- email: '电子邮件',
504
+ email: '邮件',
505
505
  phone: '电话',
506
506
  phonePlaceholder: '电话号码',
507
507
  phoneTip: '以防需要与您联系有关您的订单',
508
508
  },
509
509
  upsell: {
510
- save: '使用{recurring}计费方式节省',
510
+ save: '使用{recurring}计费方式',
511
511
  revert: '切换到{recurring}计费方式',
512
- off: '{saving}%折扣',
512
+ off: '{saving}%',
513
513
  },
514
514
  expired: {
515
515
  title: '链接已过期',
@@ -26,7 +26,7 @@ export default function PriceActions(props: Props) {
26
26
 
27
27
  const canEdit = props.data.active;
28
28
  const canArchive = props.data.active;
29
- const canRemove = !props.data.locked && props.setAsDefault;
29
+ const canRemove = !props.data.locked && !props.setAsDefault;
30
30
 
31
31
  const [state, setState] = useSetState({
32
32
  action: '',