@endora-commerce/mod-inventory 0.100.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 (223) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/dist/admin/api/warehouses-client.d.ts +22 -0
  4. package/dist/admin/api/warehouses-client.d.ts.map +1 -0
  5. package/dist/admin/api/warehouses-client.js +32 -0
  6. package/dist/admin/api/warehouses-client.js.map +1 -0
  7. package/dist/admin/index.d.ts +43 -0
  8. package/dist/admin/index.d.ts.map +1 -0
  9. package/dist/admin/index.js +190 -0
  10. package/dist/admin/index.js.map +1 -0
  11. package/dist/admin/pages/AvailabilityNotificationsPage.d.ts +15 -0
  12. package/dist/admin/pages/AvailabilityNotificationsPage.d.ts.map +1 -0
  13. package/dist/admin/pages/AvailabilityNotificationsPage.js +111 -0
  14. package/dist/admin/pages/AvailabilityNotificationsPage.js.map +1 -0
  15. package/dist/admin/pages/InventoryPage.d.ts +17 -0
  16. package/dist/admin/pages/InventoryPage.d.ts.map +1 -0
  17. package/dist/admin/pages/InventoryPage.js +135 -0
  18. package/dist/admin/pages/InventoryPage.js.map +1 -0
  19. package/dist/admin/pages/LowStockPage.d.ts +16 -0
  20. package/dist/admin/pages/LowStockPage.d.ts.map +1 -0
  21. package/dist/admin/pages/LowStockPage.js +61 -0
  22. package/dist/admin/pages/LowStockPage.js.map +1 -0
  23. package/dist/admin/pages/StockImportWizard.d.ts +17 -0
  24. package/dist/admin/pages/StockImportWizard.d.ts.map +1 -0
  25. package/dist/admin/pages/StockImportWizard.js +111 -0
  26. package/dist/admin/pages/StockImportWizard.js.map +1 -0
  27. package/dist/admin/pages/WarehouseEditor.d.ts +12 -0
  28. package/dist/admin/pages/WarehouseEditor.d.ts.map +1 -0
  29. package/dist/admin/pages/WarehouseEditor.js +194 -0
  30. package/dist/admin/pages/WarehouseEditor.js.map +1 -0
  31. package/dist/admin/pages/WarehousesList.d.ts +12 -0
  32. package/dist/admin/pages/WarehousesList.d.ts.map +1 -0
  33. package/dist/admin/pages/WarehousesList.js +69 -0
  34. package/dist/admin/pages/WarehousesList.js.map +1 -0
  35. package/dist/admin/zones/ChannelWarehouses.d.ts +49 -0
  36. package/dist/admin/zones/ChannelWarehouses.d.ts.map +1 -0
  37. package/dist/admin/zones/ChannelWarehouses.js +109 -0
  38. package/dist/admin/zones/ChannelWarehouses.js.map +1 -0
  39. package/dist/backend/demo/reset.d.ts +18 -0
  40. package/dist/backend/demo/reset.d.ts.map +1 -0
  41. package/dist/backend/demo/reset.js +20 -0
  42. package/dist/backend/demo/reset.js.map +1 -0
  43. package/dist/backend/demo/rows.d.ts +38 -0
  44. package/dist/backend/demo/rows.d.ts.map +1 -0
  45. package/dist/backend/demo/rows.js +38 -0
  46. package/dist/backend/demo/rows.js.map +1 -0
  47. package/dist/backend/demo/seed.d.ts +26 -0
  48. package/dist/backend/demo/seed.d.ts.map +1 -0
  49. package/dist/backend/demo/seed.js +65 -0
  50. package/dist/backend/demo/seed.js.map +1 -0
  51. package/dist/backend/email-templates/transactional-defaults.d.ts +11 -0
  52. package/dist/backend/email-templates/transactional-defaults.d.ts.map +1 -0
  53. package/dist/backend/email-templates/transactional-defaults.js +49 -0
  54. package/dist/backend/email-templates/transactional-defaults.js.map +1 -0
  55. package/dist/backend/entities/availability-notification.entity.d.ts +49 -0
  56. package/dist/backend/entities/availability-notification.entity.d.ts.map +1 -0
  57. package/dist/backend/entities/availability-notification.entity.js +93 -0
  58. package/dist/backend/entities/availability-notification.entity.js.map +1 -0
  59. package/dist/backend/entities/inventory-threshold.entity.d.ts +24 -0
  60. package/dist/backend/entities/inventory-threshold.entity.d.ts.map +1 -0
  61. package/dist/backend/entities/inventory-threshold.entity.js +61 -0
  62. package/dist/backend/entities/inventory-threshold.entity.js.map +1 -0
  63. package/dist/backend/entities/product-warehouse-low-stock-threshold.entity.d.ts +15 -0
  64. package/dist/backend/entities/product-warehouse-low-stock-threshold.entity.d.ts.map +1 -0
  65. package/dist/backend/entities/product-warehouse-low-stock-threshold.entity.js +51 -0
  66. package/dist/backend/entities/product-warehouse-low-stock-threshold.entity.js.map +1 -0
  67. package/dist/backend/entities/stock-allocation.entity.d.ts +22 -0
  68. package/dist/backend/entities/stock-allocation.entity.d.ts.map +1 -0
  69. package/dist/backend/entities/stock-allocation.entity.js +69 -0
  70. package/dist/backend/entities/stock-allocation.entity.js.map +1 -0
  71. package/dist/backend/entities/stock-level.entity.d.ts +24 -0
  72. package/dist/backend/entities/stock-level.entity.d.ts.map +1 -0
  73. package/dist/backend/entities/stock-level.entity.js +74 -0
  74. package/dist/backend/entities/stock-level.entity.js.map +1 -0
  75. package/dist/backend/entities/warehouse-channel-assignment.entity.d.ts +17 -0
  76. package/dist/backend/entities/warehouse-channel-assignment.entity.d.ts.map +1 -0
  77. package/dist/backend/entities/warehouse-channel-assignment.entity.js +60 -0
  78. package/dist/backend/entities/warehouse-channel-assignment.entity.js.map +1 -0
  79. package/dist/backend/entities/warehouse.entity.d.ts +37 -0
  80. package/dist/backend/entities/warehouse.entity.d.ts.map +1 -0
  81. package/dist/backend/entities/warehouse.entity.js +90 -0
  82. package/dist/backend/entities/warehouse.entity.js.map +1 -0
  83. package/dist/backend/index.d.ts +139 -0
  84. package/dist/backend/index.d.ts.map +1 -0
  85. package/dist/backend/index.js +351 -0
  86. package/dist/backend/index.js.map +1 -0
  87. package/dist/backend/plugin.d.ts +113 -0
  88. package/dist/backend/plugin.d.ts.map +1 -0
  89. package/dist/backend/plugin.js +75 -0
  90. package/dist/backend/plugin.js.map +1 -0
  91. package/dist/backend/prompt-tools.d.ts +23 -0
  92. package/dist/backend/prompt-tools.d.ts.map +1 -0
  93. package/dist/backend/prompt-tools.js +77 -0
  94. package/dist/backend/prompt-tools.js.map +1 -0
  95. package/dist/backend/routes.admin.d.ts +62 -0
  96. package/dist/backend/routes.admin.d.ts.map +1 -0
  97. package/dist/backend/routes.admin.js +383 -0
  98. package/dist/backend/routes.admin.js.map +1 -0
  99. package/dist/backend/routes.d.ts +40 -0
  100. package/dist/backend/routes.d.ts.map +1 -0
  101. package/dist/backend/routes.js +263 -0
  102. package/dist/backend/routes.js.map +1 -0
  103. package/dist/backend/services/audit-references.d.ts +15 -0
  104. package/dist/backend/services/audit-references.d.ts.map +1 -0
  105. package/dist/backend/services/audit-references.js +27 -0
  106. package/dist/backend/services/audit-references.js.map +1 -0
  107. package/dist/backend/services/availability-notification-service.d.ts +155 -0
  108. package/dist/backend/services/availability-notification-service.d.ts.map +1 -0
  109. package/dist/backend/services/availability-notification-service.js +363 -0
  110. package/dist/backend/services/availability-notification-service.js.map +1 -0
  111. package/dist/backend/services/availability-worker.d.ts +57 -0
  112. package/dist/backend/services/availability-worker.d.ts.map +1 -0
  113. package/dist/backend/services/availability-worker.js +135 -0
  114. package/dist/backend/services/availability-worker.js.map +1 -0
  115. package/dist/backend/services/csv-stock-importer.d.ts +52 -0
  116. package/dist/backend/services/csv-stock-importer.d.ts.map +1 -0
  117. package/dist/backend/services/csv-stock-importer.js +154 -0
  118. package/dist/backend/services/csv-stock-importer.js.map +1 -0
  119. package/dist/backend/services/display-band-resolver.d.ts +19 -0
  120. package/dist/backend/services/display-band-resolver.d.ts.map +1 -0
  121. package/dist/backend/services/display-band-resolver.js +21 -0
  122. package/dist/backend/services/display-band-resolver.js.map +1 -0
  123. package/dist/backend/services/effective-fulfilment-strategy.d.ts +16 -0
  124. package/dist/backend/services/effective-fulfilment-strategy.d.ts.map +1 -0
  125. package/dist/backend/services/effective-fulfilment-strategy.js +16 -0
  126. package/dist/backend/services/effective-fulfilment-strategy.js.map +1 -0
  127. package/dist/backend/services/fulfilment-strategy-resolver.d.ts +13 -0
  128. package/dist/backend/services/fulfilment-strategy-resolver.d.ts.map +1 -0
  129. package/dist/backend/services/fulfilment-strategy-resolver.js +13 -0
  130. package/dist/backend/services/fulfilment-strategy-resolver.js.map +1 -0
  131. package/dist/backend/services/inventory-read-port.d.ts +65 -0
  132. package/dist/backend/services/inventory-read-port.d.ts.map +1 -0
  133. package/dist/backend/services/inventory-read-port.js +168 -0
  134. package/dist/backend/services/inventory-read-port.js.map +1 -0
  135. package/dist/backend/services/inventory-reservation-apply-port.d.ts +62 -0
  136. package/dist/backend/services/inventory-reservation-apply-port.d.ts.map +1 -0
  137. package/dist/backend/services/inventory-reservation-apply-port.js +134 -0
  138. package/dist/backend/services/inventory-reservation-apply-port.js.map +1 -0
  139. package/dist/backend/services/low-stock-alert-service.d.ts +87 -0
  140. package/dist/backend/services/low-stock-alert-service.d.ts.map +1 -0
  141. package/dist/backend/services/low-stock-alert-service.js +164 -0
  142. package/dist/backend/services/low-stock-alert-service.js.map +1 -0
  143. package/dist/backend/services/product-threshold-write.service.d.ts +38 -0
  144. package/dist/backend/services/product-threshold-write.service.d.ts.map +1 -0
  145. package/dist/backend/services/product-threshold-write.service.js +80 -0
  146. package/dist/backend/services/product-threshold-write.service.js.map +1 -0
  147. package/dist/backend/services/stock-import.service.d.ts +43 -0
  148. package/dist/backend/services/stock-import.service.d.ts.map +1 -0
  149. package/dist/backend/services/stock-import.service.js +124 -0
  150. package/dist/backend/services/stock-import.service.js.map +1 -0
  151. package/dist/backend/services/stock-level-service.d.ts +146 -0
  152. package/dist/backend/services/stock-level-service.d.ts.map +1 -0
  153. package/dist/backend/services/stock-level-service.js +489 -0
  154. package/dist/backend/services/stock-level-service.js.map +1 -0
  155. package/dist/backend/services/threshold-admin-service.d.ts +73 -0
  156. package/dist/backend/services/threshold-admin-service.d.ts.map +1 -0
  157. package/dist/backend/services/threshold-admin-service.js +167 -0
  158. package/dist/backend/services/threshold-admin-service.js.map +1 -0
  159. package/dist/backend/services/threshold-resolver.d.ts +23 -0
  160. package/dist/backend/services/threshold-resolver.d.ts.map +1 -0
  161. package/dist/backend/services/threshold-resolver.js +29 -0
  162. package/dist/backend/services/threshold-resolver.js.map +1 -0
  163. package/dist/backend/services/threshold-settings-mirror.d.ts +55 -0
  164. package/dist/backend/services/threshold-settings-mirror.d.ts.map +1 -0
  165. package/dist/backend/services/threshold-settings-mirror.js +84 -0
  166. package/dist/backend/services/threshold-settings-mirror.js.map +1 -0
  167. package/dist/backend/services/warehouse-channel-reconciler.d.ts +19 -0
  168. package/dist/backend/services/warehouse-channel-reconciler.d.ts.map +1 -0
  169. package/dist/backend/services/warehouse-channel-reconciler.js +65 -0
  170. package/dist/backend/services/warehouse-channel-reconciler.js.map +1 -0
  171. package/dist/backend/services/warehouse-channel-service.d.ts +57 -0
  172. package/dist/backend/services/warehouse-channel-service.d.ts.map +1 -0
  173. package/dist/backend/services/warehouse-channel-service.js +205 -0
  174. package/dist/backend/services/warehouse-channel-service.js.map +1 -0
  175. package/dist/backend/services/warehouse-country-reference.d.ts +14 -0
  176. package/dist/backend/services/warehouse-country-reference.d.ts.map +1 -0
  177. package/dist/backend/services/warehouse-country-reference.js +27 -0
  178. package/dist/backend/services/warehouse-country-reference.js.map +1 -0
  179. package/dist/backend/services/warehouse-service.d.ts +86 -0
  180. package/dist/backend/services/warehouse-service.d.ts.map +1 -0
  181. package/dist/backend/services/warehouse-service.js +267 -0
  182. package/dist/backend/services/warehouse-service.js.map +1 -0
  183. package/dist/manifest.d.ts +219 -0
  184. package/dist/manifest.d.ts.map +1 -0
  185. package/dist/manifest.js +377 -0
  186. package/dist/manifest.js.map +1 -0
  187. package/dist/migrations/20260503T182812_inventory_workflow.d.ts +39 -0
  188. package/dist/migrations/20260503T182812_inventory_workflow.d.ts.map +1 -0
  189. package/dist/migrations/20260503T182812_inventory_workflow.js +205 -0
  190. package/dist/migrations/20260503T182812_inventory_workflow.js.map +1 -0
  191. package/dist/migrations/20260611T140347_inventory_warehouse_default_low_stock_threshold.d.ts +14 -0
  192. package/dist/migrations/20260611T140347_inventory_warehouse_default_low_stock_threshold.d.ts.map +1 -0
  193. package/dist/migrations/20260611T140347_inventory_warehouse_default_low_stock_threshold.js +18 -0
  194. package/dist/migrations/20260611T140347_inventory_warehouse_default_low_stock_threshold.js.map +1 -0
  195. package/dist/migrations/20260611T140348_inventory_per_warehouse_low_stock_thresholds.d.ts +28 -0
  196. package/dist/migrations/20260611T140348_inventory_per_warehouse_low_stock_thresholds.d.ts.map +1 -0
  197. package/dist/migrations/20260611T140348_inventory_per_warehouse_low_stock_thresholds.js +48 -0
  198. package/dist/migrations/20260611T140348_inventory_per_warehouse_low_stock_thresholds.js.map +1 -0
  199. package/dist/migrations/20260818T081243_inventory_stock_allocation_order_item_fk.d.ts +44 -0
  200. package/dist/migrations/20260818T081243_inventory_stock_allocation_order_item_fk.d.ts.map +1 -0
  201. package/dist/migrations/20260818T081243_inventory_stock_allocation_order_item_fk.js +81 -0
  202. package/dist/migrations/20260818T081243_inventory_stock_allocation_order_item_fk.js.map +1 -0
  203. package/dist/migrations/20260830T182139_inventory_organization_attribution.d.ts +109 -0
  204. package/dist/migrations/20260830T182139_inventory_organization_attribution.d.ts.map +1 -0
  205. package/dist/migrations/20260830T182139_inventory_organization_attribution.js +178 -0
  206. package/dist/migrations/20260830T182139_inventory_organization_attribution.js.map +1 -0
  207. package/dist/migrations/20260912T125716_inventory_organization_warehouses.d.ts +34 -0
  208. package/dist/migrations/20260912T125716_inventory_organization_warehouses.d.ts.map +1 -0
  209. package/dist/migrations/20260912T125716_inventory_organization_warehouses.js +50 -0
  210. package/dist/migrations/20260912T125716_inventory_organization_warehouses.js.map +1 -0
  211. package/dist/migrations/index.d.ts +32 -0
  212. package/dist/migrations/index.d.ts.map +1 -0
  213. package/dist/migrations/index.js +39 -0
  214. package/dist/migrations/index.js.map +1 -0
  215. package/dist/ports/index.d.ts +183 -0
  216. package/dist/ports/index.d.ts.map +1 -0
  217. package/dist/ports/index.js +2 -0
  218. package/dist/ports/index.js.map +1 -0
  219. package/docs/inventory.md +177 -0
  220. package/i18n/en.json +32 -0
  221. package/i18n/pl.json +32 -0
  222. package/package.json +105 -0
  223. package/tailwind.css +14 -0
