@molecule/api-resource-inventory 1.0.0

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 (53) hide show
  1. package/LICENSE +115 -0
  2. package/dist/authorizers/index.d.ts +133 -0
  3. package/dist/authorizers/index.d.ts.map +1 -0
  4. package/dist/authorizers/index.js +188 -0
  5. package/dist/browser-guard.d.ts +2 -0
  6. package/dist/browser-guard.d.ts.map +1 -0
  7. package/dist/browser-guard.js +18 -0
  8. package/dist/handlers/bulkUpdate.d.ts +13 -0
  9. package/dist/handlers/bulkUpdate.d.ts.map +1 -0
  10. package/dist/handlers/bulkUpdate.js +168 -0
  11. package/dist/handlers/confirm.d.ts +13 -0
  12. package/dist/handlers/confirm.d.ts.map +1 -0
  13. package/dist/handlers/confirm.js +68 -0
  14. package/dist/handlers/getAlerts.d.ts +14 -0
  15. package/dist/handlers/getAlerts.d.ts.map +1 -0
  16. package/dist/handlers/getAlerts.js +52 -0
  17. package/dist/handlers/getMovements.d.ts +13 -0
  18. package/dist/handlers/getMovements.d.ts.map +1 -0
  19. package/dist/handlers/getMovements.js +62 -0
  20. package/dist/handlers/getStock.d.ts +13 -0
  21. package/dist/handlers/getStock.d.ts.map +1 -0
  22. package/dist/handlers/getStock.js +58 -0
  23. package/dist/handlers/index.d.ts +9 -0
  24. package/dist/handlers/index.d.ts.map +1 -0
  25. package/dist/handlers/index.js +8 -0
  26. package/dist/handlers/release.d.ts +13 -0
  27. package/dist/handlers/release.d.ts.map +1 -0
  28. package/dist/handlers/release.js +67 -0
  29. package/dist/handlers/reserve.d.ts +13 -0
  30. package/dist/handlers/reserve.d.ts.map +1 -0
  31. package/dist/handlers/reserve.js +112 -0
  32. package/dist/handlers/updateStock.d.ts +13 -0
  33. package/dist/handlers/updateStock.d.ts.map +1 -0
  34. package/dist/handlers/updateStock.js +126 -0
  35. package/dist/i18n.d.ts +14 -0
  36. package/dist/i18n.d.ts.map +1 -0
  37. package/dist/i18n.js +13 -0
  38. package/dist/index.d.ts +79 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +78 -0
  41. package/dist/requestHandlerMap.d.ts +34 -0
  42. package/dist/requestHandlerMap.d.ts.map +1 -0
  43. package/dist/requestHandlerMap.js +34 -0
  44. package/dist/routes.d.ts +61 -0
  45. package/dist/routes.d.ts.map +1 -0
  46. package/dist/routes.js +69 -0
  47. package/dist/types.d.ts +234 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +6 -0
  50. package/dist/utilities.d.ts +31 -0
  51. package/dist/utilities.d.ts.map +1 -0
  52. package/dist/utilities.js +79 -0
  53. package/package.json +54 -0
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Handler for confirming a stock reservation (permanently removing from stock).
3
+ *
4
+ * @module
5
+ */
6
+ import { create, deleteById, findById, findOne, updateById } from '@molecule/api-database';
7
+ import { t } from '@molecule/api-i18n';
8
+ import { logger } from '@molecule/api-logger';
9
+ import { assertReservationActor } from '../authorizers/index.js';
10
+ /**
11
+ * Confirms a reservation, permanently removing the reserved quantity from total stock.
12
+ * @param req - The request with `reservationId` param.
13
+ * @param res - The response object.
14
+ */
15
+ export async function confirm(req, res) {
16
+ const { reservationId } = req.params;
17
+ try {
18
+ const reservation = await findById('inventory_reservations', reservationId);
19
+ if (!reservation) {
20
+ res.status(404).json({
21
+ error: t('inventory.error.reservationNotFound', undefined, {
22
+ defaultValue: 'Reservation not found',
23
+ }),
24
+ errorKey: 'inventory.error.reservationNotFound',
25
+ });
26
+ return;
27
+ }
28
+ // Only the reservation's owner or an inventory admin may confirm it.
29
+ if (!assertReservationActor(res, reservation.userId)) {
30
+ return;
31
+ }
32
+ // Find the stock record
33
+ const where = [
34
+ { field: 'productId', operator: '=', value: reservation.productId },
35
+ { field: 'variantId', operator: '=', value: reservation.variantId },
36
+ ];
37
+ const stockRow = await findOne('inventory_stock', where);
38
+ if (stockRow) {
39
+ // Decrease both total and reserved
40
+ await updateById('inventory_stock', stockRow.id, {
41
+ total: stockRow.total - reservation.quantity,
42
+ reserved: Math.max(0, stockRow.reserved - reservation.quantity),
43
+ });
44
+ }
45
+ // Record movement
46
+ await create('inventory_movements', {
47
+ productId: reservation.productId,
48
+ variantId: reservation.variantId,
49
+ type: 'confirmation',
50
+ quantity: -reservation.quantity,
51
+ reason: null,
52
+ referenceId: reservationId,
53
+ });
54
+ // Delete reservation
55
+ await deleteById('inventory_reservations', reservationId);
56
+ logger.debug('Reservation confirmed', { reservationId, productId: reservation.productId });
57
+ res.status(204).end();
58
+ }
59
+ catch (error) {
60
+ logger.error('Failed to confirm reservation', { reservationId, error });
61
+ res.status(500).json({
62
+ error: t('inventory.error.confirmFailed', undefined, {
63
+ defaultValue: 'Failed to confirm reservation',
64
+ }),
65
+ errorKey: 'inventory.error.confirmFailed',
66
+ });
67
+ }
68
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Handler for retrieving low-stock alerts.
3
+ *
4
+ * @module
5
+ */
6
+ import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
7
+ /**
8
+ * Returns all products whose available stock is at or below their low-stock threshold.
9
+ * Accepts an optional `threshold` query parameter to override the per-product threshold.
10
+ * @param req - The request with optional `threshold` query parameter.
11
+ * @param res - The response object.
12
+ */
13
+ export declare function getAlerts(req: MoleculeRequest, res: MoleculeResponse): Promise<void>;
14
+ //# sourceMappingURL=getAlerts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getAlerts.d.ts","sourceRoot":"","sources":["../../src/handlers/getAlerts.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAM/E;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqC1F"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Handler for retrieving low-stock alerts.
3
+ *
4
+ * @module
5
+ */
6
+ import { findMany } from '@molecule/api-database';
7
+ import { t } from '@molecule/api-i18n';
8
+ import { logger } from '@molecule/api-logger';
9
+ import { getInventorySession } from '../authorizers/index.js';
10
+ import { toLowStockAlert } from '../utilities.js';
11
+ /**
12
+ * Returns all products whose available stock is at or below their low-stock threshold.
13
+ * Accepts an optional `threshold` query parameter to override the per-product threshold.
14
+ * @param req - The request with optional `threshold` query parameter.
15
+ * @param res - The response object.
16
+ */
17
+ export async function getAlerts(req, res) {
18
+ // Low-stock alerts expose the full product/stock ledger — require auth in the
19
+ // handler, not just route middleware (the scanner can drop a declared
20
+ // authorizer whose name has no handler-map entry). Fail closed.
21
+ if (!getInventorySession(res)?.userId) {
22
+ res.status(401).json({
23
+ error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
24
+ errorKey: 'resource.error.unauthorized',
25
+ });
26
+ return;
27
+ }
28
+ try {
29
+ const customThreshold = req.query.threshold ? Number(req.query.threshold) : undefined;
30
+ // Fetch all stock records — filtering by computed field (available = total - reserved)
31
+ // must be done in application code since the DataStore abstraction doesn't support
32
+ // computed-column filters.
33
+ const rows = await findMany('inventory_stock');
34
+ const alerts = rows
35
+ .filter((row) => {
36
+ const available = row.total - row.reserved;
37
+ const threshold = customThreshold ?? row.lowStockThreshold;
38
+ return available <= threshold;
39
+ })
40
+ .map(toLowStockAlert);
41
+ res.json(alerts);
42
+ }
43
+ catch (error) {
44
+ logger.error('Failed to get low stock alerts', { error });
45
+ res.status(500).json({
46
+ error: t('inventory.error.alertsFailed', undefined, {
47
+ defaultValue: 'Failed to get low stock alerts',
48
+ }),
49
+ errorKey: 'inventory.error.alertsFailed',
50
+ });
51
+ }
52
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Handler for retrieving stock movement history for a product.
3
+ *
4
+ * @module
5
+ */
6
+ import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
7
+ /**
8
+ * Returns paginated stock movement history for the given product.
9
+ * @param req - The request with `productId` param and optional pagination query params.
10
+ * @param res - The response object.
11
+ */
12
+ export declare function getMovements(req: MoleculeRequest, res: MoleculeResponse): Promise<void>;
13
+ //# sourceMappingURL=getMovements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getMovements.d.ts","sourceRoot":"","sources":["../../src/handlers/getMovements.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAW/E;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6C7F"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Handler for retrieving stock movement history for a product.
3
+ *
4
+ * @module
5
+ */
6
+ import { count, findMany } from '@molecule/api-database';
7
+ import { t } from '@molecule/api-i18n';
8
+ import { logger } from '@molecule/api-logger';
9
+ import { getInventorySession } from '../authorizers/index.js';
10
+ import { toStockMovement } from '../utilities.js';
11
+ /** Default page size. */
12
+ const DEFAULT_LIMIT = 20;
13
+ /** Maximum page size. */
14
+ const MAX_LIMIT = 100;
15
+ /**
16
+ * Returns paginated stock movement history for the given product.
17
+ * @param req - The request with `productId` param and optional pagination query params.
18
+ * @param res - The response object.
19
+ */
20
+ export async function getMovements(req, res) {
21
+ // The full stock ledger for a product — require auth in the handler. Fail closed.
22
+ if (!getInventorySession(res)?.userId) {
23
+ res.status(401).json({
24
+ error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
25
+ errorKey: 'resource.error.unauthorized',
26
+ });
27
+ return;
28
+ }
29
+ const { productId } = req.params;
30
+ try {
31
+ const page = Math.max(1, Number(req.query.page) || 1);
32
+ const limit = Math.min(MAX_LIMIT, Math.max(1, Number(req.query.limit) || DEFAULT_LIMIT));
33
+ const offset = (page - 1) * limit;
34
+ const where = [
35
+ { field: 'productId', operator: '=', value: productId },
36
+ ];
37
+ const [rows, total] = await Promise.all([
38
+ findMany('inventory_movements', {
39
+ where,
40
+ orderBy: [{ field: 'createdAt', direction: 'desc' }],
41
+ limit,
42
+ offset,
43
+ }),
44
+ count('inventory_movements', where),
45
+ ]);
46
+ res.json({
47
+ data: rows.map(toStockMovement),
48
+ total,
49
+ page,
50
+ limit,
51
+ });
52
+ }
53
+ catch (error) {
54
+ logger.error('Failed to get stock movements', { productId, error });
55
+ res.status(500).json({
56
+ error: t('inventory.error.movementsFailed', undefined, {
57
+ defaultValue: 'Failed to get stock movements',
58
+ }),
59
+ errorKey: 'inventory.error.movementsFailed',
60
+ });
61
+ }
62
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Handler for retrieving stock information for a product.
3
+ *
4
+ * @module
5
+ */
6
+ import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
7
+ /**
8
+ * Returns stock information for the given product (and optional variant).
9
+ * @param req - The request with `productId` param and optional `variantId` query.
10
+ * @param res - The response object.
11
+ */
12
+ export declare function getStock(req: MoleculeRequest, res: MoleculeResponse): Promise<void>;
13
+ //# sourceMappingURL=getStock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getStock.d.ts","sourceRoot":"","sources":["../../src/handlers/getStock.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAM/E;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6CzF"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Handler for retrieving stock information for a product.
3
+ *
4
+ * @module
5
+ */
6
+ import { findOne } from '@molecule/api-database';
7
+ import { t } from '@molecule/api-i18n';
8
+ import { logger } from '@molecule/api-logger';
9
+ import { getInventorySession } from '../authorizers/index.js';
10
+ import { toStockInfo } from '../utilities.js';
11
+ /**
12
+ * Returns stock information for the given product (and optional variant).
13
+ * @param req - The request with `productId` param and optional `variantId` query.
14
+ * @param res - The response object.
15
+ */
16
+ export async function getStock(req, res) {
17
+ // Stock levels are business data — require auth in the handler. Fail closed.
18
+ if (!getInventorySession(res)?.userId) {
19
+ res.status(401).json({
20
+ error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
21
+ errorKey: 'resource.error.unauthorized',
22
+ });
23
+ return;
24
+ }
25
+ const { productId } = req.params;
26
+ const variantId = req.query.variantId;
27
+ try {
28
+ const where = [
29
+ { field: 'productId', operator: '=', value: productId },
30
+ ];
31
+ if (variantId) {
32
+ where.push({ field: 'variantId', operator: '=', value: variantId });
33
+ }
34
+ else {
35
+ where.push({ field: 'variantId', operator: '=', value: null });
36
+ }
37
+ const row = await findOne('inventory_stock', where);
38
+ if (!row) {
39
+ res.status(404).json({
40
+ error: t('inventory.error.stockNotFound', undefined, {
41
+ defaultValue: 'Stock record not found',
42
+ }),
43
+ errorKey: 'inventory.error.stockNotFound',
44
+ });
45
+ return;
46
+ }
47
+ res.json(toStockInfo(row));
48
+ }
49
+ catch (error) {
50
+ logger.error('Failed to get stock', { productId, variantId, error });
51
+ res.status(500).json({
52
+ error: t('inventory.error.getStockFailed', undefined, {
53
+ defaultValue: 'Failed to get stock information',
54
+ }),
55
+ errorKey: 'inventory.error.getStockFailed',
56
+ });
57
+ }
58
+ }
@@ -0,0 +1,9 @@
1
+ export { bulkUpdate } from './bulkUpdate.js';
2
+ export { confirm } from './confirm.js';
3
+ export { getAlerts } from './getAlerts.js';
4
+ export { getMovements } from './getMovements.js';
5
+ export { getStock } from './getStock.js';
6
+ export { release } from './release.js';
7
+ export { reserve } from './reserve.js';
8
+ export { updateStock } from './updateStock.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1,8 @@
1
+ export { bulkUpdate } from './bulkUpdate.js';
2
+ export { confirm } from './confirm.js';
3
+ export { getAlerts } from './getAlerts.js';
4
+ export { getMovements } from './getMovements.js';
5
+ export { getStock } from './getStock.js';
6
+ export { release } from './release.js';
7
+ export { reserve } from './reserve.js';
8
+ export { updateStock } from './updateStock.js';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Handler for releasing a stock reservation.
3
+ *
4
+ * @module
5
+ */
6
+ import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
7
+ /**
8
+ * Releases a stock reservation, returning the reserved quantity to available stock.
9
+ * @param req - The request with `reservationId` param.
10
+ * @param res - The response object.
11
+ */
12
+ export declare function release(req: MoleculeRequest, res: MoleculeResponse): Promise<void>;
13
+ //# sourceMappingURL=release.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"release.d.ts","sourceRoot":"","sources":["../../src/handlers/release.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAK/E;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6DxF"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Handler for releasing a stock reservation.
3
+ *
4
+ * @module
5
+ */
6
+ import { create, deleteById, findById, findOne, updateById } from '@molecule/api-database';
7
+ import { t } from '@molecule/api-i18n';
8
+ import { logger } from '@molecule/api-logger';
9
+ import { assertReservationActor } from '../authorizers/index.js';
10
+ /**
11
+ * Releases a stock reservation, returning the reserved quantity to available stock.
12
+ * @param req - The request with `reservationId` param.
13
+ * @param res - The response object.
14
+ */
15
+ export async function release(req, res) {
16
+ const { reservationId } = req.params;
17
+ try {
18
+ const reservation = await findById('inventory_reservations', reservationId);
19
+ if (!reservation) {
20
+ res.status(404).json({
21
+ error: t('inventory.error.reservationNotFound', undefined, {
22
+ defaultValue: 'Reservation not found',
23
+ }),
24
+ errorKey: 'inventory.error.reservationNotFound',
25
+ });
26
+ return;
27
+ }
28
+ // Only the reservation's owner or an inventory admin may release it.
29
+ if (!assertReservationActor(res, reservation.userId)) {
30
+ return;
31
+ }
32
+ // Find the stock record
33
+ const where = [
34
+ { field: 'productId', operator: '=', value: reservation.productId },
35
+ { field: 'variantId', operator: '=', value: reservation.variantId },
36
+ ];
37
+ const stockRow = await findOne('inventory_stock', where);
38
+ if (stockRow) {
39
+ // Decrease reserved count
40
+ await updateById('inventory_stock', stockRow.id, {
41
+ reserved: Math.max(0, stockRow.reserved - reservation.quantity),
42
+ });
43
+ }
44
+ // Record movement
45
+ await create('inventory_movements', {
46
+ productId: reservation.productId,
47
+ variantId: reservation.variantId,
48
+ type: 'release',
49
+ quantity: reservation.quantity,
50
+ reason: null,
51
+ referenceId: reservationId,
52
+ });
53
+ // Delete reservation
54
+ await deleteById('inventory_reservations', reservationId);
55
+ logger.debug('Reservation released', { reservationId, productId: reservation.productId });
56
+ res.status(204).end();
57
+ }
58
+ catch (error) {
59
+ logger.error('Failed to release reservation', { reservationId, error });
60
+ res.status(500).json({
61
+ error: t('inventory.error.releaseFailed', undefined, {
62
+ defaultValue: 'Failed to release reservation',
63
+ }),
64
+ errorKey: 'inventory.error.releaseFailed',
65
+ });
66
+ }
67
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Handler for reserving stock for an order.
3
+ *
4
+ * @module
5
+ */
6
+ import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
7
+ /**
8
+ * Reserves stock for the given product and order.
9
+ * @param req - The request with `productId` param and {@link ReserveStockInput} body.
10
+ * @param res - The response object.
11
+ */
12
+ export declare function reserve(req: MoleculeRequest, res: MoleculeResponse): Promise<void>;
13
+ //# sourceMappingURL=reserve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reserve.d.ts","sourceRoot":"","sources":["../../src/handlers/reserve.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAM/E;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2GxF"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Handler for reserving stock for an order.
3
+ *
4
+ * @module
5
+ */
6
+ import { create, findOne, updateById } from '@molecule/api-database';
7
+ import { t } from '@molecule/api-i18n';
8
+ import { logger } from '@molecule/api-logger';
9
+ import { getInventorySession } from '../authorizers/index.js';
10
+ import { toReservation } from '../utilities.js';
11
+ /**
12
+ * Reserves stock for the given product and order.
13
+ * @param req - The request with `productId` param and {@link ReserveStockInput} body.
14
+ * @param res - The response object.
15
+ */
16
+ export async function reserve(req, res) {
17
+ // A reservation is tied to the caller's cart/order — require an authenticated
18
+ // user and bind the reservation to them so release/confirm can verify
19
+ // ownership. Fail closed in-handler regardless of route middleware.
20
+ const userId = getInventorySession(res)?.userId;
21
+ if (!userId) {
22
+ res.status(401).json({
23
+ error: t('resource.error.unauthorized', undefined, { defaultValue: 'Unauthorized' }),
24
+ errorKey: 'resource.error.unauthorized',
25
+ });
26
+ return;
27
+ }
28
+ const { productId } = req.params;
29
+ const input = req.body;
30
+ const variantId = req.query.variantId ?? null;
31
+ if (!input.quantity || input.quantity < 1) {
32
+ res.status(400).json({
33
+ error: t('inventory.error.invalidQuantity', undefined, {
34
+ defaultValue: 'Quantity must be at least 1',
35
+ }),
36
+ errorKey: 'inventory.error.invalidQuantity',
37
+ });
38
+ return;
39
+ }
40
+ if (!input.orderId) {
41
+ res.status(400).json({
42
+ error: t('inventory.error.orderIdRequired', undefined, {
43
+ defaultValue: 'Order ID is required',
44
+ }),
45
+ errorKey: 'inventory.error.orderIdRequired',
46
+ });
47
+ return;
48
+ }
49
+ try {
50
+ const where = [
51
+ { field: 'productId', operator: '=', value: productId },
52
+ { field: 'variantId', operator: '=', value: variantId },
53
+ ];
54
+ const row = await findOne('inventory_stock', where);
55
+ if (!row) {
56
+ res.status(404).json({
57
+ error: t('inventory.error.stockNotFound', undefined, {
58
+ defaultValue: 'Stock record not found',
59
+ }),
60
+ errorKey: 'inventory.error.stockNotFound',
61
+ });
62
+ return;
63
+ }
64
+ const available = row.total - row.reserved;
65
+ if (input.quantity > available) {
66
+ res.status(409).json({
67
+ error: t('inventory.error.insufficientAvailable', undefined, {
68
+ defaultValue: 'Insufficient stock available',
69
+ }),
70
+ errorKey: 'inventory.error.insufficientAvailable',
71
+ });
72
+ return;
73
+ }
74
+ // Create reservation record
75
+ const reservationResult = await create('inventory_reservations', {
76
+ productId,
77
+ variantId,
78
+ quantity: input.quantity,
79
+ orderId: input.orderId,
80
+ userId,
81
+ });
82
+ // Increase reserved count
83
+ await updateById('inventory_stock', row.id, {
84
+ reserved: row.reserved + input.quantity,
85
+ });
86
+ // Record movement
87
+ await create('inventory_movements', {
88
+ productId,
89
+ variantId,
90
+ type: 'reservation',
91
+ quantity: -input.quantity,
92
+ reason: null,
93
+ referenceId: input.orderId,
94
+ });
95
+ logger.debug('Stock reserved', {
96
+ productId,
97
+ variantId,
98
+ quantity: input.quantity,
99
+ orderId: input.orderId,
100
+ });
101
+ res.status(201).json(toReservation(reservationResult.data));
102
+ }
103
+ catch (error) {
104
+ logger.error('Failed to reserve stock', { productId, error });
105
+ res.status(500).json({
106
+ error: t('inventory.error.reserveFailed', undefined, {
107
+ defaultValue: 'Failed to reserve stock',
108
+ }),
109
+ errorKey: 'inventory.error.reserveFailed',
110
+ });
111
+ }
112
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Handler for updating stock levels for a product.
3
+ *
4
+ * @module
5
+ */
6
+ import type { MoleculeRequest, MoleculeResponse } from '@molecule/api-resource';
7
+ /**
8
+ * Updates stock for the given product. Creates the stock record if it doesn't exist.
9
+ * @param req - The request with `productId` param and {@link StockAdjustment} body.
10
+ * @param res - The response object.
11
+ */
12
+ export declare function updateStock(req: MoleculeRequest, res: MoleculeResponse): Promise<void>;
13
+ //# sourceMappingURL=updateStock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updateStock.d.ts","sourceRoot":"","sources":["../../src/handlers/updateStock.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAS/E;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsH5F"}