@molecule/api-resource-inventory 1.0.0 → 1.0.1
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.
- package/README.md +786 -0
- package/package.json +17 -11
package/README.md
ADDED
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
3
|
+
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
|
+
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
|
+
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
+
Generated: 2026-08-04T01:49:14.008Z
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# @molecule/api-resource-inventory
|
|
10
|
+
|
|
11
|
+
> **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
|
|
12
|
+
> It is written to be read by coding agents as much as by people, and is generated from this
|
|
13
|
+
> package's source — edit `src/index.ts` JSDoc, not this file.
|
|
14
|
+
|
|
15
|
+
Inventory resource for molecule.dev.
|
|
16
|
+
|
|
17
|
+
Provides stock tracking with reservations, low-stock alerts,
|
|
18
|
+
movement history, and bulk update support.
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { routes, requestHandlerMap } from '@molecule/api-resource-inventory'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Type
|
|
27
|
+
|
|
28
|
+
`resource`
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @molecule/api-resource-inventory @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-resource
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API
|
|
37
|
+
|
|
38
|
+
### Interfaces
|
|
39
|
+
|
|
40
|
+
#### `BulkStockAdjustment`
|
|
41
|
+
|
|
42
|
+
Input for a single item in a bulk stock update.
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
interface BulkStockAdjustment {
|
|
46
|
+
/** The product to adjust. */
|
|
47
|
+
productId: string
|
|
48
|
+
/** Optional variant identifier. */
|
|
49
|
+
variantId?: string
|
|
50
|
+
/** Quantity to adjust. */
|
|
51
|
+
quantity: number
|
|
52
|
+
/** Type of adjustment. */
|
|
53
|
+
type: StockAdjustmentType
|
|
54
|
+
/** Reason for the adjustment. */
|
|
55
|
+
reason?: string
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### `BulkUpdateItemResult`
|
|
60
|
+
|
|
61
|
+
Result of a single item in a bulk update.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
interface BulkUpdateItemResult {
|
|
65
|
+
/** The product that was adjusted. */
|
|
66
|
+
productId: string
|
|
67
|
+
/** Optional variant identifier. */
|
|
68
|
+
variantId?: string
|
|
69
|
+
/** Whether this individual adjustment succeeded. */
|
|
70
|
+
success: boolean
|
|
71
|
+
/** Error message if the adjustment failed. */
|
|
72
|
+
error?: string
|
|
73
|
+
/** Updated stock info if successful. */
|
|
74
|
+
stock?: StockInfo
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### `BulkUpdateResult`
|
|
79
|
+
|
|
80
|
+
Result of a bulk stock update operation.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
interface BulkUpdateResult {
|
|
84
|
+
/** Total number of adjustments attempted. */
|
|
85
|
+
total: number
|
|
86
|
+
/** Number of successful adjustments. */
|
|
87
|
+
succeeded: number
|
|
88
|
+
/** Number of failed adjustments. */
|
|
89
|
+
failed: number
|
|
90
|
+
/** Per-item results. */
|
|
91
|
+
results: BulkUpdateItemResult[]
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### `InventorySession`
|
|
96
|
+
|
|
97
|
+
Structural view of the fields on `res.locals.session` this resource inspects to
|
|
98
|
+
decide authorization. All fields are optional — a standard molecule session
|
|
99
|
+
carries only `userId`; apps that model roles may also set `isAdmin`/`role`/
|
|
100
|
+
`roles`/`permissions` claims, which are honored here.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
interface InventorySession {
|
|
104
|
+
/** Authenticated user id (set by the global auth middleware). */
|
|
105
|
+
userId?: string
|
|
106
|
+
/** Optional boolean admin claim. */
|
|
107
|
+
isAdmin?: boolean
|
|
108
|
+
/** Optional single-role claim. */
|
|
109
|
+
role?: string
|
|
110
|
+
/** Optional multi-role claim. */
|
|
111
|
+
roles?: string[]
|
|
112
|
+
/** Optional permission strings claim. */
|
|
113
|
+
permissions?: string[]
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### `LowStockAlert`
|
|
118
|
+
|
|
119
|
+
A low-stock alert entry.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
interface LowStockAlert {
|
|
123
|
+
/** The product that is low on stock. */
|
|
124
|
+
productId: string
|
|
125
|
+
/** Optional variant identifier. */
|
|
126
|
+
variantId?: string
|
|
127
|
+
/** Current available quantity. */
|
|
128
|
+
available: number
|
|
129
|
+
/** The low-stock threshold. */
|
|
130
|
+
threshold: number
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### `PaginatedResult`
|
|
135
|
+
|
|
136
|
+
A paginated result set.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
interface PaginatedResult<T> {
|
|
140
|
+
/** The items in this page. */
|
|
141
|
+
data: T[]
|
|
142
|
+
/** Total number of items matching the query. */
|
|
143
|
+
total: number
|
|
144
|
+
/** Current page number (1-based). */
|
|
145
|
+
page: number
|
|
146
|
+
/** Number of items per page. */
|
|
147
|
+
limit: number
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### `PaginationOptions`
|
|
152
|
+
|
|
153
|
+
Pagination options for list queries.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
interface PaginationOptions {
|
|
157
|
+
/** Page number (1-based). */
|
|
158
|
+
page?: number
|
|
159
|
+
/** Number of items per page. */
|
|
160
|
+
limit?: number
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### `Reservation`
|
|
165
|
+
|
|
166
|
+
A stock reservation tied to an order.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
interface Reservation {
|
|
170
|
+
/** Unique reservation identifier. */
|
|
171
|
+
id: string
|
|
172
|
+
/** The product being reserved. */
|
|
173
|
+
productId: string
|
|
174
|
+
/** Optional variant identifier. */
|
|
175
|
+
variantId?: string
|
|
176
|
+
/** Quantity reserved. */
|
|
177
|
+
quantity: number
|
|
178
|
+
/** The order this reservation belongs to. */
|
|
179
|
+
orderId: string
|
|
180
|
+
/** The authenticated user who created this reservation, if known. */
|
|
181
|
+
userId?: string
|
|
182
|
+
/** When the reservation was created. */
|
|
183
|
+
createdAt: string
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### `ReservationRow`
|
|
188
|
+
|
|
189
|
+
Internal database row for a stock reservation.
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
interface ReservationRow {
|
|
193
|
+
/** Unique reservation identifier. */
|
|
194
|
+
id: string
|
|
195
|
+
/** The product being reserved. */
|
|
196
|
+
productId: string
|
|
197
|
+
/** Optional variant identifier. */
|
|
198
|
+
variantId: string | null
|
|
199
|
+
/** Quantity reserved. */
|
|
200
|
+
quantity: number
|
|
201
|
+
/** The order this reservation belongs to. */
|
|
202
|
+
orderId: string
|
|
203
|
+
/** The authenticated user who created this reservation (null for legacy rows). */
|
|
204
|
+
userId: string | null
|
|
205
|
+
/** Creation timestamp. */
|
|
206
|
+
createdAt: string
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### `ReserveStockInput`
|
|
211
|
+
|
|
212
|
+
Input for creating a stock reservation.
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
interface ReserveStockInput {
|
|
216
|
+
/** Quantity to reserve. */
|
|
217
|
+
quantity: number
|
|
218
|
+
/** The order to associate this reservation with. */
|
|
219
|
+
orderId: string
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### `StockAdjustment`
|
|
224
|
+
|
|
225
|
+
Input for adjusting stock levels.
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
interface StockAdjustment {
|
|
229
|
+
/** Optional variant identifier. */
|
|
230
|
+
variantId?: string
|
|
231
|
+
/** Quantity to adjust by (or absolute value for 'set'). */
|
|
232
|
+
quantity: number
|
|
233
|
+
/** Type of adjustment: add, remove, or set to absolute value. */
|
|
234
|
+
type: StockAdjustmentType
|
|
235
|
+
/** Human-readable reason for the adjustment. */
|
|
236
|
+
reason?: string
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
#### `StockInfo`
|
|
241
|
+
|
|
242
|
+
Current stock information for a product or variant.
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
interface StockInfo {
|
|
246
|
+
/** The product this stock belongs to. */
|
|
247
|
+
productId: string
|
|
248
|
+
/** Optional variant identifier (size, color, etc.). */
|
|
249
|
+
variantId?: string
|
|
250
|
+
/** Quantity available for purchase (total − reserved). */
|
|
251
|
+
available: number
|
|
252
|
+
/** Quantity currently reserved by pending orders. */
|
|
253
|
+
reserved: number
|
|
254
|
+
/** Total quantity in stock (available + reserved). */
|
|
255
|
+
total: number
|
|
256
|
+
/** Threshold below which the product is considered low-stock. */
|
|
257
|
+
lowStockThreshold: number
|
|
258
|
+
/** Whether current available stock is at or below the threshold. */
|
|
259
|
+
isLowStock: boolean
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### `StockMovement`
|
|
264
|
+
|
|
265
|
+
A record of a stock movement (adjustment, reservation, release, or confirmation).
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
interface StockMovement {
|
|
269
|
+
/** Unique movement identifier. */
|
|
270
|
+
id: string
|
|
271
|
+
/** The product affected. */
|
|
272
|
+
productId: string
|
|
273
|
+
/** Optional variant identifier. */
|
|
274
|
+
variantId?: string
|
|
275
|
+
/** Type of movement. */
|
|
276
|
+
type: StockMovementType
|
|
277
|
+
/** Quantity change (positive for additions, negative for removals). */
|
|
278
|
+
quantity: number
|
|
279
|
+
/** Reason or description for this movement. */
|
|
280
|
+
reason?: string
|
|
281
|
+
/** Associated order or reservation identifier. */
|
|
282
|
+
referenceId?: string
|
|
283
|
+
/** When this movement occurred. */
|
|
284
|
+
createdAt: string
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
#### `StockMovementRow`
|
|
289
|
+
|
|
290
|
+
Internal database row for a stock movement.
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
interface StockMovementRow {
|
|
294
|
+
/** Unique movement identifier. */
|
|
295
|
+
id: string
|
|
296
|
+
/** The product affected. */
|
|
297
|
+
productId: string
|
|
298
|
+
/** Optional variant identifier. */
|
|
299
|
+
variantId: string | null
|
|
300
|
+
/** Type of movement. */
|
|
301
|
+
type: StockMovementType
|
|
302
|
+
/** Quantity change. */
|
|
303
|
+
quantity: number
|
|
304
|
+
/** Reason for this movement. */
|
|
305
|
+
reason: string | null
|
|
306
|
+
/** Associated reference identifier. */
|
|
307
|
+
referenceId: string | null
|
|
308
|
+
/** Creation timestamp. */
|
|
309
|
+
createdAt: string
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
#### `StockRow`
|
|
314
|
+
|
|
315
|
+
Internal database row for an inventory stock record.
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
interface StockRow {
|
|
319
|
+
/** Unique stock record identifier. */
|
|
320
|
+
id: string
|
|
321
|
+
/** The product this stock belongs to. */
|
|
322
|
+
productId: string
|
|
323
|
+
/** Optional variant identifier. */
|
|
324
|
+
variantId: string | null
|
|
325
|
+
/** Total quantity in stock. */
|
|
326
|
+
total: number
|
|
327
|
+
/** Quantity currently reserved. */
|
|
328
|
+
reserved: number
|
|
329
|
+
/** Low-stock threshold. */
|
|
330
|
+
lowStockThreshold: number
|
|
331
|
+
/** Creation timestamp. */
|
|
332
|
+
createdAt: string
|
|
333
|
+
/** Last modification timestamp. */
|
|
334
|
+
updatedAt: string
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Types
|
|
339
|
+
|
|
340
|
+
#### `StockAdjustmentType`
|
|
341
|
+
|
|
342
|
+
Types of stock adjustment operations.
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
type StockAdjustmentType = 'add' | 'remove' | 'set'
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
#### `StockMovementType`
|
|
349
|
+
|
|
350
|
+
Types of stock movements recorded in the movement history.
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
type StockMovementType = 'adjustment' | 'reservation' | 'release' | 'confirmation'
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Functions
|
|
357
|
+
|
|
358
|
+
#### `assertInventoryAdmin(res)`
|
|
359
|
+
|
|
360
|
+
In-handler admin guard for the admin-only inventory mutations. Writes the
|
|
361
|
+
appropriate JSON error and returns `false` when the caller is not an
|
|
362
|
+
authorized admin — `401` when unauthenticated, `403` when authenticated but
|
|
363
|
+
not an admin. Returns `true` (and writes nothing) when the caller is an admin.
|
|
364
|
+
|
|
365
|
+
Call this at the top of every admin handler so protection holds independently
|
|
366
|
+
of the route middleware (defense-in-depth, fail-closed).
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
function assertInventoryAdmin(res: MoleculeResponse): boolean
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
- `res` — The response, whose `locals.session` is inspected and onto which an error is written when access is denied.
|
|
373
|
+
|
|
374
|
+
**Returns:** `true` when the caller is an authorized admin, otherwise `false`.
|
|
375
|
+
|
|
376
|
+
#### `assertReservationActor(res, reservationUserId)`
|
|
377
|
+
|
|
378
|
+
In-handler guard for reservation-lifecycle mutations (`release`, `confirm`).
|
|
379
|
+
A reservation is owned by the user who created it; only that owner — or an
|
|
380
|
+
inventory admin — may release or confirm it. Writes the appropriate JSON
|
|
381
|
+
error and returns `false` when access is denied (`401` when unauthenticated,
|
|
382
|
+
`403` when authenticated but neither the owner nor an admin); returns `true`
|
|
383
|
+
(writing nothing) when the caller may act.
|
|
384
|
+
|
|
385
|
+
Fail-closed: a reservation with no recorded owner (`null` — e.g. a legacy row
|
|
386
|
+
created before ownership binding) is accessible only to admins.
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
function assertReservationActor(res: MoleculeResponse, reservationUserId: string | null): boolean
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
- `res` — The response, whose `locals.session` is inspected and onto which an error is written when access is denied.
|
|
393
|
+
- `reservationUserId` — The `userId` recorded on the reservation, or `null`.
|
|
394
|
+
|
|
395
|
+
**Returns:** `true` when the caller is the owner or an admin, otherwise `false`.
|
|
396
|
+
|
|
397
|
+
#### `bulkUpdate(req, res)`
|
|
398
|
+
|
|
399
|
+
Processes multiple stock adjustments in a single request.
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
function bulkUpdate(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
- `req` — The request with `adjustments` array body.
|
|
406
|
+
- `res` — The response object.
|
|
407
|
+
|
|
408
|
+
#### `confirm(req, res)`
|
|
409
|
+
|
|
410
|
+
Confirms a reservation, permanently removing the reserved quantity from total stock.
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
function confirm(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
- `req` — The request with `reservationId` param.
|
|
417
|
+
- `res` — The response object.
|
|
418
|
+
|
|
419
|
+
#### `getAlerts(req, res)`
|
|
420
|
+
|
|
421
|
+
Returns all products whose available stock is at or below their low-stock threshold.
|
|
422
|
+
Accepts an optional `threshold` query parameter to override the per-product threshold.
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
function getAlerts(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
- `req` — The request with optional `threshold` query parameter.
|
|
429
|
+
- `res` — The response object.
|
|
430
|
+
|
|
431
|
+
#### `getInventorySession(res)`
|
|
432
|
+
|
|
433
|
+
Reads the structural session off `res.locals`.
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
function getInventorySession(res: MoleculeResponse): InventorySession | undefined
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
- `res` — The response whose `locals.session` is inspected.
|
|
440
|
+
|
|
441
|
+
**Returns:** The session, or `undefined` when unauthenticated.
|
|
442
|
+
|
|
443
|
+
#### `getMovements(req, res)`
|
|
444
|
+
|
|
445
|
+
Returns paginated stock movement history for the given product.
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
function getMovements(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
- `req` — The request with `productId` param and optional pagination query params.
|
|
452
|
+
- `res` — The response object.
|
|
453
|
+
|
|
454
|
+
#### `getStock(req, res)`
|
|
455
|
+
|
|
456
|
+
Returns stock information for the given product (and optional variant).
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
function getStock(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
- `req` — The request with `productId` param and optional `variantId` query.
|
|
463
|
+
- `res` — The response object.
|
|
464
|
+
|
|
465
|
+
#### `isInventoryAdmin(res)`
|
|
466
|
+
|
|
467
|
+
Resolves whether the current request's session belongs to an actor authorized
|
|
468
|
+
to administer inventory (rewrite/bulk-update stock, release/confirm any
|
|
469
|
+
reservation). Fail-closed: returns `false` when there is no authenticated
|
|
470
|
+
session, and otherwise only `true` when the session carries an admin claim —
|
|
471
|
+
`isAdmin === true`, `role === 'admin'`, `roles` containing `'admin'`, or
|
|
472
|
+
`permissions` containing `'admin'` / `'inventory:manage'`.
|
|
473
|
+
|
|
474
|
+
Use this for in-handler defense-in-depth (it does not depend on the route
|
|
475
|
+
middleware being preserved by the injector).
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
function isInventoryAdmin(res: MoleculeResponse): boolean
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
- `res` — The response whose `locals.session` is inspected.
|
|
482
|
+
|
|
483
|
+
**Returns:** `true` when the session is an authorized inventory admin.
|
|
484
|
+
|
|
485
|
+
#### `release(req, res)`
|
|
486
|
+
|
|
487
|
+
Releases a stock reservation, returning the reserved quantity to available stock.
|
|
488
|
+
|
|
489
|
+
```typescript
|
|
490
|
+
function release(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
- `req` — The request with `reservationId` param.
|
|
494
|
+
- `res` — The response object.
|
|
495
|
+
|
|
496
|
+
#### `requireInventoryAdmin()`
|
|
497
|
+
|
|
498
|
+
Route middleware that gates the admin-only inventory routes (`updateStock`,
|
|
499
|
+
`bulkUpdate`). Calls `next()` only for an authenticated admin; otherwise
|
|
500
|
+
forwards an error to the framework error handler — `Unauthorized` when no
|
|
501
|
+
session is present, `Forbidden` when the session is authenticated but not an
|
|
502
|
+
admin.
|
|
503
|
+
|
|
504
|
+
Exposed as a `requestHandlerMap` key so the injector's route scanner keeps it
|
|
505
|
+
(unlike the inert global `'authenticate'` string, which is dropped).
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
function requireInventoryAdmin(): MoleculeRequestHandler
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**Returns:** An Express-compatible middleware function.
|
|
512
|
+
|
|
513
|
+
#### `reserve(req, res)`
|
|
514
|
+
|
|
515
|
+
Reserves stock for the given product and order.
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
function reserve(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
- `req` — The request with `productId` param and {@link ReserveStockInput} body.
|
|
522
|
+
- `res` — The response object.
|
|
523
|
+
|
|
524
|
+
#### `toLowStockAlert(row)`
|
|
525
|
+
|
|
526
|
+
Converts a database stock row into a typed {@link LowStockAlert}.
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
function toLowStockAlert(row: StockRow): LowStockAlert
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
- `row` — The raw database row.
|
|
533
|
+
|
|
534
|
+
**Returns:** The low-stock alert.
|
|
535
|
+
|
|
536
|
+
#### `toReservation(row)`
|
|
537
|
+
|
|
538
|
+
Converts a database reservation row into a typed {@link Reservation}.
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
function toReservation(row: ReservationRow): Reservation
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
- `row` — The raw database row.
|
|
545
|
+
|
|
546
|
+
**Returns:** The deserialized reservation.
|
|
547
|
+
|
|
548
|
+
#### `toStockInfo(row)`
|
|
549
|
+
|
|
550
|
+
Converts a database stock row into a typed {@link StockInfo}.
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
function toStockInfo(row: StockRow): StockInfo
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
- `row` — The raw database row.
|
|
557
|
+
|
|
558
|
+
**Returns:** The deserialized stock info.
|
|
559
|
+
|
|
560
|
+
#### `toStockMovement(row)`
|
|
561
|
+
|
|
562
|
+
Converts a database stock movement row into a typed {@link StockMovement}.
|
|
563
|
+
|
|
564
|
+
```typescript
|
|
565
|
+
function toStockMovement(row: StockMovementRow): StockMovement
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
- `row` — The raw database row.
|
|
569
|
+
|
|
570
|
+
**Returns:** The deserialized stock movement.
|
|
571
|
+
|
|
572
|
+
#### `updateStock(req, res)`
|
|
573
|
+
|
|
574
|
+
Updates stock for the given product. Creates the stock record if it doesn't exist.
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
function updateStock(req: MoleculeRequest, res: MoleculeResponse): Promise<void>
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
- `req` — The request with `productId` param and {@link StockAdjustment} body.
|
|
581
|
+
- `res` — The response object.
|
|
582
|
+
|
|
583
|
+
### Constants
|
|
584
|
+
|
|
585
|
+
#### `i18nRegistered`
|
|
586
|
+
|
|
587
|
+
Whether i18n registration has been attempted. Always `true`; this module is
|
|
588
|
+
a placeholder for symmetry with locale-bonded resources.
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
const i18nRegistered: true
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
#### `INVENTORY_ADMIN_PERMISSION`
|
|
595
|
+
|
|
596
|
+
Session-claim permission string (`'inventory:manage'`) that, when present in a
|
|
597
|
+
session's `permissions` array, grants inventory administration.
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
const INVENTORY_ADMIN_PERMISSION: 'inventory:manage'
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
#### `INVENTORY_PERMISSION_ACTION`
|
|
604
|
+
|
|
605
|
+
Permission action describing inventory administration, e.g. for an app's own
|
|
606
|
+
`@molecule/api-permissions` wiring.
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
const INVENTORY_PERMISSION_ACTION: 'manage'
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
#### `INVENTORY_PERMISSION_RESOURCE`
|
|
613
|
+
|
|
614
|
+
Permission resource describing inventory administration, e.g. for an app's own
|
|
615
|
+
`@molecule/api-permissions` wiring.
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
const INVENTORY_PERMISSION_RESOURCE: 'inventory'
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
#### `requestHandlerMap`
|
|
622
|
+
|
|
623
|
+
Handler map for the inventory resource routes.
|
|
624
|
+
|
|
625
|
+
`requireInventoryAdmin` is the admin authorizer middleware referenced by the
|
|
626
|
+
`updateStock`/`bulkUpdate` routes. It must live here (as a real handler-map
|
|
627
|
+
key) so the mlcl injector's route scanner preserves it — a bare middleware
|
|
628
|
+
string that isn't a handler-map key is silently dropped, which is exactly how
|
|
629
|
+
the previous bare `'authenticate'` gate became inert.
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
const requestHandlerMap: {
|
|
633
|
+
readonly getStock: typeof getStock
|
|
634
|
+
readonly updateStock: typeof updateStock
|
|
635
|
+
readonly reserve: typeof reserve
|
|
636
|
+
readonly release: typeof release
|
|
637
|
+
readonly confirm: typeof confirm
|
|
638
|
+
readonly getAlerts: typeof getAlerts
|
|
639
|
+
readonly getMovements: typeof getMovements
|
|
640
|
+
readonly bulkUpdate: typeof bulkUpdate
|
|
641
|
+
readonly requireInventoryAdmin: MoleculeRequestHandler
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
#### `routes`
|
|
646
|
+
|
|
647
|
+
Inventory resource routes.
|
|
648
|
+
|
|
649
|
+
All routes require authentication. The destructive admin-side mutations
|
|
650
|
+
(`updateStock`, `bulkUpdate`) are additionally gated by the
|
|
651
|
+
`requireInventoryAdmin` middleware — a real `requestHandlerMap` key (see
|
|
652
|
+
{@link requireInventoryAdmin}) so the injector preserves it; the previously
|
|
653
|
+
declared bare `'authenticate'` string was silently dropped by the route
|
|
654
|
+
scanner, leaving stock open to rewrite by any authenticated user. Each
|
|
655
|
+
admin/reservation handler additionally re-checks authorization internally, so
|
|
656
|
+
the gate holds even if a consumer wires the routes without these middlewares.
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
const routes: readonly [
|
|
660
|
+
{
|
|
661
|
+
readonly method: 'get'
|
|
662
|
+
readonly path: '/inventory/:productId'
|
|
663
|
+
readonly handler: 'getStock'
|
|
664
|
+
readonly middlewares: readonly ['authenticate']
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
readonly method: 'put'
|
|
668
|
+
readonly path: '/inventory/:productId'
|
|
669
|
+
readonly handler: 'updateStock'
|
|
670
|
+
readonly middlewares: readonly ['requireInventoryAdmin']
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
readonly method: 'post'
|
|
674
|
+
readonly path: '/inventory/:productId/reserve'
|
|
675
|
+
readonly handler: 'reserve'
|
|
676
|
+
readonly middlewares: readonly ['authenticate']
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
readonly method: 'delete'
|
|
680
|
+
readonly path: '/inventory/reservations/:reservationId'
|
|
681
|
+
readonly handler: 'release'
|
|
682
|
+
readonly middlewares: readonly ['authenticate']
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
readonly method: 'post'
|
|
686
|
+
readonly path: '/inventory/reservations/:reservationId/confirm'
|
|
687
|
+
readonly handler: 'confirm'
|
|
688
|
+
readonly middlewares: readonly ['authenticate']
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
readonly method: 'get'
|
|
692
|
+
readonly path: '/inventory/alerts'
|
|
693
|
+
readonly handler: 'getAlerts'
|
|
694
|
+
readonly middlewares: readonly ['authenticate']
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
readonly method: 'get'
|
|
698
|
+
readonly path: '/inventory/:productId/movements'
|
|
699
|
+
readonly handler: 'getMovements'
|
|
700
|
+
readonly middlewares: readonly ['authenticate']
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
readonly method: 'post'
|
|
704
|
+
readonly path: '/inventory/bulk'
|
|
705
|
+
readonly handler: 'bulkUpdate'
|
|
706
|
+
readonly middlewares: readonly ['requireInventoryAdmin']
|
|
707
|
+
},
|
|
708
|
+
]
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
## Injection Notes
|
|
712
|
+
|
|
713
|
+
### Requirements
|
|
714
|
+
|
|
715
|
+
Peer dependencies:
|
|
716
|
+
|
|
717
|
+
- `@molecule/api-database` ^1.0.1
|
|
718
|
+
- `@molecule/api-i18n` ^1.0.1
|
|
719
|
+
- `@molecule/api-logger` ^1.0.1
|
|
720
|
+
- `@molecule/api-resource` ^1.0.1
|
|
721
|
+
|
|
722
|
+
### Runtime Dependencies
|
|
723
|
+
|
|
724
|
+
- `@molecule/api-database`
|
|
725
|
+
- `@molecule/api-i18n`
|
|
726
|
+
- `@molecule/api-logger`
|
|
727
|
+
- `@molecule/api-resource`
|
|
728
|
+
|
|
729
|
+
Tables: `src/__setup__/inventory.sql` creates `inventory_stock`,
|
|
730
|
+
`inventory_reservations`, and `inventory_movements`. An mlcl-scaffolded API
|
|
731
|
+
replays `__setup__/*.sql` automatically on migrate; anywhere else run it
|
|
732
|
+
once — nothing at runtime creates them.
|
|
733
|
+
|
|
734
|
+
Stock rows are keyed by `productId` — SHARED app-wide state, not per-user
|
|
735
|
+
rows. Writing stock (`PUT /inventory/:productId`) and
|
|
736
|
+
`POST /inventory/bulk` are role-gated and DENY BY DEFAULT (admin session
|
|
737
|
+
claim or an `@molecule/api-permissions` grant), enforced both as the
|
|
738
|
+
`requireInventoryAdmin` route middleware and inside the handlers
|
|
739
|
+
(fail-closed). Out of the box no one can mutate stock — grant the role
|
|
740
|
+
first; never "fix" the 403 by removing the gate.
|
|
741
|
+
|
|
742
|
+
Reservation flow: `POST /inventory/:productId/reserve` holds quantity →
|
|
743
|
+
`POST /inventory/reservations/:id/confirm` deducts it,
|
|
744
|
+
`DELETE /inventory/reservations/:id` releases the hold. All handlers read
|
|
745
|
+
the authenticated user from `res.locals.session` (mount behind your global
|
|
746
|
+
auth middleware; 401 otherwise).
|
|
747
|
+
|
|
748
|
+
## E2E Tests
|
|
749
|
+
|
|
750
|
+
Integration checklist — drive the real UI (live preview, no mocks), adapt
|
|
751
|
+
each item to this app's actual stock/admin screens, and check every box off
|
|
752
|
+
one by one. Correctness is the whole point here: a box you can't check is a
|
|
753
|
+
bug to fix, not a skip.
|
|
754
|
+
|
|
755
|
+
- [ ] Setting stock (`PUT /inventory/:productId`, `type:'set'`) then
|
|
756
|
+
reloading `GET /inventory/:productId` shows the exact `total`/`available`
|
|
757
|
+
you set — it persisted, not just optimistic UI. `available` = `total` -
|
|
758
|
+
`reserved`.
|
|
759
|
+
- [ ] `type:'add' N` raises `total` by exactly N and `type:'remove' N`
|
|
760
|
+
lowers it by exactly N — verify the arithmetic on the specific
|
|
761
|
+
`productId`/`variantId`; variant stock is tracked independently, so
|
|
762
|
+
adjusting one variant must not move another.
|
|
763
|
+
- [ ] Stock never goes negative: removing or `set`ting below the currently
|
|
764
|
+
reserved quantity is rejected with a visible 409 error
|
|
765
|
+
(`inventory.error.insufficientStock`) and the stored `total` is unchanged
|
|
766
|
+
— never persisted as a negative; reserving more than `available` is
|
|
767
|
+
likewise rejected (409 `insufficientAvailable`).
|
|
768
|
+
- [ ] Low-stock crossing flags the item: when `available` falls to or below
|
|
769
|
+
`lowStockThreshold` (default 10) it reads `isLowStock:true` and appears in
|
|
770
|
+
`GET /inventory/alerts`; raising stock back above the threshold clears it.
|
|
771
|
+
- [ ] Every mutation appends an `inventory_movements` row shown in
|
|
772
|
+
`GET /inventory/:productId/movements` with the signed delta (`+N`/`-N`),
|
|
773
|
+
type (adjustment/reservation/confirmation), timestamp, and `referenceId`
|
|
774
|
+
(orderId/reservationId); the acting user is recorded on the reservation.
|
|
775
|
+
The movement log must reconstruct the current total.
|
|
776
|
+
- [ ] Concurrency: two reservations or removals fired at once that together
|
|
777
|
+
exceed `available` don't double-spend the last unit — exactly one
|
|
778
|
+
succeeds, and final `total`/`reserved` stay consistent (`available` never
|
|
779
|
+
goes negative).
|
|
780
|
+
- [ ] AUTHORIZATION — stock mutation is admin-only and denies by default:
|
|
781
|
+
`PUT /inventory/:productId` and `POST /inventory/bulk` return 403 for a
|
|
782
|
+
normal signed-in user (no `isAdmin`/`role:'admin'`/`roles`/`permissions`
|
|
783
|
+
claim) and 401 when signed out; only an admin session can change stock. A
|
|
784
|
+
customer cannot mutate the shared catalog stock through any endpoint, and
|
|
785
|
+
one user cannot release/confirm another user's reservation (403
|
|
786
|
+
`reservationForbidden`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-resource-inventory",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Inventory resource with stock tracking, reservations, low-stock alerts, movement history, and bulk updates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
21
22
|
],
|
|
22
23
|
"keywords": [
|
|
23
24
|
"molecule",
|
|
@@ -27,21 +28,26 @@
|
|
|
27
28
|
"resource"
|
|
28
29
|
],
|
|
29
30
|
"license": "Apache-2.0",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/molecule-dev/molecule.git",
|
|
34
|
+
"directory": "packages/api/resources/inventory"
|
|
35
|
+
},
|
|
30
36
|
"devDependencies": {
|
|
31
|
-
"@molecule/api-bond": "1.0.
|
|
32
|
-
"@molecule/api-database": "1.0.
|
|
33
|
-
"@molecule/api-i18n": "1.0.
|
|
34
|
-
"@molecule/api-logger": "1.0.
|
|
35
|
-
"@molecule/api-resource": "1.0.
|
|
37
|
+
"@molecule/api-bond": "1.0.1",
|
|
38
|
+
"@molecule/api-database": "1.0.1",
|
|
39
|
+
"@molecule/api-i18n": "1.0.1",
|
|
40
|
+
"@molecule/api-logger": "1.0.1",
|
|
41
|
+
"@molecule/api-resource": "1.0.1",
|
|
36
42
|
"@types/node": "26.1.2",
|
|
37
43
|
"typescript": "6.0.3",
|
|
38
44
|
"vitest": "4.1.10"
|
|
39
45
|
},
|
|
40
46
|
"peerDependencies": {
|
|
41
|
-
"@molecule/api-database": "^1.0.
|
|
42
|
-
"@molecule/api-i18n": "^1.0.
|
|
43
|
-
"@molecule/api-logger": "^1.0.
|
|
44
|
-
"@molecule/api-resource": "^1.0.
|
|
47
|
+
"@molecule/api-database": "^1.0.1",
|
|
48
|
+
"@molecule/api-i18n": "^1.0.1",
|
|
49
|
+
"@molecule/api-logger": "^1.0.1",
|
|
50
|
+
"@molecule/api-resource": "^1.0.1"
|
|
45
51
|
},
|
|
46
52
|
"peerDependenciesMeta": {
|
|
47
53
|
"@molecule/api-database": {
|