@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.
Files changed (2) hide show
  1. package/README.md +786 -0
  2. 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.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.0",
32
- "@molecule/api-database": "1.0.0",
33
- "@molecule/api-i18n": "1.0.0",
34
- "@molecule/api-logger": "1.0.0",
35
- "@molecule/api-resource": "1.0.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.0",
42
- "@molecule/api-i18n": "^1.0.0",
43
- "@molecule/api-logger": "^1.0.0",
44
- "@molecule/api-resource": "^1.0.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": {