@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,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"}