@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.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/admin/api/warehouses-client.d.ts +22 -0
- package/dist/admin/api/warehouses-client.d.ts.map +1 -0
- package/dist/admin/api/warehouses-client.js +32 -0
- package/dist/admin/api/warehouses-client.js.map +1 -0
- package/dist/admin/index.d.ts +43 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +190 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/AvailabilityNotificationsPage.d.ts +15 -0
- package/dist/admin/pages/AvailabilityNotificationsPage.d.ts.map +1 -0
- package/dist/admin/pages/AvailabilityNotificationsPage.js +111 -0
- package/dist/admin/pages/AvailabilityNotificationsPage.js.map +1 -0
- package/dist/admin/pages/InventoryPage.d.ts +17 -0
- package/dist/admin/pages/InventoryPage.d.ts.map +1 -0
- package/dist/admin/pages/InventoryPage.js +135 -0
- package/dist/admin/pages/InventoryPage.js.map +1 -0
- package/dist/admin/pages/LowStockPage.d.ts +16 -0
- package/dist/admin/pages/LowStockPage.d.ts.map +1 -0
- package/dist/admin/pages/LowStockPage.js +61 -0
- package/dist/admin/pages/LowStockPage.js.map +1 -0
- package/dist/admin/pages/StockImportWizard.d.ts +17 -0
- package/dist/admin/pages/StockImportWizard.d.ts.map +1 -0
- package/dist/admin/pages/StockImportWizard.js +111 -0
- package/dist/admin/pages/StockImportWizard.js.map +1 -0
- package/dist/admin/pages/WarehouseEditor.d.ts +12 -0
- package/dist/admin/pages/WarehouseEditor.d.ts.map +1 -0
- package/dist/admin/pages/WarehouseEditor.js +194 -0
- package/dist/admin/pages/WarehouseEditor.js.map +1 -0
- package/dist/admin/pages/WarehousesList.d.ts +12 -0
- package/dist/admin/pages/WarehousesList.d.ts.map +1 -0
- package/dist/admin/pages/WarehousesList.js +69 -0
- package/dist/admin/pages/WarehousesList.js.map +1 -0
- package/dist/admin/zones/ChannelWarehouses.d.ts +49 -0
- package/dist/admin/zones/ChannelWarehouses.d.ts.map +1 -0
- package/dist/admin/zones/ChannelWarehouses.js +109 -0
- package/dist/admin/zones/ChannelWarehouses.js.map +1 -0
- package/dist/backend/demo/reset.d.ts +18 -0
- package/dist/backend/demo/reset.d.ts.map +1 -0
- package/dist/backend/demo/reset.js +20 -0
- package/dist/backend/demo/reset.js.map +1 -0
- package/dist/backend/demo/rows.d.ts +38 -0
- package/dist/backend/demo/rows.d.ts.map +1 -0
- package/dist/backend/demo/rows.js +38 -0
- package/dist/backend/demo/rows.js.map +1 -0
- package/dist/backend/demo/seed.d.ts +26 -0
- package/dist/backend/demo/seed.d.ts.map +1 -0
- package/dist/backend/demo/seed.js +65 -0
- package/dist/backend/demo/seed.js.map +1 -0
- package/dist/backend/email-templates/transactional-defaults.d.ts +11 -0
- package/dist/backend/email-templates/transactional-defaults.d.ts.map +1 -0
- package/dist/backend/email-templates/transactional-defaults.js +49 -0
- package/dist/backend/email-templates/transactional-defaults.js.map +1 -0
- package/dist/backend/entities/availability-notification.entity.d.ts +49 -0
- package/dist/backend/entities/availability-notification.entity.d.ts.map +1 -0
- package/dist/backend/entities/availability-notification.entity.js +93 -0
- package/dist/backend/entities/availability-notification.entity.js.map +1 -0
- package/dist/backend/entities/inventory-threshold.entity.d.ts +24 -0
- package/dist/backend/entities/inventory-threshold.entity.d.ts.map +1 -0
- package/dist/backend/entities/inventory-threshold.entity.js +61 -0
- package/dist/backend/entities/inventory-threshold.entity.js.map +1 -0
- package/dist/backend/entities/product-warehouse-low-stock-threshold.entity.d.ts +15 -0
- package/dist/backend/entities/product-warehouse-low-stock-threshold.entity.d.ts.map +1 -0
- package/dist/backend/entities/product-warehouse-low-stock-threshold.entity.js +51 -0
- package/dist/backend/entities/product-warehouse-low-stock-threshold.entity.js.map +1 -0
- package/dist/backend/entities/stock-allocation.entity.d.ts +22 -0
- package/dist/backend/entities/stock-allocation.entity.d.ts.map +1 -0
- package/dist/backend/entities/stock-allocation.entity.js +69 -0
- package/dist/backend/entities/stock-allocation.entity.js.map +1 -0
- package/dist/backend/entities/stock-level.entity.d.ts +24 -0
- package/dist/backend/entities/stock-level.entity.d.ts.map +1 -0
- package/dist/backend/entities/stock-level.entity.js +74 -0
- package/dist/backend/entities/stock-level.entity.js.map +1 -0
- package/dist/backend/entities/warehouse-channel-assignment.entity.d.ts +17 -0
- package/dist/backend/entities/warehouse-channel-assignment.entity.d.ts.map +1 -0
- package/dist/backend/entities/warehouse-channel-assignment.entity.js +60 -0
- package/dist/backend/entities/warehouse-channel-assignment.entity.js.map +1 -0
- package/dist/backend/entities/warehouse.entity.d.ts +37 -0
- package/dist/backend/entities/warehouse.entity.d.ts.map +1 -0
- package/dist/backend/entities/warehouse.entity.js +90 -0
- package/dist/backend/entities/warehouse.entity.js.map +1 -0
- package/dist/backend/index.d.ts +139 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +351 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +113 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +75 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/prompt-tools.d.ts +23 -0
- package/dist/backend/prompt-tools.d.ts.map +1 -0
- package/dist/backend/prompt-tools.js +77 -0
- package/dist/backend/prompt-tools.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +62 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +383 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.d.ts +40 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +263 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/services/audit-references.d.ts +15 -0
- package/dist/backend/services/audit-references.d.ts.map +1 -0
- package/dist/backend/services/audit-references.js +27 -0
- package/dist/backend/services/audit-references.js.map +1 -0
- package/dist/backend/services/availability-notification-service.d.ts +155 -0
- package/dist/backend/services/availability-notification-service.d.ts.map +1 -0
- package/dist/backend/services/availability-notification-service.js +363 -0
- package/dist/backend/services/availability-notification-service.js.map +1 -0
- package/dist/backend/services/availability-worker.d.ts +57 -0
- package/dist/backend/services/availability-worker.d.ts.map +1 -0
- package/dist/backend/services/availability-worker.js +135 -0
- package/dist/backend/services/availability-worker.js.map +1 -0
- package/dist/backend/services/csv-stock-importer.d.ts +52 -0
- package/dist/backend/services/csv-stock-importer.d.ts.map +1 -0
- package/dist/backend/services/csv-stock-importer.js +154 -0
- package/dist/backend/services/csv-stock-importer.js.map +1 -0
- package/dist/backend/services/display-band-resolver.d.ts +19 -0
- package/dist/backend/services/display-band-resolver.d.ts.map +1 -0
- package/dist/backend/services/display-band-resolver.js +21 -0
- package/dist/backend/services/display-band-resolver.js.map +1 -0
- package/dist/backend/services/effective-fulfilment-strategy.d.ts +16 -0
- package/dist/backend/services/effective-fulfilment-strategy.d.ts.map +1 -0
- package/dist/backend/services/effective-fulfilment-strategy.js +16 -0
- package/dist/backend/services/effective-fulfilment-strategy.js.map +1 -0
- package/dist/backend/services/fulfilment-strategy-resolver.d.ts +13 -0
- package/dist/backend/services/fulfilment-strategy-resolver.d.ts.map +1 -0
- package/dist/backend/services/fulfilment-strategy-resolver.js +13 -0
- package/dist/backend/services/fulfilment-strategy-resolver.js.map +1 -0
- package/dist/backend/services/inventory-read-port.d.ts +65 -0
- package/dist/backend/services/inventory-read-port.d.ts.map +1 -0
- package/dist/backend/services/inventory-read-port.js +168 -0
- package/dist/backend/services/inventory-read-port.js.map +1 -0
- package/dist/backend/services/inventory-reservation-apply-port.d.ts +62 -0
- package/dist/backend/services/inventory-reservation-apply-port.d.ts.map +1 -0
- package/dist/backend/services/inventory-reservation-apply-port.js +134 -0
- package/dist/backend/services/inventory-reservation-apply-port.js.map +1 -0
- package/dist/backend/services/low-stock-alert-service.d.ts +87 -0
- package/dist/backend/services/low-stock-alert-service.d.ts.map +1 -0
- package/dist/backend/services/low-stock-alert-service.js +164 -0
- package/dist/backend/services/low-stock-alert-service.js.map +1 -0
- package/dist/backend/services/product-threshold-write.service.d.ts +38 -0
- package/dist/backend/services/product-threshold-write.service.d.ts.map +1 -0
- package/dist/backend/services/product-threshold-write.service.js +80 -0
- package/dist/backend/services/product-threshold-write.service.js.map +1 -0
- package/dist/backend/services/stock-import.service.d.ts +43 -0
- package/dist/backend/services/stock-import.service.d.ts.map +1 -0
- package/dist/backend/services/stock-import.service.js +124 -0
- package/dist/backend/services/stock-import.service.js.map +1 -0
- package/dist/backend/services/stock-level-service.d.ts +146 -0
- package/dist/backend/services/stock-level-service.d.ts.map +1 -0
- package/dist/backend/services/stock-level-service.js +489 -0
- package/dist/backend/services/stock-level-service.js.map +1 -0
- package/dist/backend/services/threshold-admin-service.d.ts +73 -0
- package/dist/backend/services/threshold-admin-service.d.ts.map +1 -0
- package/dist/backend/services/threshold-admin-service.js +167 -0
- package/dist/backend/services/threshold-admin-service.js.map +1 -0
- package/dist/backend/services/threshold-resolver.d.ts +23 -0
- package/dist/backend/services/threshold-resolver.d.ts.map +1 -0
- package/dist/backend/services/threshold-resolver.js +29 -0
- package/dist/backend/services/threshold-resolver.js.map +1 -0
- package/dist/backend/services/threshold-settings-mirror.d.ts +55 -0
- package/dist/backend/services/threshold-settings-mirror.d.ts.map +1 -0
- package/dist/backend/services/threshold-settings-mirror.js +84 -0
- package/dist/backend/services/threshold-settings-mirror.js.map +1 -0
- package/dist/backend/services/warehouse-channel-reconciler.d.ts +19 -0
- package/dist/backend/services/warehouse-channel-reconciler.d.ts.map +1 -0
- package/dist/backend/services/warehouse-channel-reconciler.js +65 -0
- package/dist/backend/services/warehouse-channel-reconciler.js.map +1 -0
- package/dist/backend/services/warehouse-channel-service.d.ts +57 -0
- package/dist/backend/services/warehouse-channel-service.d.ts.map +1 -0
- package/dist/backend/services/warehouse-channel-service.js +205 -0
- package/dist/backend/services/warehouse-channel-service.js.map +1 -0
- package/dist/backend/services/warehouse-country-reference.d.ts +14 -0
- package/dist/backend/services/warehouse-country-reference.d.ts.map +1 -0
- package/dist/backend/services/warehouse-country-reference.js +27 -0
- package/dist/backend/services/warehouse-country-reference.js.map +1 -0
- package/dist/backend/services/warehouse-service.d.ts +86 -0
- package/dist/backend/services/warehouse-service.d.ts.map +1 -0
- package/dist/backend/services/warehouse-service.js +267 -0
- package/dist/backend/services/warehouse-service.js.map +1 -0
- package/dist/manifest.d.ts +219 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +377 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260503T182812_inventory_workflow.d.ts +39 -0
- package/dist/migrations/20260503T182812_inventory_workflow.d.ts.map +1 -0
- package/dist/migrations/20260503T182812_inventory_workflow.js +205 -0
- package/dist/migrations/20260503T182812_inventory_workflow.js.map +1 -0
- package/dist/migrations/20260611T140347_inventory_warehouse_default_low_stock_threshold.d.ts +14 -0
- package/dist/migrations/20260611T140347_inventory_warehouse_default_low_stock_threshold.d.ts.map +1 -0
- package/dist/migrations/20260611T140347_inventory_warehouse_default_low_stock_threshold.js +18 -0
- package/dist/migrations/20260611T140347_inventory_warehouse_default_low_stock_threshold.js.map +1 -0
- package/dist/migrations/20260611T140348_inventory_per_warehouse_low_stock_thresholds.d.ts +28 -0
- package/dist/migrations/20260611T140348_inventory_per_warehouse_low_stock_thresholds.d.ts.map +1 -0
- package/dist/migrations/20260611T140348_inventory_per_warehouse_low_stock_thresholds.js +48 -0
- package/dist/migrations/20260611T140348_inventory_per_warehouse_low_stock_thresholds.js.map +1 -0
- package/dist/migrations/20260818T081243_inventory_stock_allocation_order_item_fk.d.ts +44 -0
- package/dist/migrations/20260818T081243_inventory_stock_allocation_order_item_fk.d.ts.map +1 -0
- package/dist/migrations/20260818T081243_inventory_stock_allocation_order_item_fk.js +81 -0
- package/dist/migrations/20260818T081243_inventory_stock_allocation_order_item_fk.js.map +1 -0
- package/dist/migrations/20260830T182139_inventory_organization_attribution.d.ts +109 -0
- package/dist/migrations/20260830T182139_inventory_organization_attribution.d.ts.map +1 -0
- package/dist/migrations/20260830T182139_inventory_organization_attribution.js +178 -0
- package/dist/migrations/20260830T182139_inventory_organization_attribution.js.map +1 -0
- package/dist/migrations/20260912T125716_inventory_organization_warehouses.d.ts +34 -0
- package/dist/migrations/20260912T125716_inventory_organization_warehouses.d.ts.map +1 -0
- package/dist/migrations/20260912T125716_inventory_organization_warehouses.js +50 -0
- package/dist/migrations/20260912T125716_inventory_organization_warehouses.js.map +1 -0
- package/dist/migrations/index.d.ts +32 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +39 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/ports/index.d.ts +183 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +2 -0
- package/dist/ports/index.js.map +1 -0
- package/docs/inventory.md +177 -0
- package/i18n/en.json +32 -0
- package/i18n/pl.json +32 -0
- package/package.json +105 -0
- 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";
|