@@ -0,0 +1,177 @@
1
+ ---
2
+ title: inventory
3
+ description: Stock levels, reservations, availability notifications
4
+ ---
5
+
6
+ # `inventory`
7
+
8
+ Multi-warehouse stock module: warehouse identity, per-`(product, warehouse)`
9
+ on-hand and reserved counters, sales-channel binding, low-stock alerts,
10
+ display bands, backorder + unmanaged + notify-when-available, CSV import,
11
+ and per-line `stock_allocations` writes.
12
+
13
+ This module replaces the foundation's single-bucket model. Migration 030
14
+ keeps the foundation `stock_levels` table but
15
+ extends its uniqueness shape to `(product_id, variant_id, warehouse_id)`,
16
+ seeds a `Default` warehouse with the deterministic UUID
17
+ `00000000-0000-4000-8000-00000000d017`, and pairs every active sales
18
+ channel with that warehouse via the new
19
+ `warehouse_channel_assignments` table.
20
+
21
+ ## Module-owned entities
22
+
23
+ | Entity | Purpose |
24
+ | --- | --- |
25
+ | `Warehouse` | Stocking location identity (name, code, active flag, contact, address) |
26
+ | `StockLevel` | `(product_id, variant_id, warehouse_id)` row carrying `on_hand` + `reserved` |
27
+ | `WarehouseChannelAssignment` | m:n binding warehouse ↔ sales channel; at most one `is_default = true` per channel |
28
+ | `InventoryThreshold` | Display-band thresholds at `global` / `category` / `product` scope |
29
+ | `StockAllocation` | One row per `(order_item, warehouse)` — fulfilment provenance + release support |
30
+ | `AvailabilityNotification` | Customer or anonymous email subscribed to a back-in-stock signal |
31
+
32
+ ## Settings (Module Settings)
33
+
34
+ Seven keys under the `inventory` group:
35
+
36
+ | Code | Type | Default | Notes |
37
+ | --- | --- | --- | --- |
38
+ | `inventory.display_mode` | string | `band` | Storefront display: `exact` / `band` / `available_or_not` |
39
+ | `inventory.fulfilment_strategy` | string | `default_first` | `any` / `default_first` / `lowest_stock_first` / `highest_stock_first` / `defined_order` |
40
+ | `inventory.fulfilment_strategy_warehouse_order` | json | `[]` | Walk order for `defined_order` strategy |
41
+ | `inventory.global_threshold_high` | number | `100` | Cumulative on-hand at-or-above which a product is "high stock" |
42
+ | `inventory.global_threshold_medium` | number | `20` | At-or-above for "medium" |
43
+ | `inventory.global_threshold_low` | number | `1` | At-or-above for "low"; below is out-of-stock |
44
+ | `inventory.low_stock_alert_recipient_email` | string | `''` | Empty falls back to env `INVENTORY_LOW_STOCK_RECIPIENT` |
45
+
46
+ ## Public surface
47
+
48
+ Admin routes are gated by `inventory:read` (read) / `inventory:write` (write) —
49
+ this module's own codes since 2026-08-29. All 21 used to enforce `orders:read`
50
+ and `catalog:write`; see **Permissions** below.
51
+
52
+ The table lists all 21 admin sites. It listed ten until 2026-08-29 and omitted
53
+ the per-(product, warehouse) threshold write and both foundation
54
+ backward-compatibility routes, which is the kind of gap the four preceding
55
+ permission repairs each found in a module page.
56
+
57
+ | Verb + Path | Permission | Purpose |
58
+ | --- | --- | --- |
59
+ | `GET /api/v1/admin/inventory` | `inventory:read` | Landing KPIs: products tracked, total on-hand, out-of-stock count, low-stock count, per-warehouse totals |
60
+ | `GET /api/v1/admin/inventory/levels` | `inventory:read` | Per-product roster with cumulative on-hand, per-warehouse breakdown, display band |
61
+ | `PUT /api/v1/admin/inventory/levels` | `inventory:write` | Set absolute on-hand for `(productId, warehouseId, variantId?)`; emits `inventory.adjusted.v1` |
62
+ | `PUT /api/v1/admin/inventory/warehouse-low-stock-thresholds` | `inventory:write` | Per-(product, warehouse) low-stock thresholds |
63
+ | `GET /api/v1/admin/inventory/low-stock` | `inventory:read` | Products whose cumulative on-hand is at-or-below `lowStockThreshold` |
64
+ | `GET /api/v1/admin/inventory/thresholds` | `inventory:read` | Read global / per-category / per-product display-band thresholds |
65
+ | `PATCH /api/v1/admin/inventory/thresholds` | `inventory:write` | Update them |
66
+ | `GET /api/v1/admin/warehouses[/:id]` | `inventory:read` | Warehouse list and detail |
67
+ | `POST/PATCH/DELETE /api/v1/admin/warehouses[/:id]` | `inventory:write` | Warehouse CRUD; refuses delete when warehouse is a channel default or holds stock |
68
+ | `GET /api/v1/admin/sales-channels/:id/warehouses` | `inventory:read` | The channel's bound warehouses |
69
+ | `POST/PATCH/DELETE /api/v1/admin/sales-channels/:id/warehouses[/:assignmentId]` | `inventory:write` | Sales channel ↔ warehouse binding with at most one default per channel |
70
+ | `GET /api/v1/admin/inventory/availability-notifications` | `inventory:read` | Admin browses the back-in-stock queue |
71
+ | `PATCH /api/v1/admin/inventory/availability-notifications/:id` | `inventory:write` | Cancels a subscription |
72
+ | `POST /api/v1/admin/inventory/import` | `inventory:write` | CSV stock import (`?dryRun=true` validates without writing) |
73
+ | `PUT /api/v1/admin/inventory` | `inventory:write` | **Deprecated** foundation single-bucket write; delegates to `StockLevelService.setOnHand` against the seeded Default warehouse |
74
+ | `GET /api/v1/admin/inventory/legacy` | `inventory:read` | **Deprecated** foundation single-bucket list |
75
+ | `GET /api/v1/storefront/inventory/display-mode` | — | Storefront-public read: which display mode the channel uses |
76
+
77
+ ## Permissions
78
+
79
+ `inventory:read` and `inventory:write`, this module's own since 2026-08-29.
80
+
81
+ Before that all 21 admin routes enforced two other modules' codes — nine reads
82
+ on `orders:read` and twelve writes on `catalog:write`. So whoever could edit a
83
+ product description could create, rename and delete a warehouse, rewrite a stock
84
+ count, run a CSV import across every product's stock, and bind or unbind a
85
+ warehouse from a sales channel; and whoever could read orders could enumerate
86
+ every warehouse and the address on it. Neither code names the data being
87
+ touched, which is the discriminator permission ownership is decided on: per
88
+ route rather than per module.
89
+
90
+ There is **no data migration**: a role that reached these screens through
91
+ `catalog:write` or `orders:read` is granted the new codes explicitly, on
92
+ `/admin-roles`, where the manifest puts them automatically. Granting them to
93
+ every holder of the old codes would reproduce the over-grant the split removes.
94
+
95
+ `test/contract/inventory/permission-authority.test.ts` pins both directions and
96
+ both old codes.
97
+ | `GET /api/v1/storefront/inventory/stock/:id` | Storefront-public per-product stock with cumulative on-hand summed only over the caller's channel-bound warehouses |
98
+ | `POST /api/v1/catalog/products/:id/notify-when-available` | Customer subscribes to back-in-stock; signed-in callers have email pre-filled |
99
+
100
+ ### Deprecated
101
+
102
+ Two foundation routes predate the per-warehouse surface above and always
103
+ address the seeded Default warehouse. Nothing in the platform calls either one
104
+ — no admin screen, no admin API client call, no seed, no script — so they
105
+ exist for a deployment's own integration and nothing else. Do not build against
106
+ them.
107
+
108
+ | Verb + Path | Purpose | Replacement |
109
+ | --- | --- | --- |
110
+ | `PUT /api/v1/admin/inventory` | Set absolute on-hand for `(productId, variantId?)` in the Default warehouse | `PUT /api/v1/admin/inventory/levels`, which takes an explicit `warehouseId` |
111
+ | `GET /api/v1/admin/inventory/legacy` | Flat `stock_levels` rows, newest first, optionally filtered by `productId` | `GET /api/v1/admin/inventory/levels` for everything except `variantId` and `updatedAt`, which it does not carry |
112
+
113
+ The `PUT` now delegates to the same service as
114
+ `PUT .../levels`, so it emits `inventory.adjusted.v1` and answers `404` for an
115
+ unknown product instead of writing a stock row for one. It will be removed once
116
+ a production access log or the deployment owner confirms nothing calls it.
117
+
118
+ ## Per-product flags
119
+
120
+ Five new fields live on `products` and ride through `PATCH /api/v1/admin/catalog/products/:id`:
121
+
122
+ - `manageStock` (default `true`) — when `false`, the storefront treats the product as always available and the cart/order paths skip reservation entirely.
123
+ - `backorderEnabled` (default `false`) — when `true`, zero-stock checkout is accepted; the resulting `stock_allocations` row is flagged `is_backorder = true`.
124
+ - `lowStockThreshold` — optional integer; when null the product is exempt from low-stock alerts.
125
+ - `fulfilmentStrategy` — per-product override of the global strategy.
126
+ - `fulfilmentStrategyWarehouseOrder` — when the strategy is `defined_order`, the ordered list of warehouse UUIDs to walk.
127
+
128
+ ## Display-band resolution
129
+
130
+ `(product, category[], global)` triple lookup runs per-key (high / medium / low) so a product can override only `low` while inheriting `high` and `medium` from the global default. The resolver lives at
131
+ `packages/modules/inventory/src/backend/services/threshold-resolver.ts` and is a pure function with full unit-test coverage.
132
+
133
+ The display-band resolver in
134
+ `packages/modules/inventory/src/backend/services/display-band-resolver.ts` then maps cumulative on-hand to one of `high | medium | low | out_of_stock | available` (`available` is the special bucket for `manageStock = false`).
135
+
136
+ ## Fulfilment strategies
137
+
138
+ Five strategies live in `fulfilment-strategy-resolver.ts`:
139
+
140
+ | Strategy | Behaviour |
141
+ | --- | --- |
142
+ | `any` | Pick the first warehouse (lex by code) that can satisfy the line in full |
143
+ | `default_first` | The only strategy that splits a line across warehouses; default warehouse first, others lex by code |
144
+ | `lowest_stock_first` | Pick the warehouse with the smallest sufficient `available` (lex tie-break) |
145
+ | `highest_stock_first` | Pick the warehouse with the largest `available` (lex tie-break) |
146
+ | `defined_order` | Walk the configured warehouse-id list in order; first sufficient wins |
147
+
148
+ When `backorderEnabled = true`, all five strategies allow the line to go through with the residual flagged as a backorder against the first-choice warehouse.
149
+
150
+ The order-placement path writes one `stock_allocations` row per order item. Cancellation runs `OrderService.releaseAllocations(orderId)` which decrements `stock_levels.reserved` per allocation and stamps `released_at`.
151
+
152
+ ## Reserve / release contract
153
+
154
+ `OrderService.placeOrder` opens a `SELECT … FOR UPDATE` per `(product_id, variant_id, warehouse_id)` row inside the placement transaction. The default warehouse is resolved from `warehouse_channel_assignments` for the order's sales channel. Concurrent placers serialise; the loser raises `409 STOCK_UNAVAILABLE` unless `backorderEnabled = true` on the product, in which case the line goes through with `is_backorder = true`.
155
+
156
+ `releaseAllocations(orderId)` is idempotent — already-released rows are filtered out by `released_at IS NULL`. It runs automatically on order cancellation alongside the credit-limit release.
157
+
158
+ ## Notify-when-available
159
+
160
+ The customer subscribes via either the storefront `/notify-when-available` endpoint (signed-in path; email pre-filled) or the customer-side dialog (anonymous; email supplied in the body). Subscription is refused with `PRODUCT_UNMANAGED_STOCK` when the product has opted out of stock tracking, and idempotent re-subscribes return the existing row.
161
+
162
+ `AvailabilityWorker.attach(eventBus)` listens for `inventory.adjusted.v1` events. Fan-out fires only when *cumulative across warehouses* crosses 0 → > 0 — single-warehouse top-ups that don't bring the cumulative above zero never trigger emails.
163
+
164
+ ## Low-stock alerts
165
+
166
+ `LowStockAlertService.attach(eventBus)` listens for the same event. When cumulative on-hand crosses from above the product's `lowStockThreshold` to at-or-below it, one email goes to the recipient configured by `inventory.low_stock_alert_recipient_email`. The detector is platform-wide, not per-channel.
167
+
168
+ ## Boot-time reconciler
169
+
170
+ `WarehouseChannelReconciler` runs at boot AFTER `DefaultChannelReconciler` so every active sales channel ends up paired with at least one warehouse and exactly one `is_default` assignment. The reconciler is idempotent and handles the case where channels are created post-migration.
171
+
172
+ ## Constants
173
+
174
+ - `DEFAULT_WAREHOUSE_ID` = `00000000-0000-4000-8000-00000000d017`
175
+ - `DEFAULT_WAREHOUSE_CODE` = `default`
176
+
177
+ Both are exported from `packages/modules/inventory/src/backend/entities/warehouse.entity.ts`.
package/i18n/en.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "actions.openInventory.label": "Open inventory",
3
+ "actions.openInventory.description": "View and manage warehouse stock levels",
4
+ "errors.STOCK_UNAVAILABLE": "Stock Unavailable.",
5
+ "errors.WAREHOUSE_NOT_FOUND": "Warehouse Not Found.",
6
+ "errors.WAREHOUSE_CODE_TAKEN": "Warehouse Code Taken.",
7
+ "errors.WAREHOUSE_INVALID_CODE": "Warehouse Invalid Code.",
8
+ "errors.WAREHOUSE_CANNOT_DELETE_DEFAULT": "Warehouse Cannot Delete Default.",
9
+ "errors.WAREHOUSE_IS_DEFAULT_FOR_CHANNELS": "Warehouse Is Default For Channels.",
10
+ "errors.WAREHOUSE_HAS_STOCK": "Warehouse Has Stock.",
11
+ "errors.CHANNEL_NO_WAREHOUSES": "Channel No Warehouses.",
12
+ "errors.CHANNEL_WAREHOUSE_NOT_FOUND": "Channel Warehouse Not Found.",
13
+ "errors.STOCK_LEVEL_NOT_FOUND": "Stock Level Not Found.",
14
+ "errors.THRESHOLDS_INVALID": "Thresholds Invalid.",
15
+ "errors.AVAILABILITY_NOTIFICATION_NOT_FOUND": "Availability Notification Not Found.",
16
+ "errors.STOCK_IMPORT_INVALID_FILE": "Stock Import Invalid File.",
17
+ "activity.verb.low_stock_threshold.create": "set low-stock threshold for",
18
+ "activity.verb.low_stock_threshold.delete": "removed low-stock threshold for",
19
+ "activity.verb.low_stock_threshold.update": "updated low-stock threshold for",
20
+ "activity.verb.stock_level.adjust": "adjusted stock for",
21
+ "activity.verb.stock_level.bulk_import": "imported stock levels:",
22
+ "activity.verb.warehouse.create": "created warehouse",
23
+ "activity.verb.warehouse.deactivate": "deactivated warehouse",
24
+ "activity.verb.warehouse.reactivate": "reactivated warehouse",
25
+ "activity.verb.warehouse.update": "updated warehouse",
26
+
27
+ "nav.stockOverview.label": "Stock overview",
28
+ "nav.warehouses.label": "Warehouses",
29
+ "nav.lowStock.label": "Low stock",
30
+ "nav.notifyWhenAvailable.label": "Notify-when-available",
31
+ "nav.importStock.label": "Import stock"
32
+ }
package/i18n/pl.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "actions.openInventory.label": "Otwórz zapasy",
3
+ "actions.openInventory.description": "Wyświetl i zarządzaj stanami magazynowymi",
4
+ "errors.STOCK_UNAVAILABLE": "Błąd: stock unavailable.",
5
+ "errors.WAREHOUSE_NOT_FOUND": "Błąd: warehouse not found.",
6
+ "errors.WAREHOUSE_CODE_TAKEN": "Błąd: warehouse code taken.",
7
+ "errors.WAREHOUSE_INVALID_CODE": "Błąd: warehouse invalid code.",
8
+ "errors.WAREHOUSE_CANNOT_DELETE_DEFAULT": "Błąd: warehouse cannot delete default.",
9
+ "errors.WAREHOUSE_IS_DEFAULT_FOR_CHANNELS": "Błąd: warehouse is default for channels.",
10
+ "errors.WAREHOUSE_HAS_STOCK": "Błąd: warehouse has stock.",
11
+ "errors.CHANNEL_NO_WAREHOUSES": "Błąd: channel no warehouses.",
12
+ "errors.CHANNEL_WAREHOUSE_NOT_FOUND": "Błąd: channel warehouse not found.",
13
+ "errors.STOCK_LEVEL_NOT_FOUND": "Błąd: stock level not found.",
14
+ "errors.THRESHOLDS_INVALID": "Błąd: thresholds invalid.",
15
+ "errors.AVAILABILITY_NOTIFICATION_NOT_FOUND": "Błąd: availability notification not found.",
16
+ "errors.STOCK_IMPORT_INVALID_FILE": "Błąd: stock import invalid file.",
17
+ "activity.verb.low_stock_threshold.create": "ustawił(a) próg niskiego stanu dla",
18
+ "activity.verb.low_stock_threshold.delete": "usunął(ęła) próg niskiego stanu dla",
19
+ "activity.verb.low_stock_threshold.update": "zaktualizował(a) próg niskiego stanu dla",
20
+ "activity.verb.stock_level.adjust": "skorygował(a) stan magazynowy",
21
+ "activity.verb.stock_level.bulk_import": "zaimportował(a) stany magazynowe:",
22
+ "activity.verb.warehouse.create": "utworzył(a) magazyn",
23
+ "activity.verb.warehouse.deactivate": "dezaktywował(a) magazyn",
24
+ "activity.verb.warehouse.reactivate": "ponownie aktywował(a) magazyn",
25
+ "activity.verb.warehouse.update": "zaktualizował(a) magazyn",
26
+
27
+ "nav.stockOverview.label": "Stany magazynowe",
28
+ "nav.warehouses.label": "Magazyny",
29
+ "nav.lowStock.label": "Niskie stany",
30
+ "nav.notifyWhenAvailable.label": "Powiadom o dostępności",
31
+ "nav.importStock.label": "Import stanów"
32
+ }
package/package.json ADDED
@@ -0,0 +1,105 @@
1
+ {
2
+ "name": "@endora-commerce/mod-inventory",
3
+ "version": "0.100.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Multi-warehouse stock levels, fulfilment strategy, and storefront display modes.",
7
+ "license": "MIT",
8
+ "endora": {
9
+ "type": "module",
10
+ "id": "inventory"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/endora-commerce/endora-commerce.git",
15
+ "directory": "packages/modules/inventory"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/manifest.d.ts",
23
+ "default": "./dist/manifest.js"
24
+ },
25
+ "./backend": {
26
+ "types": "./dist/backend/index.d.ts",
27
+ "default": "./dist/backend/index.js"
28
+ },
29
+ "./migrations": {
30
+ "types": "./dist/migrations/index.d.ts",
31
+ "default": "./dist/migrations/index.js"
32
+ },
33
+ "./ports": {
34
+ "types": "./dist/ports/index.d.ts",
35
+ "default": "./dist/ports/index.js"
36
+ },
37
+ "./admin": {
38
+ "types": "./dist/admin/index.d.ts",
39
+ "default": "./dist/admin/index.js"
40
+ },
41
+ "./tailwind.css": "./tailwind.css",
42
+ "./package.json": "./package.json"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "i18n",
47
+ "docs",
48
+ "tailwind.css"
49
+ ],
50
+ "engines": {
51
+ "node": ">=22.18.0"
52
+ },
53
+ "peerDependencies": {
54
+ "@mikro-orm/core": "^6",
55
+ "@mikro-orm/migrations": "^6",
56
+ "@mikro-orm/postgresql": "^6",
57
+ "fastify": "^5",
58
+ "lucide-react": "^1",
59
+ "react": "^19",
60
+ "react-router-dom": "^7",
61
+ "zod": "^4",
62
+ "@endora-commerce/admin-kit": "0.100.0",
63
+ "@endora-commerce/contracts": "0.100.0",
64
+ "@endora-commerce/email-components": "0.100.0",
65
+ "@endora-commerce/platform": "0.100.0"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "@endora-commerce/admin-kit": {
69
+ "optional": true
70
+ },
71
+ "lucide-react": {
72
+ "optional": true
73
+ },
74
+ "react": {
75
+ "optional": true
76
+ },
77
+ "react-router-dom": {
78
+ "optional": true
79
+ }
80
+ },
81
+ "devDependencies": {
82
+ "@mikro-orm/core": "^6.6.13",
83
+ "@mikro-orm/migrations": "^6.6.13",
84
+ "@mikro-orm/postgresql": "^6.6.13",
85
+ "@types/node": "^22.9.0",
86
+ "@types/react": "^19.2.14",
87
+ "fastify": "^5.12.5",
88
+ "lucide-react": "^1.11.0",
89
+ "react": "^19.2.5",
90
+ "react-router-dom": "^7.18.2",
91
+ "typescript": "^5.9.3",
92
+ "vitest": "^4.1.11",
93
+ "zod": "^4.2.0",
94
+ "@endora-commerce/admin-kit": "0.100.0",
95
+ "@endora-commerce/contracts": "0.100.0",
96
+ "@endora-commerce/email-components": "0.100.0",
97
+ "@endora-commerce/platform": "0.100.0"
98
+ },
99
+ "scripts": {
100
+ "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.ui.json",
101
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.ui.json --noEmit",
102
+ "lint": "eslint src",
103
+ "test": "vitest run"
104
+ }
105
+ }
package/tailwind.css ADDED
@@ -0,0 +1,14 @@
1
+ /* @endora-commerce/mod-inventory — AUTO-GENERATED by `pnpm --filter backend run manifests:generate`.
2
+ *
3
+ * The `@source` directives this package asks its host to scan
4
+ * (`specs/110-instance-repository/contracts/admin-stylesheet-composition.md` R1).
5
+ * They resolve relative to **this file**, so they hold wherever the package is
6
+ * installed — a workspace link here, `node_modules` in a client's instance.
7
+ *
8
+ * The `dist` line is what a published tarball ships and is what an instance
9
+ * scans; the `src` line is inert there and is what keeps `pnpm --filter admin
10
+ * run dev` reading source in this repository. Do not edit: run
11
+ * `pnpm --filter backend run manifests:generate`.
12
+ */
13
+ @source "./dist/admin";
14
+ @source "./src/admin";