payment-kit 1.13.301 → 1.13.302

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.
@@ -38,6 +38,21 @@ type PaymentJob = {
38
38
  retryOnError?: boolean;
39
39
  };
40
40
 
41
+ async function updateQuantitySold(checkoutSession: CheckoutSession) {
42
+ const updatePromises = checkoutSession.line_items.map((item) => {
43
+ const priceId = item.upsell_price_id || item.price_id;
44
+ return Price.increment({ quantity_sold: Number(item.quantity) }, { where: { id: priceId } }).catch((err) => {
45
+ logger.error('Update quantity_sold failed', {
46
+ error: err,
47
+ priceId,
48
+ checkoutSessionId: checkoutSession.id,
49
+ });
50
+ });
51
+ });
52
+
53
+ await Promise.all(updatePromises);
54
+ }
55
+
41
56
  export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
42
57
  // FIXME: @wangshijun we should check stripe payment here before
43
58
 
@@ -194,6 +209,12 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
194
209
  if (invoice.checkout_session_id) {
195
210
  const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
196
211
  if (checkoutSession && checkoutSession.status === 'open') {
212
+ updateQuantitySold(checkoutSession).catch((err) => {
213
+ logger.error('Updating quantity_sold for line items failed', {
214
+ error: err,
215
+ checkoutSessionId: checkoutSession.id,
216
+ });
217
+ });
197
218
  await checkoutSession.update({
198
219
  status: 'complete',
199
220
  payment_status: 'paid',
@@ -77,6 +77,52 @@ const getPaymentTypes = async (items: any[]) => {
77
77
  return methods.map((x) => x.type);
78
78
  };
79
79
 
80
+ export async function validateInventory(line_items: LineItem[], includePendingQuantity = false) {
81
+ const checks = line_items.map(async (item) => {
82
+ const priceId = item.price_id;
83
+ const quantity = Number(item.quantity || 0);
84
+
85
+ const price = await Price.findOne({ where: { id: priceId } });
86
+
87
+ let delta = quantity;
88
+
89
+ if (!price) {
90
+ throw new Error(`Price not found for priceId: ${priceId}`);
91
+ }
92
+
93
+ if (!price.quantity_available) {
94
+ // if quantity_available equal to 0 , we assume it's unlimited
95
+ return true;
96
+ }
97
+
98
+ if (Number(price.quantity_limit_per_checkout) > 0 && quantity > Number(price.quantity_limit_per_checkout)) {
99
+ throw new Error(`Can not exceed per checkout quantity for price: ${priceId}`);
100
+ }
101
+
102
+ delta += price.quantity_sold;
103
+ if (includePendingQuantity) {
104
+ const checkoutSessions = await CheckoutSession.findAll({
105
+ where: {
106
+ status: 'open',
107
+ },
108
+ attributes: ['line_items'],
109
+ });
110
+ const pendingQuantity = checkoutSessions.reduce((acc, session: any) => {
111
+ const lineItems = session.line_items || [];
112
+ const sessionQuantity = lineItems
113
+ .filter((lineItem: any) => lineItem.priceId === priceId)
114
+ .reduce((total: number, lineItem: any) => total + Number(lineItem.quantity), 0);
115
+ return acc + sessionQuantity;
116
+ }, 0);
117
+ delta += pendingQuantity;
118
+ }
119
+ if (delta > Number(price.quantity_available)) {
120
+ throw new Error(`Can not exceed available quantity for price: ${priceId}`);
121
+ }
122
+ });
123
+ await Promise.all(checks);
124
+ }
125
+
80
126
  export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = true) => {
81
127
  const raw: Partial<CheckoutSession> = Object.assign(
82
128
  {
@@ -288,6 +334,14 @@ router.post('/', auth, async (req, res) => {
288
334
  const raw: Partial<CheckoutSession> = await formatCheckoutSession(req.body);
289
335
  raw.livemode = !!req.livemode;
290
336
  raw.created_via = req.user?.via as string;
337
+ if (raw.line_items) {
338
+ try {
339
+ await validateInventory(raw.line_items, true);
340
+ } catch (err) {
341
+ logger.error('validateInventory failed', { error: err, line_items: raw.line_items });
342
+ return res.status(400).json({ error: err.message });
343
+ }
344
+ }
291
345
 
292
346
  const doc = await CheckoutSession.create(raw as any);
293
347
 
@@ -460,7 +514,18 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
460
514
  }
461
515
 
462
516
  const checkoutSession = req.doc as CheckoutSession;
463
-
517
+ if (checkoutSession.line_items) {
518
+ try {
519
+ await validateInventory(checkoutSession.line_items);
520
+ } catch (err) {
521
+ logger.error('validateInventory failed', {
522
+ error: err,
523
+ line_items: checkoutSession.line_items,
524
+ checkoutSessionId: checkoutSession.id,
525
+ });
526
+ return res.status(400).json({ error: err.message });
527
+ }
528
+ }
464
529
  // validate cross sell
465
530
  if (checkoutSession.cross_sell_behavior === 'required') {
466
531
  if (checkoutSession.line_items.some((x) => x.cross_sell) === false) {
@@ -167,16 +167,28 @@ export async function createPrice(payload: any) {
167
167
  return getExpandedPrice(price.id as string);
168
168
  }
169
169
 
170
+ const priceQuantitySchema = Joi.object({
171
+ quantity_available: Joi.number().integer().min(0).optional().default(0),
172
+ quantity_limit_per_checkout: Joi.number().integer().min(0).optional().default(0),
173
+ });
174
+
170
175
  // FIXME: @wangshijun use schema validation
171
176
  // create price
172
177
  // eslint-disable-next-line consistent-return
173
178
  router.post('/', auth, async (req, res) => {
174
179
  try {
180
+ const { error } = priceQuantitySchema.validate(
181
+ pick(req.body, ['quantity_available', 'quantity_limit_per_checkout'])
182
+ );
183
+ if (error) {
184
+ return res.status(400).json({ error: `Price create request invalid: ${error.message}` });
185
+ }
175
186
  const result = await createPrice({
176
187
  ...req.body,
177
188
  livemode: !!req.livemode,
178
189
  currency_id: req.body.currency_id || req.currency.id,
179
190
  created_via: req.user?.via as string,
191
+ quantity_sold: 0,
180
192
  });
181
193
 
182
194
  res.json(result);
@@ -231,6 +243,11 @@ router.get('/:id/upsell', auth, async (req, res) => {
231
243
  // update price
232
244
  // FIXME: upsell validate https://stripe.com/docs/payments/checkout/upsells
233
245
  router.put('/:id', auth, async (req, res) => {
246
+ const quantityKeys = ['quantity_available', 'quantity_limit_per_checkout'];
247
+ const { error } = priceQuantitySchema.validate(pick(req.body, quantityKeys));
248
+ if (error) {
249
+ return res.status(400).json({ error: `Price update request invalid: ${error.message}` });
250
+ }
234
251
  const doc = await Price.findByPkOrLookupKey(req.params.id as string);
235
252
 
236
253
  if (!doc) {
@@ -241,13 +258,18 @@ router.put('/:id', auth, async (req, res) => {
241
258
  return res.status(403).json({ error: 'price archived' });
242
259
  }
243
260
 
261
+ if (Number(req.body.quantity_available) > 0 && Number(req.body.quantity_available) < Number(doc.quantity_sold)) {
262
+ // 可售数量不得小于已售数量
263
+ return res.status(400).json({ error: 'the available quantity cannot be less than the quantity sold' });
264
+ }
265
+
244
266
  const locked = doc.locked && process.env.PAYMENT_CHANGE_LOCKED_PRICE !== '1';
245
267
  const updates: Partial<Price> = Price.formatBeforeSave(
246
268
  pick(
247
269
  req.body,
248
270
  locked
249
- ? ['nickname', 'description', 'metadata', 'currency_options', 'upsell']
250
- : ['type', 'model', 'active', 'livemode', 'nickname', 'recurring', 'description', 'tiers', 'unit_amount', 'transform_quantity', 'metadata', 'lookup_key', 'currency_options', 'upsell'] // prettier-ignore
271
+ ? ['nickname', 'description', 'metadata', 'currency_options', 'upsell', ...quantityKeys]
272
+ : ['type', 'model', 'active', 'livemode', 'nickname', 'recurring', 'description', 'tiers', 'unit_amount', 'transform_quantity', 'metadata', 'lookup_key', 'currency_options', 'upsell', ...quantityKeys] // prettier-ignore
251
273
  )
252
274
  );
253
275
 
@@ -0,0 +1,41 @@
1
+ /* eslint-disable no-await-in-loop */
2
+ import { DataTypes } from 'sequelize';
3
+
4
+ import { Migration, safeApplyColumnChanges } from '../migrate';
5
+
6
+ export const up: Migration = async ({ context }) => {
7
+ await safeApplyColumnChanges(context, {
8
+ prices: [
9
+ {
10
+ name: 'quantity_available',
11
+ field: {
12
+ type: DataTypes.INTEGER,
13
+ defaultValue: 0,
14
+ allowNull: false,
15
+ },
16
+ },
17
+ {
18
+ name: 'quantity_sold',
19
+ field: {
20
+ type: DataTypes.INTEGER,
21
+ defaultValue: 0,
22
+ allowNull: false,
23
+ },
24
+ },
25
+ {
26
+ name: 'quantity_limit_per_checkout',
27
+ field: {
28
+ type: DataTypes.INTEGER,
29
+ defaultValue: 0,
30
+ allowNull: false,
31
+ },
32
+ },
33
+ ],
34
+ });
35
+ };
36
+
37
+ export const down: Migration = async ({ context }) => {
38
+ await context.removeColumn('prices', 'quantity_available');
39
+ await context.removeColumn('prices', 'quantity_sold');
40
+ await context.removeColumn('prices', 'quantity_limit_per_checkout');
41
+ };
@@ -90,6 +90,13 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
90
90
  declare created_via: LiteralUnion<'api' | 'dashboard' | 'portal', string>;
91
91
  declare updated_at: CreationOptional<Date>;
92
92
 
93
+ // Quantity available for purchase, 0 means no limit
94
+ declare quantity_available: number;
95
+ // Quantity has been sold
96
+ declare quantity_sold: number;
97
+ // Quantity limit per checkout, 0 means no limit
98
+ declare quantity_limit_per_checkout: number;
99
+
93
100
  public static readonly GENESIS_ATTRIBUTES = {
94
101
  id: {
95
102
  type: DataTypes.STRING(32),
@@ -187,6 +194,21 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
187
194
  type: DataTypes.JSON,
188
195
  allowNull: true,
189
196
  },
197
+ quantity_available: {
198
+ type: DataTypes.INTEGER,
199
+ defaultValue: 0,
200
+ allowNull: false,
201
+ },
202
+ quantity_sold: {
203
+ type: DataTypes.INTEGER,
204
+ defaultValue: 0,
205
+ allowNull: false,
206
+ },
207
+ quantity_limit_per_checkout: {
208
+ type: DataTypes.INTEGER,
209
+ defaultValue: 0,
210
+ allowNull: false,
211
+ },
190
212
  },
191
213
  {
192
214
  sequelize,
@@ -280,6 +302,18 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
280
302
  );
281
303
  }
282
304
 
305
+ if (price.quantity_available) {
306
+ price.quantity_available = Number(price.quantity_available);
307
+ }
308
+
309
+ if (price.quantity_limit_per_checkout) {
310
+ price.quantity_limit_per_checkout = Number(price.quantity_limit_per_checkout);
311
+ }
312
+
313
+ if (price.quantity_sold) {
314
+ price.quantity_sold = Number(price.quantity_sold);
315
+ }
316
+
283
317
  return price;
284
318
  }
285
319
 
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.301
17
+ version: 1.13.302
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.301",
3
+ "version": "1.13.302",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -52,7 +52,7 @@
52
52
  "@arcblock/validator": "^1.18.124",
53
53
  "@blocklet/js-sdk": "1.16.28",
54
54
  "@blocklet/logger": "1.16.28",
55
- "@blocklet/payment-react": "1.13.301",
55
+ "@blocklet/payment-react": "1.13.302",
56
56
  "@blocklet/sdk": "1.16.28",
57
57
  "@blocklet/ui-react": "^2.10.3",
58
58
  "@blocklet/uploader": "^0.1.18",
@@ -118,7 +118,7 @@
118
118
  "devDependencies": {
119
119
  "@abtnode/types": "1.16.28",
120
120
  "@arcblock/eslint-config-ts": "^0.3.2",
121
- "@blocklet/payment-types": "1.13.301",
121
+ "@blocklet/payment-types": "1.13.302",
122
122
  "@types/cookie-parser": "^1.4.7",
123
123
  "@types/cors": "^2.8.17",
124
124
  "@types/debug": "^4.1.12",
@@ -160,5 +160,5 @@
160
160
  "parser": "typescript"
161
161
  }
162
162
  },
163
- "gitHead": "51232b3901a744f8340c345dc738552f9df899a2"
163
+ "gitHead": "366468bdbb1c5e8837a3baff852067fa041609a1"
164
164
  }
package/scripts/sdk.js CHANGED
@@ -17,13 +17,13 @@ const payment = require('@blocklet/payment-js').default;
17
17
  // });
18
18
  // console.log('refundResult', refundResult);
19
19
 
20
- const refund = await payment.refunds.retrieve('re_loHv143R78cSe38uGjxRBsfv');
21
- console.log('🚀 ~ refund:', refund);
20
+ // const refund = await payment.refunds.retrieve('re_loHv143R78cSe38uGjxRBsfv');
21
+ // console.log('🚀 ~ refund:', refund);
22
22
 
23
- const refunds = await payment.refunds.list({
24
- invoice_id: 'in_EidRR3yL3PL5tnOgd17eyGJr',
25
- });
26
- console.log('🚀 ~ refunds:', refunds);
23
+ // const refunds = await payment.refunds.list({
24
+ // invoice_id: paymentIntent.invoice_id,
25
+ // });
26
+ // console.log('🚀 ~ refunds:', refunds);
27
27
 
28
28
  // const customRefundResult = await payment.refunds.create({
29
29
  // amount: '0.001',
@@ -37,5 +37,44 @@ const payment = require('@blocklet/payment-js').default;
37
37
  // });
38
38
  // console.log('🚀 ~ customRefundResult:', customRefundResult);
39
39
 
40
+ // const price = await payment.prices.create({
41
+ // locked: false,
42
+ // model: 'standard',
43
+ // billing_scheme: '',
44
+ // currency_id: 'pc_aW2zy2y8yoi7',
45
+ // nickname: '',
46
+ // type: 'recurring',
47
+ // unit_amount: '0.001',
48
+ // lookup_key: '',
49
+ // recurring: {
50
+ // interval_config: 'month_1',
51
+ // interval: 'month',
52
+ // interval_count: 1,
53
+ // usage_type: 'licensed',
54
+ // aggregate_usage: 'sum',
55
+ // },
56
+ // transform_quantity: { divide_by: 1, round: 'up' },
57
+ // tiers: [],
58
+ // metadata: [],
59
+ // custom_unit_amount: null,
60
+ // currency_options: [],
61
+ // tiers_mode: null,
62
+ // quantity_available: 10,
63
+ // quantity_limit_per_checkout: '2',
64
+ // product_id: 'prod_fSuzmRV137Qxmt',
65
+ // });
66
+ // console.log('🚀 ~ price:', price);
67
+
68
+ // const checkoutSession = await payment.checkout.sessions.create({
69
+ // success_url:
70
+ // 'https://bbqa2t5pfyfroyobmzknmktshckzto4btkfagxyjqwy.did.abtnet.io/store/api/payment/success?redirect=https://bbqa2t5pfyfroyobmzknmktshckzto4btkfagxyjqwy.did.abtnet.io/maker/mint/z3CtLCSchoZj4H5gqwyATSAEdvfd1m88VnDUZ',
71
+ // cancel_url:
72
+ // 'https://bbqa2t5pfyfroyobmzknmktshckzto4btkfagxyjqwy.did.abtnet.io/store/api/payment/cancel?redirect=https://bbqa2t5pfyfroyobmzknmktshckzto4btkfagxyjqwy.did.abtnet.io/maker/mint/z3CtLCSchoZj4H5gqwyATSAEdvfd1m88VnDUZ',
73
+ // mode: 'payment',
74
+ // line_items: [{ price_id: 'price_wc1WPJy7FrbX1CBPJj7zuIys', quantity: 2 }],
75
+ // metadata: { factoryAddress: 'z3CtLCSchoZj4H5gqwyATSAEdvfd1m88VnDUZ', quantity: 2 },
76
+ // expires_at: 1721121607,
77
+ // });
78
+ // console.log('checkoutSession', checkoutSession);
40
79
  process.exit(0);
41
80
  })();
@@ -34,7 +34,7 @@ export type Price = Omit<InferFormType<TPriceExpanded>, 'product_id' | 'object'>
34
34
  };
35
35
  };
36
36
 
37
- export const DEFAULT_PRICE: Omit<Price, 'product' | 'currency'> = {
37
+ export const DEFAULT_PRICE: Omit<Price, 'product' | 'currency' | 'quantity_sold'> = {
38
38
  locked: false,
39
39
  model: 'standard',
40
40
  billing_scheme: '',
@@ -59,6 +59,8 @@ export const DEFAULT_PRICE: Omit<Price, 'product' | 'currency'> = {
59
59
  custom_unit_amount: null,
60
60
  currency_options: [],
61
61
  tiers_mode: null,
62
+ quantity_available: 0,
63
+ quantity_limit_per_checkout: 0,
62
64
  };
63
65
 
64
66
  type PriceFormProps = {
@@ -92,6 +94,7 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
92
94
  const isCustomInterval = useWatch({ control, name: getFieldName('recurring.interval_config') }) === 'month_2';
93
95
  const model = useWatch({ control, name: getFieldName('model') });
94
96
  const positive = (v: number) => v >= 0;
97
+ const quantityPositive = (v: number | undefined) => !v || v.toString().match(/^(0|[1-9]\d*)$/);
95
98
 
96
99
  const basePaymentMethod = settings.paymentMethods.find((x) =>
97
100
  x.payment_currencies.some((c) => c.id === settings.baseCurrency.id)
@@ -365,6 +368,42 @@ export default function PriceForm({ prefix, simple }: PriceFormProps) {
365
368
  {!simple && (
366
369
  <Collapse trigger={t('admin.price.additional')} expanded={isLocked}>
367
370
  <Stack spacing={2} alignItems="flex-start">
371
+ <Controller
372
+ name={getFieldName('quantity_available')}
373
+ control={control}
374
+ render={({ field }) => (
375
+ <Box>
376
+ <FormLabel>{t('admin.price.quantityAvailable.label')}</FormLabel>
377
+ <TextField
378
+ {...field}
379
+ size="small"
380
+ sx={{ width: INPUT_WIDTH }}
381
+ type="number"
382
+ placeholder={t('admin.price.quantityAvailable.placeholder')}
383
+ error={!quantityPositive(field.value)}
384
+ helperText={!quantityPositive(field.value) && t('admin.price.quantity.tip')}
385
+ />
386
+ </Box>
387
+ )}
388
+ />
389
+ <Controller
390
+ name={getFieldName('quantity_limit_per_checkout')}
391
+ control={control}
392
+ render={({ field }) => (
393
+ <Box>
394
+ <FormLabel>{t('admin.price.quantityLimitPerCheckout.label')}</FormLabel>
395
+ <TextField
396
+ {...field}
397
+ size="small"
398
+ sx={{ width: INPUT_WIDTH }}
399
+ type="number"
400
+ placeholder={t('admin.price.quantityLimitPerCheckout.placeholder')}
401
+ error={!quantityPositive(field.value)}
402
+ helperText={!quantityPositive(field.value) && t('admin.price.quantity.tip')}
403
+ />
404
+ </Box>
405
+ )}
406
+ />
368
407
  <Controller
369
408
  name={getFieldName('nickname')}
370
409
  control={control}
@@ -5,6 +5,7 @@ import { Button, CircularProgress, Stack } from '@mui/material';
5
5
  import { fromUnitToToken } from '@ocap/util';
6
6
  import { cloneDeep, isEmpty } from 'lodash';
7
7
  import type { EventHandler } from 'react';
8
+ import Toast from '@arcblock/ux/lib/Toast';
8
9
  import { FormProvider, useForm } from 'react-hook-form';
9
10
 
10
11
  import { getPricingModel } from '../../libs/util';
@@ -48,6 +49,13 @@ export default function EditPrice({
48
49
  const { handleSubmit, reset } = methods;
49
50
  const onSubmit = () => {
50
51
  handleSubmit(async (formData: any) => {
52
+ if (
53
+ Number(formData.quantity_available) > 0 &&
54
+ Number(formData.quantity_available) < Number(formData.quantity_sold)
55
+ ) {
56
+ Toast.warning(t('admin.price.quantityAvailable.valid'));
57
+ return;
58
+ }
51
59
  await onSave(formData);
52
60
  reset();
53
61
  onCancel(null);
@@ -165,6 +165,27 @@ export default flat({
165
165
  to: 'Upsells to',
166
166
  tip: '',
167
167
  },
168
+ quantity: {
169
+ tip: 'Quantity must be equal or greater than 0',
170
+ },
171
+ quantityAvailable: {
172
+ label: 'Available quantity',
173
+ placeholder: '0 means unlimited',
174
+ format: 'Available {num} pieces',
175
+ noLimit: 'No limit on available quantity',
176
+ valid: 'Available quantity must be greater than or equal to sold quantity',
177
+ },
178
+ quantitySold: {
179
+ label: 'Sold quantity',
180
+ format: 'Sold {num} pieces',
181
+ },
182
+ quantityLimitPerCheckout: {
183
+ label: 'Limit per checkout quantity',
184
+ placeholder: '0 means unlimited',
185
+ format: 'Limit {num} pieces per checkout',
186
+ noLimit: 'No limit on quantity per checkout',
187
+ },
188
+ inventory: 'Inventory Settings',
168
189
  },
169
190
  coupon: {
170
191
  create: 'Create Coupon',
@@ -161,6 +161,27 @@ export default flat({
161
161
  to: '可升级至',
162
162
  tip: '',
163
163
  },
164
+ quantity: {
165
+ tip: '数量必须是自然数',
166
+ },
167
+ quantityAvailable: {
168
+ label: '可售数量',
169
+ placeholder: '0表示无限制',
170
+ format: '可售{num}件',
171
+ noLimit: '不限制可售数量',
172
+ valid: '可售数量不得少于已售数量',
173
+ },
174
+ quantitySold: {
175
+ label: '已售数量',
176
+ format: '已售{num}件',
177
+ },
178
+ quantityLimitPerCheckout: {
179
+ label: '单次购买最大数量',
180
+ placeholder: '0表示无限制',
181
+ format: '单次最多购买{num}件',
182
+ noLimit: '不限制单次购买数量',
183
+ },
184
+ inventory: '库存设置',
164
185
  },
165
186
  coupon: {
166
187
  create: '创建优惠券',
@@ -35,6 +35,10 @@ export default function PriceActions({ data, onChange, variant, setAsDefault }:
35
35
  });
36
36
 
37
37
  const onEditPrice = async (updates: TPrice) => {
38
+ if (Number(updates.quantity_available) > 0 && Number(updates.quantity_available) < Number(updates.quantity_sold)) {
39
+ Toast.warning(t('admin.price.quantityAvailable.valid'));
40
+ return;
41
+ }
38
42
  try {
39
43
  setState({ loading: true });
40
44
  await api.put(`/api/prices/${data.id}`, updates).then((res) => res.data);
@@ -71,6 +71,26 @@ export default function PricesList({ product, onChange }: { product: Product; on
71
71
  },
72
72
  },
73
73
  },
74
+ {
75
+ label: t('admin.price.inventory'),
76
+ name: 'id',
77
+ options: {
78
+ sort: false,
79
+ customBodyRenderLite: (_: any, index: number) => {
80
+ const price = product.prices[index] as any;
81
+ let additional = '';
82
+ if (price.nickname) {
83
+ additional = `${price.nickname}:`;
84
+ }
85
+ additional += `${price.quantity_available ? t('admin.price.quantityAvailable.format', { num: price.quantity_available }) : t('admin.price.quantityAvailable.noLimit')}`;
86
+ additional += ` / ${t('admin.price.quantitySold.format', {
87
+ num: price.quantity_sold,
88
+ })}`;
89
+ additional += ` / ${price.quantity_limit_per_checkout ? t('admin.price.quantityLimitPerCheckout.format', { num: price.quantity_limit_per_checkout }) : t('admin.price.quantityLimitPerCheckout.noLimit')}`;
90
+ return additional;
91
+ },
92
+ },
93
+ },
74
94
  {
75
95
  label: t('common.actions'),
76
96
  name: 'id',