payment-kit 1.13.32 → 1.13.34

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) {
@@ -75,7 +75,7 @@ export function formatMetadata(metadata?: Record<string, any>): Record<string, a
75
75
 
76
76
  if (Array.isArray(metadata)) {
77
77
  return metadata.reduce((acc: Record<string, any>, x: { key: string; value: any }) => {
78
- if (x.value) {
78
+ if (x.value !== '') {
79
79
  acc[x.key] = x.value;
80
80
  }
81
81
  return acc;
@@ -84,7 +84,7 @@ export function formatMetadata(metadata?: Record<string, any>): Record<string, a
84
84
 
85
85
  // remove empty values
86
86
  return Object.keys(metadata).reduce((acc: Record<string, any>, key: string) => {
87
- if (metadata[key]) {
87
+ if (metadata[key] !== '') {
88
88
  acc[key] = metadata[key];
89
89
  }
90
90
  return acc;
@@ -12,7 +12,33 @@ const router = Router();
12
12
 
13
13
  const auth = authenticate<Price>({ component: true, roles: ['owner', 'admin'] });
14
14
 
15
- // FIXME: @wangshijun use schema validation, validate product exist
15
+ export async function getExpandedPrice(id: string) {
16
+ const price = await Price.findByPkOrLookupKey(id, {
17
+ include: [
18
+ { model: Product, as: 'product' },
19
+ { model: PaymentCurrency, as: 'currency' },
20
+ ],
21
+ });
22
+
23
+ if (price) {
24
+ const currencies = await PaymentCurrency.findAll();
25
+ const doc = Price.formatAfterRead(price.toJSON(), currencies);
26
+
27
+ if (doc.upsell?.upsells_to_id) {
28
+ const to = await Price.findByPk(doc.upsell.upsells_to_id);
29
+ if (to) {
30
+ // @ts-ignore
31
+ doc.upsell.upsells_to = Price.formatAfterRead(to.toJSON(), currencies);
32
+ }
33
+ }
34
+
35
+ return doc;
36
+ }
37
+
38
+ return null;
39
+ }
40
+
41
+ // FIXME: @wangshijun use schema validation
16
42
  // create price
17
43
  // eslint-disable-next-line consistent-return
18
44
  router.post('/', auth, async (req, res) => {
@@ -23,14 +49,17 @@ router.post('/', auth, async (req, res) => {
23
49
  raw.currency_id = raw.currency_id || req.currency.id;
24
50
  raw.created_via = req.user?.via as string;
25
51
 
52
+ if (!raw.product_id) {
53
+ return res.status(400).json({ error: 'product_id is required to create a price' });
54
+ }
55
+
26
56
  if (!raw.unit_amount) {
27
57
  return res.status(400).json({ error: 'price unit_amount is required' });
28
58
  }
29
59
 
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
- ];
60
+ const product = await Product.findByPk(raw.product_id);
61
+ if (!product) {
62
+ return res.status(400).json({ error: `product ${raw.product_id} not found for price` });
34
63
  }
35
64
 
36
65
  const currencies = await PaymentCurrency.findAll({ where: { active: true } });
@@ -38,39 +67,29 @@ router.post('/', auth, async (req, res) => {
38
67
  if (!currency) {
39
68
  return res.status(400).json({ error: `currency used in price or not active: ${raw.currency_id}` });
40
69
  }
41
- raw.unit_amount = fromTokenToUnit(raw.unit_amount, currency.decimal).toString();
70
+
71
+ if (Array.isArray(raw.currency_options) === false) {
72
+ raw.currency_options = [];
73
+ }
74
+ if (raw.currency_options.some((x) => x.currency_id === raw.currency_id) === false) {
75
+ raw.currency_options.unshift({
76
+ currency_id: raw.currency_id,
77
+ unit_amount: raw.unit_amount,
78
+ tiers: null,
79
+ custom_unit_amount: null,
80
+ });
81
+ }
82
+
42
83
  raw.currency_options = Price.formatCurrencies(raw.currency_options, currencies);
84
+ raw.unit_amount = fromTokenToUnit(raw.unit_amount, currency.decimal).toString();
43
85
 
44
86
  const price = await Price.insert(raw);
45
-
46
- res.json(price);
87
+ res.json(await getExpandedPrice(price.id as string));
47
88
  });
48
89
 
49
90
  // get price detail
50
91
  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
- }
92
+ res.json(await getExpandedPrice(req.params.id as string));
74
93
  });
75
94
 
76
95
  router.get('/:id/upsell', auth, async (req, res) => {
@@ -107,7 +126,7 @@ router.put('/:id', auth, async (req, res) => {
107
126
  pick(
108
127
  req.body,
109
128
  price.locked
110
- ? ['nickname', 'description', 'metadata', 'upsell']
129
+ ? ['nickname', 'description', 'metadata', 'currency_options', 'upsell']
111
130
  : ['type', 'model', 'active', 'livemode', 'nickname', 'recurring', 'description', 'tiers', 'unit_amount', 'transform_quantity', 'metadata', 'lookup_key', 'currency_options', 'upsell'] // prettier-ignore
112
131
  )
113
132
  );
@@ -120,17 +139,16 @@ router.put('/:id', auth, async (req, res) => {
120
139
  }
121
140
 
122
141
  const currencies = await PaymentCurrency.findAll({ where: { active: true } });
142
+ const currency = currencies.find((x) => x.id === price.currency_id);
143
+ if (!currency) {
144
+ return res.status(400).json({ error: `currency used in price not found or not active: ${price.currency_id}` });
145
+ }
123
146
  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
147
  updates.unit_amount = fromTokenToUnit(updates.unit_amount, currency.decimal).toString();
129
148
  }
130
149
  if (updates.currency_options) {
131
150
  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) {
151
+ if (updates.currency_options.some((x) => x.currency_id === price.currency_id) === false) {
134
152
  updates.currency_options.unshift({
135
153
  currency_id: price.currency_id,
136
154
  unit_amount: price.unit_amount,
@@ -139,21 +157,16 @@ router.put('/:id', auth, async (req, res) => {
139
157
  });
140
158
  }
141
159
  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;
160
+ const base = price.currency_options.find((x) => x.currency_id === price.currency_id);
161
+ if (base) {
162
+ base.unit_amount = updates.unit_amount;
145
163
  }
146
164
  }
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
165
  }
153
166
 
154
167
  await price.update(Price.formatBeforeSave(updates));
155
168
 
156
- return res.json(price);
169
+ return res.json(await getExpandedPrice(req.params.id as string));
157
170
  });
158
171
 
159
172
  // archive
@@ -173,7 +186,8 @@ router.put('/:id/archive', auth, async (req, res) => {
173
186
  }
174
187
 
175
188
  await price.update({ active: false });
176
- return res.json(price);
189
+
190
+ return res.json(await getExpandedPrice(req.params.id as string));
177
191
  });
178
192
 
179
193
  // delete price
@@ -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.32
17
+ version: 1.13.34
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.32",
3
+ "version": "1.13.34",
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.32",
106
+ "@did-pay/types": "1.13.34",
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": "354fff1bd72e201fb51e8cf0ee6d26dbf5211b7c"
143
+ "gitHead": "136d14107082c64e9fdd822dc3f815cb5c8497b4"
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: '',