@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,134 @@
|
|
|
1
|
+
import { LockMode } from '@mikro-orm/core';
|
|
2
|
+
import { StockAllocation } from '../entities/stock-allocation.entity.js';
|
|
3
|
+
import { StockLevel } from '../entities/stock-level.entity.js';
|
|
4
|
+
/**
|
|
5
|
+
* `inventoryReservationApplyPort` — the stock reservation order placement
|
|
6
|
+
* performs, written on the caller's `EntityManager` (feature 080, T048; D-169).
|
|
7
|
+
*
|
|
8
|
+
* `orders` held this module's `StockLevel` and `StockAllocation` classes for it
|
|
9
|
+
* until T048, and held them **twice**: statically at the top of
|
|
10
|
+
* `order-service.ts` for the reservation, and through an `await import()`
|
|
11
|
+
* inside `releaseAllocations` for the release. The header comment above those
|
|
12
|
+
* static imports claimed D-94.4 had removed the dynamic pair; it had not, and a
|
|
13
|
+
* dynamic import is invisible to a reviewer scanning the block, which is
|
|
14
|
+
* exactly the property a boundary exception must not have. Both spellings are
|
|
15
|
+
* gone with this file.
|
|
16
|
+
*
|
|
17
|
+
* **The transaction is unchanged, and that is the point of the conversion.**
|
|
18
|
+
* The `PESSIMISTIC_WRITE` is still taken on placement's own `EntityManager` and
|
|
19
|
+
* still held until the order commits; the allocation rows are still written
|
|
20
|
+
* after the order items are flushed, because
|
|
21
|
+
* `stock_allocations_order_item_fk` is `on delete restrict`; the release still
|
|
22
|
+
* runs in the transaction `releaseAllocations` opens. What changed is that the
|
|
23
|
+
* statements are written by the module that owns the two tables, and that what
|
|
24
|
+
* crosses the boundary is `PlacementStockSnapshot` — published records —
|
|
25
|
+
* instead of managed rows the consumer could move any column of (D-77's first
|
|
26
|
+
* narrowing).
|
|
27
|
+
*
|
|
28
|
+
* The port's own doc block, in `../ports/index.js`, states the constraint, the
|
|
29
|
+
* lock and the off-state answer.
|
|
30
|
+
*/
|
|
31
|
+
export class InventoryReservationApplyService {
|
|
32
|
+
async lockAvailabilityForPlacement(em, input) {
|
|
33
|
+
const snapshot = [];
|
|
34
|
+
// One row at a time, so the lock is fine-grained — a single `$in` query
|
|
35
|
+
// would take a write lock on every candidate warehouse's row for the line
|
|
36
|
+
// whether or not the plan ends up touching it. On `em`, so every lock is
|
|
37
|
+
// held by the caller's transaction until the order commits.
|
|
38
|
+
for (const warehouseId of input.warehouseIds) {
|
|
39
|
+
const row = await em.findOne(StockLevel, { productId: input.productId, variantId: input.variantId, warehouseId }, { lockMode: LockMode.PESSIMISTIC_WRITE });
|
|
40
|
+
// A published record, never the managed entity (D-77's first narrowing).
|
|
41
|
+
// An absent row answers `0`, which is the same promise a row at zero
|
|
42
|
+
// makes; whether the write below inserts or updates is this module's to
|
|
43
|
+
// know and not the caller's.
|
|
44
|
+
snapshot.push({ warehouseId, available: row ? row.onHand - row.reserved : 0 });
|
|
45
|
+
}
|
|
46
|
+
return snapshot;
|
|
47
|
+
}
|
|
48
|
+
async reserveForPlacement(em, input) {
|
|
49
|
+
// command-coverage-ignore: the stock half of an order placement, inside the
|
|
50
|
+
// caller's transaction. `orders` audits the placement as one operation
|
|
51
|
+
// through its own Command, and a stock movement this module records
|
|
52
|
+
// separately would attribute the same decision twice; the audited stock
|
|
53
|
+
// writes are the admin adjustments in `StockLevelService`, which are
|
|
54
|
+
// decisions in their own right.
|
|
55
|
+
for (const allocation of input.allocations) {
|
|
56
|
+
// The row this transaction locked a moment ago, back out of the identity
|
|
57
|
+
// map. Re-reading it rather than carrying the managed instance across the
|
|
58
|
+
// seam is the whole content of D-77's first narrowing: a consumer that
|
|
59
|
+
// holds the row can move any column of it.
|
|
60
|
+
const row = await em.findOne(StockLevel, {
|
|
61
|
+
productId: input.productId,
|
|
62
|
+
variantId: input.variantId,
|
|
63
|
+
warehouseId: allocation.warehouseId,
|
|
64
|
+
});
|
|
65
|
+
if (row) {
|
|
66
|
+
row.reserved += allocation.quantity;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// No row for this line in this warehouse: open one so the reserved
|
|
70
|
+
// counter has somewhere to live. Still no on-hand — nothing has arrived,
|
|
71
|
+
// it is only promised.
|
|
72
|
+
const fresh = em.create(StockLevel, {
|
|
73
|
+
productId: input.productId,
|
|
74
|
+
...(input.variantId ? { variantId: input.variantId } : {}),
|
|
75
|
+
warehouseId: allocation.warehouseId,
|
|
76
|
+
onHand: 0,
|
|
77
|
+
reserved: allocation.quantity,
|
|
78
|
+
});
|
|
79
|
+
em.persist(fresh);
|
|
80
|
+
}
|
|
81
|
+
// Deliberately not flushed: the caller's transaction flushes this beside
|
|
82
|
+
// the order items whose foreign key holds the allocation rows, which is the
|
|
83
|
+
// statement order the reservation has always had.
|
|
84
|
+
}
|
|
85
|
+
async recordAllocationsForOrderItems(em, input) {
|
|
86
|
+
// command-coverage-ignore: the provenance rows of an audited order
|
|
87
|
+
// placement, inside that placement's own transaction — the same reasoning
|
|
88
|
+
// `reserveForPlacement` above carries.
|
|
89
|
+
for (const allocation of input.allocations) {
|
|
90
|
+
em.persist(em.create(StockAllocation, {
|
|
91
|
+
orderItemId: allocation.orderItemId,
|
|
92
|
+
warehouseId: allocation.warehouseId,
|
|
93
|
+
quantity: allocation.quantity,
|
|
94
|
+
isBackorder: allocation.isBackorder,
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
await em.flush();
|
|
98
|
+
}
|
|
99
|
+
async releaseForOrderItems(em, input) {
|
|
100
|
+
// command-coverage-ignore: the stock side of an audited cancellation. The
|
|
101
|
+
// `order.status_transition` Command owns the audit trail for the decision;
|
|
102
|
+
// this is its lifecycle side effect, and mirrors `credit_limits`'
|
|
103
|
+
// reserve/release pair.
|
|
104
|
+
if (input.items.length === 0)
|
|
105
|
+
return { released: 0 };
|
|
106
|
+
const allocations = await em.find(StockAllocation, {
|
|
107
|
+
orderItemId: { $in: input.items.map((i) => i.orderItemId) },
|
|
108
|
+
releasedAt: null,
|
|
109
|
+
});
|
|
110
|
+
if (allocations.length === 0)
|
|
111
|
+
return { released: 0 };
|
|
112
|
+
const lineByOrderItemId = new Map(input.items.map((i) => [i.orderItemId, i]));
|
|
113
|
+
const now = new Date();
|
|
114
|
+
for (const allocation of allocations) {
|
|
115
|
+
const line = lineByOrderItemId.get(allocation.orderItemId);
|
|
116
|
+
if (!line)
|
|
117
|
+
continue;
|
|
118
|
+
const row = await em.findOne(StockLevel, {
|
|
119
|
+
productId: line.productId,
|
|
120
|
+
variantId: line.variantId,
|
|
121
|
+
warehouseId: allocation.warehouseId,
|
|
122
|
+
});
|
|
123
|
+
// `Math.max(0, …)` rather than a bare subtraction: a counter that has
|
|
124
|
+
// already been corrected by hand must not be driven negative by a release
|
|
125
|
+
// it no longer accounts for.
|
|
126
|
+
if (row)
|
|
127
|
+
row.reserved = Math.max(0, row.reserved - allocation.quantity);
|
|
128
|
+
allocation.releasedAt = now;
|
|
129
|
+
}
|
|
130
|
+
await em.flush();
|
|
131
|
+
return { released: allocations.length };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=inventory-reservation-apply-port.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inventory-reservation-apply-port.js","sourceRoot":"","sources":["../../../src/backend/services/inventory-reservation-apply-port.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { CatalogProductReadPort, EmailMailerPort } from '@endora-commerce/contracts';
|
|
3
|
+
import type { SettingsReadPort } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import { StockLevel } from '../entities/stock-level.entity.js';
|
|
5
|
+
/** Feature 047 — structural port for sending via the admin-editable templates. */
|
|
6
|
+
export interface InventoryTemplateEmailPort {
|
|
7
|
+
trySend(input: {
|
|
8
|
+
code: string;
|
|
9
|
+
to: string;
|
|
10
|
+
messageId: string;
|
|
11
|
+
variables: Record<string, unknown>;
|
|
12
|
+
meta?: Record<string, unknown> | undefined;
|
|
13
|
+
}): Promise<boolean>;
|
|
14
|
+
}
|
|
15
|
+
export interface LowStockSummaryRow {
|
|
16
|
+
productId: string;
|
|
17
|
+
productSku: string;
|
|
18
|
+
productName: string;
|
|
19
|
+
cumulativeOnHand: number;
|
|
20
|
+
lowStockThreshold: number;
|
|
21
|
+
}
|
|
22
|
+
export interface AdjustedPayload {
|
|
23
|
+
productId: string;
|
|
24
|
+
warehouseId: string;
|
|
25
|
+
variantId: string | null;
|
|
26
|
+
before: number;
|
|
27
|
+
after: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* LowStockAlertService (US4 / FR-013).
|
|
31
|
+
*
|
|
32
|
+
* Listens for `inventory.adjusted.v1` and fires a single email per
|
|
33
|
+
* crossing — that is, when *cumulative* on-hand for a product drops
|
|
34
|
+
* from above the threshold to at-or-below it. The crossing detector
|
|
35
|
+
* is platform-wide (not per-channel) because the recipient is a
|
|
36
|
+
* single platform-side mailbox configured by
|
|
37
|
+
* `inventory.low_stock_alert_recipient_email`.
|
|
38
|
+
*
|
|
39
|
+
* `listLowStock()` powers the admin Home Stock Alerts panel + the
|
|
40
|
+
* dedicated `/inventory/low-stock` page.
|
|
41
|
+
*/
|
|
42
|
+
export declare class LowStockAlertService {
|
|
43
|
+
private readonly emFactory;
|
|
44
|
+
private readonly mailer;
|
|
45
|
+
/**
|
|
46
|
+
* `catalogProductReadPort`, owned by `catalog` (feature 075, Phase C). The
|
|
47
|
+
* crossing detector and the admin panel both need the product's SKU, name
|
|
48
|
+
* and threshold, and both used to load the entity out of a table that is
|
|
49
|
+
* still there when `catalog` is off — so a switched-off catalogue still
|
|
50
|
+
* produced low-stock alerts about it.
|
|
51
|
+
*/
|
|
52
|
+
private readonly catalogProducts;
|
|
53
|
+
private readonly settingsService?;
|
|
54
|
+
/** Channel id used to read inventory settings — typically the
|
|
55
|
+
* system default. */
|
|
56
|
+
private readonly settingsChannelId?;
|
|
57
|
+
private readonly templateEmail?;
|
|
58
|
+
constructor(emFactory: () => EntityManager, mailer: EmailMailerPort,
|
|
59
|
+
/**
|
|
60
|
+
* `catalogProductReadPort`, owned by `catalog` (feature 075, Phase C). The
|
|
61
|
+
* crossing detector and the admin panel both need the product's SKU, name
|
|
62
|
+
* and threshold, and both used to load the entity out of a table that is
|
|
63
|
+
* still there when `catalog` is off — so a switched-off catalogue still
|
|
64
|
+
* produced low-stock alerts about it.
|
|
65
|
+
*/
|
|
66
|
+
catalogProducts: CatalogProductReadPort, settingsService?: SettingsReadPort | undefined,
|
|
67
|
+
/** Channel id used to read inventory settings — typically the
|
|
68
|
+
* system default. */
|
|
69
|
+
settingsChannelId?: string | undefined, templateEmail?: InventoryTemplateEmailPort | undefined);
|
|
70
|
+
listLowStock(): Promise<LowStockSummaryRow[]>;
|
|
71
|
+
/**
|
|
72
|
+
* `inventory.adjusted.v1` — one e-mail per crossing of the product's low-stock
|
|
73
|
+
* threshold. Registered in this module's `backend.ts` through `ctx.subscribe`,
|
|
74
|
+
* so the alert stops with the module (issue #107); it used to be a bare
|
|
75
|
+
* `eventBus.on` here, which kept mailing while `inventory` was switched off.
|
|
76
|
+
*/
|
|
77
|
+
handleAdjusted(payload: AdjustedPayload): Promise<void>;
|
|
78
|
+
private fireEmail;
|
|
79
|
+
private resolveRecipient;
|
|
80
|
+
/**
|
|
81
|
+
* Stand-in helper used by tests + the StockLevel mutation paths to
|
|
82
|
+
* satisfy the unused-stock entity import. Removing it would risk
|
|
83
|
+
* the pruner stripping the import in incremental builds.
|
|
84
|
+
*/
|
|
85
|
+
static getStockLevelEntity(): typeof StockLevel;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=low-stock-alert-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"low-stock-alert-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/low-stock-alert-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EACV,sBAAsB,EAEtB,eAAe,EAChB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAEzE,OAAO,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AAG/D,kFAAkF;AAClF,MAAM,WAAW,0BAA0B;IACzC,OAAO,CAAC,KAAK,EAAE;QACb,IAAI,EAAE,MAAM,CAAC;QACb,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACnC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;KAC5C,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,oBAAoB;IAE7B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC;IACjC;0BACsB;IACtB,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;gBAdd,SAAS,EAAE,MAAM,aAAa,EAC9B,MAAM,EAAE,eAAe;IACxC;;;;;;OAMG;IACc,eAAe,EAAE,sBAAsB,EACvC,eAAe,CAAC,EAAE,gBAAgB,YAAA;IACnD;0BACsB;IACL,iBAAiB,CAAC,EAAE,MAAM,YAAA,EAC1B,aAAa,CAAC,EAAE,0BAA0B,YAAA;IAGvD,YAAY,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAkCnD;;;;;OAKG;IACG,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;YAuB/C,SAAS;YA0CT,gBAAgB;IAgB9B;;;;OAIG;IACH,MAAM,CAAC,mBAAmB,IAAI,OAAO,UAAU;CAGhD"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { StockLevel } from '../entities/stock-level.entity.js';
|
|
3
|
+
import { INVENTORY_SETTING_CODES } from '../../manifest.js';
|
|
4
|
+
/**
|
|
5
|
+
* LowStockAlertService (US4 / FR-013).
|
|
6
|
+
*
|
|
7
|
+
* Listens for `inventory.adjusted.v1` and fires a single email per
|
|
8
|
+
* crossing — that is, when *cumulative* on-hand for a product drops
|
|
9
|
+
* from above the threshold to at-or-below it. The crossing detector
|
|
10
|
+
* is platform-wide (not per-channel) because the recipient is a
|
|
11
|
+
* single platform-side mailbox configured by
|
|
12
|
+
* `inventory.low_stock_alert_recipient_email`.
|
|
13
|
+
*
|
|
14
|
+
* `listLowStock()` powers the admin Home Stock Alerts panel + the
|
|
15
|
+
* dedicated `/inventory/low-stock` page.
|
|
16
|
+
*/
|
|
17
|
+
export class LowStockAlertService {
|
|
18
|
+
emFactory;
|
|
19
|
+
mailer;
|
|
20
|
+
catalogProducts;
|
|
21
|
+
settingsService;
|
|
22
|
+
settingsChannelId;
|
|
23
|
+
templateEmail;
|
|
24
|
+
constructor(emFactory, mailer,
|
|
25
|
+
/**
|
|
26
|
+
* `catalogProductReadPort`, owned by `catalog` (feature 075, Phase C). The
|
|
27
|
+
* crossing detector and the admin panel both need the product's SKU, name
|
|
28
|
+
* and threshold, and both used to load the entity out of a table that is
|
|
29
|
+
* still there when `catalog` is off — so a switched-off catalogue still
|
|
30
|
+
* produced low-stock alerts about it.
|
|
31
|
+
*/
|
|
32
|
+
catalogProducts, settingsService,
|
|
33
|
+
/** Channel id used to read inventory settings — typically the
|
|
34
|
+
* system default. */
|
|
35
|
+
settingsChannelId, templateEmail) {
|
|
36
|
+
this.emFactory = emFactory;
|
|
37
|
+
this.mailer = mailer;
|
|
38
|
+
this.catalogProducts = catalogProducts;
|
|
39
|
+
this.settingsService = settingsService;
|
|
40
|
+
this.settingsChannelId = settingsChannelId;
|
|
41
|
+
this.templateEmail = templateEmail;
|
|
42
|
+
}
|
|
43
|
+
async listLowStock() {
|
|
44
|
+
const em = this.emFactory();
|
|
45
|
+
// `em.execute`, not `em.getKnex()`: a knex handle takes its own pooled
|
|
46
|
+
// connection, so the report would answer from outside a transaction the
|
|
47
|
+
// caller holds open (issue #207).
|
|
48
|
+
const rows = (await em.execute(`select product_id, sum(on_hand) as on_hand from stock_levels group by product_id`));
|
|
49
|
+
if (rows.length === 0)
|
|
50
|
+
return [];
|
|
51
|
+
const productIds = rows.map((r) => r.product_id);
|
|
52
|
+
const products = await this.catalogProducts.findByIds(productIds);
|
|
53
|
+
const productById = new Map(products.map((p) => [p.id, p]));
|
|
54
|
+
const result = [];
|
|
55
|
+
for (const row of rows) {
|
|
56
|
+
const p = productById.get(row.product_id);
|
|
57
|
+
if (!p)
|
|
58
|
+
continue;
|
|
59
|
+
if (!p.manageStock)
|
|
60
|
+
continue;
|
|
61
|
+
const threshold = p.lowStockThreshold;
|
|
62
|
+
if (threshold === null)
|
|
63
|
+
continue;
|
|
64
|
+
const cumulative = Number(row.on_hand ?? 0);
|
|
65
|
+
if (cumulative > threshold)
|
|
66
|
+
continue;
|
|
67
|
+
const productName = p.name['en-US'] ?? Object.values(p.name)[0] ?? p.sku;
|
|
68
|
+
result.push({
|
|
69
|
+
productId: p.id,
|
|
70
|
+
productSku: p.sku,
|
|
71
|
+
productName: String(productName),
|
|
72
|
+
cumulativeOnHand: cumulative,
|
|
73
|
+
lowStockThreshold: threshold,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* `inventory.adjusted.v1` — one e-mail per crossing of the product's low-stock
|
|
80
|
+
* threshold. Registered in this module's `backend.ts` through `ctx.subscribe`,
|
|
81
|
+
* so the alert stops with the module (issue #107); it used to be a bare
|
|
82
|
+
* `eventBus.on` here, which kept mailing while `inventory` was switched off.
|
|
83
|
+
*/
|
|
84
|
+
async handleAdjusted(payload) {
|
|
85
|
+
const em = this.emFactory();
|
|
86
|
+
const product = await this.catalogProducts.findById(payload.productId);
|
|
87
|
+
if (!product || !product.manageStock)
|
|
88
|
+
return;
|
|
89
|
+
const threshold = product.lowStockThreshold;
|
|
90
|
+
if (threshold === null)
|
|
91
|
+
return;
|
|
92
|
+
// Recompute cumulative across all warehouses (the event payload only
|
|
93
|
+
// carries one warehouse's delta). before/after for the cumulative
|
|
94
|
+
// crossing is `cumulativeBefore = (newCumulative - delta)`.
|
|
95
|
+
// `em.execute`, not `em.getKnex()` — same reason as the report above.
|
|
96
|
+
const sumRows = (await em.execute(`select sum(on_hand) as on_hand from stock_levels where product_id = ?`, [product.id]));
|
|
97
|
+
const cumulativeAfter = Number(sumRows[0]?.on_hand ?? 0);
|
|
98
|
+
const cumulativeBefore = cumulativeAfter - (payload.after - payload.before);
|
|
99
|
+
if (cumulativeBefore > threshold && cumulativeAfter <= threshold) {
|
|
100
|
+
await this.fireEmail(product, cumulativeAfter, threshold);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async fireEmail(product, cumulative, threshold) {
|
|
104
|
+
const recipient = await this.resolveRecipient();
|
|
105
|
+
if (!recipient)
|
|
106
|
+
return;
|
|
107
|
+
const productName = product.name['en-US'] ?? Object.values(product.name)[0] ?? product.sku;
|
|
108
|
+
const messageId = `inventory.low-stock:${product.id}:${Date.now()}`;
|
|
109
|
+
const meta = { productId: product.id, cumulativeOnHand: cumulative, threshold };
|
|
110
|
+
if (this.templateEmail) {
|
|
111
|
+
const sent = await this.templateEmail.trySend({
|
|
112
|
+
code: 'low_stock_alert',
|
|
113
|
+
to: recipient,
|
|
114
|
+
messageId,
|
|
115
|
+
variables: {
|
|
116
|
+
product: { name: productName, sku: product.sku },
|
|
117
|
+
cumulativeOnHand: cumulative,
|
|
118
|
+
threshold,
|
|
119
|
+
},
|
|
120
|
+
meta,
|
|
121
|
+
});
|
|
122
|
+
if (sent)
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const outcome = await this.mailer.send({
|
|
126
|
+
messageId,
|
|
127
|
+
to: recipient,
|
|
128
|
+
subject: `Low stock: ${productName}`,
|
|
129
|
+
text: `Cumulative on-hand for "${productName}" (SKU ${product.sku}) has crossed the low-stock threshold.\n\n Current cumulative on-hand: ${cumulative}\n Threshold: ${threshold}\n`,
|
|
130
|
+
kind: 'low_stock_alert',
|
|
131
|
+
meta,
|
|
132
|
+
});
|
|
133
|
+
if (outcome.status !== 'sent') {
|
|
134
|
+
// This fires from a crossing detector with no caller to answer, so the
|
|
135
|
+
// non-send is named here and durable in D-59's record.
|
|
136
|
+
console.warn('[inventory] the low-stock alert was not sent', {
|
|
137
|
+
productId: product.id,
|
|
138
|
+
reason: outcome.reason,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async resolveRecipient() {
|
|
143
|
+
if (this.settingsService && this.settingsChannelId) {
|
|
144
|
+
try {
|
|
145
|
+
const value = await this.settingsService.get(INVENTORY_SETTING_CODES.LOW_STOCK_ALERT_RECIPIENT_EMAIL, this.settingsChannelId, z.string());
|
|
146
|
+
if (value && value.trim().length > 0)
|
|
147
|
+
return value.trim();
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// fall through
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return process.env['INVENTORY_LOW_STOCK_RECIPIENT'] ?? null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Stand-in helper used by tests + the StockLevel mutation paths to
|
|
157
|
+
* satisfy the unused-stock entity import. Removing it would risk
|
|
158
|
+
* the pruner stripping the import in incremental builds.
|
|
159
|
+
*/
|
|
160
|
+
static getStockLevelEntity() {
|
|
161
|
+
return StockLevel;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=low-stock-alert-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"low-stock-alert-service.js","sourceRoot":"","sources":["../../../src/backend/services/low-stock-alert-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { InventoryProductThresholdWritePort, ProductThresholdCopyResult } from '@endora-commerce/contracts';
|
|
2
|
+
import type { CommandBus } from '@endora-commerce/platform/commands';
|
|
3
|
+
/**
|
|
4
|
+
* The per-warehouse threshold copy this module publishes (issue #185).
|
|
5
|
+
*
|
|
6
|
+
* `catalog`'s `duplicateProduct` used to write these rows itself:
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* insert into "product_warehouse_low_stock_thresholds"
|
|
10
|
+
* ("product_id", "warehouse_id", "threshold", "created_at", "updated_at")
|
|
11
|
+
* select ?, "warehouse_id", "threshold", now(), now()
|
|
12
|
+
* from "product_warehouse_low_stock_thresholds" where "product_id" = ?
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* — on `catalog`'s `EntityManager`, after its `CommandBus.run` had already
|
|
16
|
+
* returned, with this module's activation state never consulted. It is the same
|
|
17
|
+
* shape D-74 took off `import_export` for `stock_levels`, and it comes back for
|
|
18
|
+
* the same three reasons: the write is one Command and one audit row, it is
|
|
19
|
+
* gated by the module that owns the rows, and the caller stops needing to know
|
|
20
|
+
* the table's columns.
|
|
21
|
+
*
|
|
22
|
+
* **Its own transaction, deliberately.** `duplicateProduct` commits the product
|
|
23
|
+
* row first and then copies each bridge — the code says so at the seam and has
|
|
24
|
+
* since feature 054 ("a partially-copied dup is no worse than today"). Nothing
|
|
25
|
+
* here can be co-transactional with a transaction that has already committed,
|
|
26
|
+
* and nothing should want to be: a duplicate without per-warehouse thresholds
|
|
27
|
+
* falls back to the product-level and warehouse-level chain, which is precisely
|
|
28
|
+
* what a duplicate looks like on a deployment that never installed this module.
|
|
29
|
+
*/
|
|
30
|
+
export declare class InventoryProductThresholdWriteService implements InventoryProductThresholdWritePort {
|
|
31
|
+
private readonly commandBus;
|
|
32
|
+
constructor(commandBus: CommandBus);
|
|
33
|
+
copyProductWarehouseThresholds(input: {
|
|
34
|
+
sourceProductId: string;
|
|
35
|
+
targetProductId: string;
|
|
36
|
+
}): Promise<ProductThresholdCopyResult>;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=product-threshold-write.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"product-threshold-write.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/product-threshold-write.service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kCAAkC,EAClC,0BAA0B,EAC3B,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAGrE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,qCACX,YAAW,kCAAkC;IAEjC,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,EAAE,UAAU;IAE7C,8BAA8B,CAAC,KAAK,EAAE;QAC1C,eAAe,EAAE,MAAM,CAAC;QACxB,eAAe,EAAE,MAAM,CAAC;KACzB,GAAG,OAAO,CAAC,0BAA0B,CAAC;CAkDxC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ProductWarehouseLowStockThreshold } from '../entities/product-warehouse-low-stock-threshold.entity.js';
|
|
2
|
+
/**
|
|
3
|
+
* The per-warehouse threshold copy this module publishes (issue #185).
|
|
4
|
+
*
|
|
5
|
+
* `catalog`'s `duplicateProduct` used to write these rows itself:
|
|
6
|
+
*
|
|
7
|
+
* ```
|
|
8
|
+
* insert into "product_warehouse_low_stock_thresholds"
|
|
9
|
+
* ("product_id", "warehouse_id", "threshold", "created_at", "updated_at")
|
|
10
|
+
* select ?, "warehouse_id", "threshold", now(), now()
|
|
11
|
+
* from "product_warehouse_low_stock_thresholds" where "product_id" = ?
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* — on `catalog`'s `EntityManager`, after its `CommandBus.run` had already
|
|
15
|
+
* returned, with this module's activation state never consulted. It is the same
|
|
16
|
+
* shape D-74 took off `import_export` for `stock_levels`, and it comes back for
|
|
17
|
+
* the same three reasons: the write is one Command and one audit row, it is
|
|
18
|
+
* gated by the module that owns the rows, and the caller stops needing to know
|
|
19
|
+
* the table's columns.
|
|
20
|
+
*
|
|
21
|
+
* **Its own transaction, deliberately.** `duplicateProduct` commits the product
|
|
22
|
+
* row first and then copies each bridge — the code says so at the seam and has
|
|
23
|
+
* since feature 054 ("a partially-copied dup is no worse than today"). Nothing
|
|
24
|
+
* here can be co-transactional with a transaction that has already committed,
|
|
25
|
+
* and nothing should want to be: a duplicate without per-warehouse thresholds
|
|
26
|
+
* falls back to the product-level and warehouse-level chain, which is precisely
|
|
27
|
+
* what a duplicate looks like on a deployment that never installed this module.
|
|
28
|
+
*/
|
|
29
|
+
export class InventoryProductThresholdWriteService {
|
|
30
|
+
commandBus;
|
|
31
|
+
constructor(commandBus) {
|
|
32
|
+
this.commandBus = commandBus;
|
|
33
|
+
}
|
|
34
|
+
async copyProductWarehouseThresholds(input) {
|
|
35
|
+
return this.commandBus.run({
|
|
36
|
+
action: 'inventory.product_thresholds.copy',
|
|
37
|
+
objectType: 'product_warehouse_low_stock_threshold',
|
|
38
|
+
objectId: input.targetProductId,
|
|
39
|
+
run: async ({ em }) => {
|
|
40
|
+
const source = await em.find(ProductWarehouseLowStockThreshold, {
|
|
41
|
+
productId: input.sourceProductId,
|
|
42
|
+
});
|
|
43
|
+
if (source.length === 0) {
|
|
44
|
+
// Nothing changed state, so nothing is recorded — the rule
|
|
45
|
+
// `SalesChannelMembershipService` applies to an idempotent add.
|
|
46
|
+
return { result: { copied: 0 }, skipAudit: true };
|
|
47
|
+
}
|
|
48
|
+
const existing = await em.find(ProductWarehouseLowStockThreshold, {
|
|
49
|
+
productId: input.targetProductId,
|
|
50
|
+
warehouseId: { $in: source.map((row) => row.warehouseId) },
|
|
51
|
+
});
|
|
52
|
+
const byWarehouse = new Map(existing.map((row) => [row.warehouseId, row]));
|
|
53
|
+
for (const row of source) {
|
|
54
|
+
const target = byWarehouse.get(row.warehouseId);
|
|
55
|
+
if (target) {
|
|
56
|
+
target.threshold = row.threshold;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
em.persist(em.create(ProductWarehouseLowStockThreshold, {
|
|
60
|
+
productId: input.targetProductId,
|
|
61
|
+
warehouseId: row.warehouseId,
|
|
62
|
+
threshold: row.threshold,
|
|
63
|
+
createdAt: new Date(),
|
|
64
|
+
updatedAt: new Date(),
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
await em.flush();
|
|
68
|
+
return {
|
|
69
|
+
result: { copied: source.length },
|
|
70
|
+
after: {
|
|
71
|
+
sourceProductId: input.sourceProductId,
|
|
72
|
+
targetProductId: input.targetProductId,
|
|
73
|
+
copied: source.length,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=product-threshold-write.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"product-threshold-write.service.js","sourceRoot":"","sources":["../../../src/backend/services/product-threshold-write.service.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,iCAAiC,EAAE,MAAM,6DAA6D,CAAC;AAEhH;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,OAAO,qCAAqC;IAGnB;IAA7B,YAA6B,UAAsB;QAAtB,eAAU,GAAV,UAAU,CAAY;IAAG,CAAC;IAEvD,KAAK,CAAC,8BAA8B,CAAC,KAGpC;QACC,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAA6B;YACrD,MAAM,EAAE,mCAAmC;YAC3C,UAAU,EAAE,uCAAuC;YACnD,QAAQ,EAAE,KAAK,CAAC,eAAe;YAC/B,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE;gBACpB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,iCAAiC,EAAE;oBAC9D,SAAS,EAAE,KAAK,CAAC,eAAe;iBACjC,CAAC,CAAC;gBACH,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBACxB,2DAA2D;oBAC3D,gEAAgE;oBAChE,OAAO,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;gBACpD,CAAC;gBAED,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,iCAAiC,EAAE;oBAChE,SAAS,EAAE,KAAK,CAAC,eAAe;oBAChC,WAAW,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE;iBAC3D,CAAC,CAAC;gBACH,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;gBAE3E,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;oBACzB,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;oBAChD,IAAI,MAAM,EAAE,CAAC;wBACX,MAAM,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;wBACjC,SAAS;oBACX,CAAC;oBACD,EAAE,CAAC,OAAO,CACR,EAAE,CAAC,MAAM,CAAC,iCAAiC,EAAE;wBAC3C,SAAS,EAAE,KAAK,CAAC,eAAe;wBAChC,WAAW,EAAE,GAAG,CAAC,WAAW;wBAC5B,SAAS,EAAE,GAAG,CAAC,SAAS;wBACxB,SAAS,EAAE,IAAI,IAAI,EAAE;wBACrB,SAAS,EAAE,IAAI,IAAI,EAAE;qBACtB,CAAC,CACH,CAAC;gBACJ,CAAC;gBACD,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;gBAEjB,OAAO;oBACL,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;oBACjC,KAAK,EAAE;wBACL,eAAe,EAAE,KAAK,CAAC,eAAe;wBACtC,eAAe,EAAE,KAAK,CAAC,eAAe;wBACtC,MAAM,EAAE,MAAM,CAAC,MAAM;qBACtB;iBACF,CAAC;YACJ,CAAC;SACF,CAAC,CAAC;IACL,CAAC;CACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { BulkImportReport, CatalogProductReadPort, InventoryStockImportPort, StockLevelImportRow } from '@endora-commerce/contracts';
|
|
2
|
+
import type { CommandBus } from '@endora-commerce/platform/commands';
|
|
3
|
+
/**
|
|
4
|
+
* Bulk import of stock levels (feature 075, D-74).
|
|
5
|
+
*
|
|
6
|
+
* The counterpart of `catalog`'s bulk import, and it exists for the same three
|
|
7
|
+
* reasons: the write is one Command and one audit row, validation runs before
|
|
8
|
+
* anything is applied, and the whole run is all-or-nothing.
|
|
9
|
+
*
|
|
10
|
+
* Two things came back to this module with it. The rows are `stock_levels`, and
|
|
11
|
+
* `import_export` was writing them while declaring a dependency on `catalog` and
|
|
12
|
+
* `auth` only — an edge no check in the tree could see, because this module
|
|
13
|
+
* ships the migration and that one ships none. And the seeded warehouse was
|
|
14
|
+
* addressed by a **copy** of the deterministic UUID this module exports, pasted
|
|
15
|
+
* into a function body; a duplicated constant is a coupling `check:module-boundary`
|
|
16
|
+
* is structurally unable to report, since it reads import specifiers.
|
|
17
|
+
*
|
|
18
|
+
* The row carries no warehouse, which is the pre-existing contract of the CSV
|
|
19
|
+
* path rather than a simplification taken here: foundation 001's sheet has no
|
|
20
|
+
* warehouse column, so every row lands in the default warehouse. The
|
|
21
|
+
* warehouse-scoped importer is `CsvStockImporter`, reached from this module's
|
|
22
|
+
* own admin route.
|
|
23
|
+
*/
|
|
24
|
+
export declare class InventoryStockImportService implements InventoryStockImportPort {
|
|
25
|
+
private readonly commandBus;
|
|
26
|
+
/**
|
|
27
|
+
* SKU → product id. A spreadsheet addresses a product the way an operator
|
|
28
|
+
* does, so somebody has to ask `catalog`; doing it here is what lets the
|
|
29
|
+
* rejected-row message ("unknown product_sku") be raised by the module that
|
|
30
|
+
* knows the answer.
|
|
31
|
+
*/
|
|
32
|
+
private readonly products;
|
|
33
|
+
constructor(commandBus: CommandBus,
|
|
34
|
+
/**
|
|
35
|
+
* SKU → product id. A spreadsheet addresses a product the way an operator
|
|
36
|
+
* does, so somebody has to ask `catalog`; doing it here is what lets the
|
|
37
|
+
* rejected-row message ("unknown product_sku") be raised by the module that
|
|
38
|
+
* knows the answer.
|
|
39
|
+
*/
|
|
40
|
+
products: CatalogProductReadPort);
|
|
41
|
+
importStockLevels(rows: readonly StockLevelImportRow[]): Promise<BulkImportReport>;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=stock-import.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stock-import.service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/stock-import.service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAEhB,sBAAsB,EACtB,wBAAwB,EACxB,mBAAmB,EACpB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAKrE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,2BAA4B,YAAW,wBAAwB;IAExE,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAPR,UAAU,EAAE,UAAU;IACvC;;;;;OAKG;IACc,QAAQ,EAAE,sBAAsB;IAG7C,iBAAiB,CAAC,IAAI,EAAE,SAAS,mBAAmB,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAwFzF"}
|