@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,263 @@
1
+ import { z } from 'zod';
2
+ import { availabilityNotificationRequestSchema, inventoryDisplayModeSchema, isProductVisibleTo, } from '@endora-commerce/contracts';
3
+ import { getResolvedChannel } from '@endora-commerce/platform/kernel';
4
+ import { productAudienceOf } from '@endora-commerce/platform/http';
5
+ import { StockLevel } from './entities/stock-level.entity.js';
6
+ import { resolveDisplayBand } from './services/display-band-resolver.js';
7
+ import { resolveThresholds } from './services/threshold-resolver.js';
8
+ import { InventoryThreshold } from './entities/inventory-threshold.entity.js';
9
+ import { INVENTORY_SETTING_CODES } from '../manifest.js';
10
+ export async function registerInventoryRoutes(app, deps) {
11
+ const { emFactory, catalogProducts, catalogCategories, customerAccounts, availabilityService, requireCustomer, resolveCustomerContext, channelResolver, warehouseChannelService, settingsService, } = deps;
12
+ // ---------------------------------------------------------------------------
13
+ // Notify-when-available (foundation 001 backward-compat)
14
+ // ---------------------------------------------------------------------------
15
+ // Foundation 001 path — customer-only, pre-fills email from the
16
+ // customer account. Kept for backwards compatibility with any
17
+ // existing storefront/admin callers.
18
+ app.post('/api/v1/catalog/products/:id/notify-when-available', {
19
+ preHandler: requireCustomer,
20
+ schema: { body: availabilityNotificationRequestSchema.optional() },
21
+ }, async (request, reply) => {
22
+ const ctx = resolveCustomerContext(request);
23
+ const body = availabilityNotificationRequestSchema.parse(request.body ?? {});
24
+ const customerAccount = await customerAccounts.findById(ctx.customerAccountId);
25
+ if (!customerAccount) {
26
+ reply.status(403);
27
+ return {
28
+ error: { code: 'FORBIDDEN', message: 'Unknown customer account', requestId: request.id },
29
+ };
30
+ }
31
+ const subscription = await availabilityService.subscribe({
32
+ customerAccountId: ctx.customerAccountId,
33
+ email: customerAccount.email,
34
+ productId: request.params.id,
35
+ variantId: body.variantId ?? null,
36
+ // Issue #227 — off the actor the auth plugin resolved rather than off
37
+ // the customer context above: `productAudienceOf` is the one helper the
38
+ // six modules that serve a product to a buyer share, so an account with
39
+ // no organisation cannot be answered differently here than next door.
40
+ audience: productAudienceOf(request),
41
+ });
42
+ reply.status(202);
43
+ return {
44
+ data: {
45
+ subscriptionId: subscription.id,
46
+ requestedAt: subscription.requestedAt.toISOString(),
47
+ },
48
+ };
49
+ });
50
+ // Feature 010 / US6 — public path that accepts both anonymous and
51
+ // authenticated callers. Email is supplied by the body; the storefront
52
+ // pre-fills it from the customer account on the client side when a
53
+ // session exists. Threaded auth is intentionally optional so the
54
+ // anonymous Notify-when-available flow does not require a sign-in.
55
+ app.post('/api/v1/storefront/inventory/notify-when-available', {
56
+ schema: {
57
+ body: z.object({
58
+ productId: z.string().uuid(),
59
+ email: z.string().email(),
60
+ variantId: z.string().uuid().nullable().optional(),
61
+ }),
62
+ },
63
+ }, async (request, reply) => {
64
+ const body = request.body;
65
+ // Best-effort customer-account binding: when the request happens
66
+ // to carry a valid customer context, link the row so the admin
67
+ // queue browser shows the customer id alongside the email.
68
+ let customerAccountId = null;
69
+ try {
70
+ const ctx = resolveCustomerContext(request);
71
+ customerAccountId = ctx.customerAccountId;
72
+ }
73
+ catch {
74
+ customerAccountId = null;
75
+ }
76
+ const subscription = await availabilityService.subscribe({
77
+ productId: body.productId,
78
+ email: body.email,
79
+ variantId: body.variantId ?? null,
80
+ customerAccountId,
81
+ // Issue #227 — and deliberately **not** derived from the `try` above.
82
+ // That block answers "which account owns the row", and it swallows its
83
+ // failure; an audience derived from it would read as anonymous for a
84
+ // caller the platform did in fact identify, and worse, would silently
85
+ // become whatever a future edit to that `catch` made it.
86
+ // `productAudienceOf` reads the actor and nothing else.
87
+ audience: productAudienceOf(request),
88
+ });
89
+ reply.status(202);
90
+ return {
91
+ data: {
92
+ subscriptionId: subscription.id,
93
+ requestedAt: subscription.requestedAt.toISOString(),
94
+ },
95
+ };
96
+ });
97
+ // ---------------------------------------------------------------------------
98
+ // Storefront-public display-mode (US5)
99
+ // ---------------------------------------------------------------------------
100
+ app.get('/api/v1/storefront/inventory/display-mode', async (request) => {
101
+ const channelId = await resolveChannelId(request, channelResolver);
102
+ const mode = await readDisplayMode(channelId, settingsService);
103
+ return { data: { displayMode: mode } };
104
+ });
105
+ // ---------------------------------------------------------------------------
106
+ // Storefront-public per-product stock (US3 + US5)
107
+ // ---------------------------------------------------------------------------
108
+ app.get('/api/v1/storefront/inventory/stock/:id', async (request, reply) => {
109
+ const productId = request.params.id;
110
+ const em = emFactory();
111
+ const product = await catalogProducts.findById(productId);
112
+ // Issue #227 — this route is anonymous and answers "how many of this
113
+ // product are there". Both halves of that are a disclosure about a
114
+ // product an operator restricted: that it exists, and how it is selling.
115
+ if (!product || !isProductVisibleTo(product, productAudienceOf(request))) {
116
+ reply.status(404);
117
+ return {
118
+ error: {
119
+ code: 'PRODUCT_NOT_FOUND',
120
+ message: 'Product not found',
121
+ requestId: request.id,
122
+ },
123
+ };
124
+ }
125
+ const channelId = await resolveChannelId(request, channelResolver);
126
+ let candidateWarehouseIds = warehouseChannelService
127
+ ? await warehouseChannelService.resolveCandidateWarehouseIds(channelId)
128
+ : [];
129
+ // Feature 026 US4 — intersect with the caller's Organization allow-
130
+ // list when one is configured. Empty per-org assignment (or no
131
+ // resolver wired) falls through to the platform defaults above.
132
+ if (deps.resolveOrganizationWarehouseAllowList) {
133
+ const allowList = await deps.resolveOrganizationWarehouseAllowList(request);
134
+ if (allowList && allowList.length > 0) {
135
+ const allowSet = new Set(allowList);
136
+ if (candidateWarehouseIds.length === 0) {
137
+ candidateWarehouseIds = [...allowSet];
138
+ }
139
+ else {
140
+ candidateWarehouseIds = candidateWarehouseIds.filter((id) => allowSet.has(id));
141
+ }
142
+ }
143
+ }
144
+ let cumulativeOnHand = 0;
145
+ if (candidateWarehouseIds.length > 0) {
146
+ // `em.execute`, not `em.getKnex()`: a knex handle takes its own pooled
147
+ // connection, so this sum would answer from outside any transaction a
148
+ // caller holds open while the `em.find` fallback below answers from
149
+ // inside it — one route, two views of `stock_levels` (issue #207).
150
+ const placeholders = candidateWarehouseIds.map(() => '?').join(', ');
151
+ const sumRows = (await em.execute(`select sum(on_hand) as on_hand
152
+ from stock_levels
153
+ where product_id = ? and warehouse_id in (${placeholders})`, [productId, ...candidateWarehouseIds]));
154
+ cumulativeOnHand = Number(sumRows[0]?.on_hand ?? 0);
155
+ }
156
+ else {
157
+ // Fallback when no channel binding is wired yet.
158
+ const rows = await em.find(StockLevel, { productId });
159
+ cumulativeOnHand = rows.reduce((s, r) => s + r.onHand, 0);
160
+ }
161
+ const displayMode = await readDisplayMode(channelId, settingsService);
162
+ const globalThresholds = await loadGlobalThresholds(em);
163
+ const productThresholds = await loadProductThresholdsRow(em, productId);
164
+ // Asked of `catalog`, not joined out of its `product_categories` table:
165
+ // the bridge row is the owner's and the port has answered for it since
166
+ // D-87 (feature 075, the `inventory` shard). The `findByIds` on the next
167
+ // line was already going to the owner for the threshold columns — this is
168
+ // the first half of the same question arriving at the same place.
169
+ const assignments = await catalogCategories.listAssignmentsForProducts([productId]);
170
+ const categoryIds = assignments.map((a) => a.categoryId);
171
+ const categories = await catalogCategories.findByIds(categoryIds);
172
+ const categoryThresholds = categories.map((c) => ({
173
+ high: c.inventoryThresholdHigh,
174
+ medium: c.inventoryThresholdMedium,
175
+ low: c.inventoryThresholdLow,
176
+ }));
177
+ const thresholds = resolveThresholds({
178
+ productThresholds,
179
+ categoryThresholds,
180
+ globalThresholds,
181
+ });
182
+ const displayBand = resolveDisplayBand({
183
+ manageStock: product.manageStock,
184
+ cumulativeOnHand,
185
+ thresholds,
186
+ });
187
+ // Effective backorder = per-product flag AND the global negative-stock
188
+ // gate; this is what order placement enforces, so the storefront must
189
+ // not advertise backorder when the global gate is off.
190
+ const allowNegativeStock = await readAllowNegativeStock(channelId, settingsService);
191
+ const effectiveBackorderEnabled = product.backorderEnabled && allowNegativeStock;
192
+ const isOutOfStock = product.manageStock && cumulativeOnHand <= 0;
193
+ const showNotifyButton = product.manageStock &&
194
+ !effectiveBackorderEnabled &&
195
+ cumulativeOnHand <= 0;
196
+ const payload = {
197
+ productId,
198
+ manageStock: product.manageStock,
199
+ backorderEnabled: effectiveBackorderEnabled,
200
+ displayMode,
201
+ displayBand,
202
+ exactOnHand: displayMode === 'exact' ? cumulativeOnHand : null,
203
+ isOutOfStock,
204
+ showNotifyButton,
205
+ };
206
+ return { data: payload };
207
+ });
208
+ }
209
+ async function resolveChannelId(request, _resolver) {
210
+ // The canonical sales-channel resolver middleware already performed
211
+ // header/host/system-default resolution and active-channel validation on
212
+ // this request path, so simply read the resolved channel's id.
213
+ return getResolvedChannel(request).id;
214
+ }
215
+ async function readDisplayMode(channelId, settingsService) {
216
+ if (settingsService && channelId) {
217
+ try {
218
+ const value = await settingsService.get(INVENTORY_SETTING_CODES.DISPLAY_MODE, channelId, inventoryDisplayModeSchema);
219
+ return value;
220
+ }
221
+ catch {
222
+ // fall through
223
+ }
224
+ }
225
+ return 'band';
226
+ }
227
+ /**
228
+ * Global backorder gate (`inventory.allow_negative_stock`) for a channel.
229
+ * Mirrors `readDisplayMode`: per-channel value → global value → manifest
230
+ * default (false). Failures degrade to false so the storefront never
231
+ * promises a backorder the order placement would reject.
232
+ */
233
+ async function readAllowNegativeStock(channelId, settingsService) {
234
+ if (settingsService && channelId) {
235
+ try {
236
+ const { z } = await import('zod');
237
+ return await settingsService.get(INVENTORY_SETTING_CODES.ALLOW_NEGATIVE_STOCK, channelId, z.boolean());
238
+ }
239
+ catch {
240
+ // fall through
241
+ }
242
+ }
243
+ return false;
244
+ }
245
+ async function loadGlobalThresholds(em) {
246
+ const row = await em.findOne(InventoryThreshold, { scopeKind: 'global', scopeId: null });
247
+ return {
248
+ high: row?.thresholdHigh ?? 100,
249
+ medium: row?.thresholdMedium ?? 20,
250
+ low: row?.thresholdLow ?? 1,
251
+ };
252
+ }
253
+ async function loadProductThresholdsRow(em, productId) {
254
+ const row = await em.findOne(InventoryThreshold, { scopeKind: 'product', scopeId: productId });
255
+ if (!row)
256
+ return null;
257
+ return {
258
+ high: row.thresholdHigh ?? null,
259
+ medium: row.thresholdMedium ?? null,
260
+ low: row.thresholdLow ?? null,
261
+ };
262
+ }
263
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { AuditReferenceRegistryPort } from '@endora-commerce/contracts';
2
+ import type { EntityManager } from '@mikro-orm/postgresql';
3
+ /**
4
+ * What a `warehouse` audit row is called, and where the admin app shows it
5
+ * (feature 075, D-87 drain).
6
+ *
7
+ * This is the contribution the inversion was chosen for. `inventory` is the one
8
+ * contributor to `auditReferenceRegistry` whose manifest declares no
9
+ * `activation.nonDeactivatable`, so it is the one an operator can switch off —
10
+ * and `audit_logs` is non-deactivatable, so had it declared this module as a
11
+ * dependency to read a warehouse name, the lifecycle would have refused every
12
+ * attempt to switch this one off. A push costs the operator nothing.
13
+ */
14
+ export declare function registerInventoryAuditReferences(registry: AuditReferenceRegistryPort, emFactory: () => EntityManager): void;
15
+ //# sourceMappingURL=audit-references.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-references.d.ts","sourceRoot":"","sources":["../../../src/backend/services/audit-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D;;;;;;;;;;GAUG;AACH,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,0BAA0B,EACpC,SAAS,EAAE,MAAM,aAAa,GAC7B,IAAI,CAiBN"}
@@ -0,0 +1,27 @@
1
+ import { Warehouse } from '../entities/warehouse.entity.js';
2
+ /**
3
+ * What a `warehouse` audit row is called, and where the admin app shows it
4
+ * (feature 075, D-87 drain).
5
+ *
6
+ * This is the contribution the inversion was chosen for. `inventory` is the one
7
+ * contributor to `auditReferenceRegistry` whose manifest declares no
8
+ * `activation.nonDeactivatable`, so it is the one an operator can switch off —
9
+ * and `audit_logs` is non-deactivatable, so had it declared this module as a
10
+ * dependency to read a warehouse name, the lifecycle would have refused every
11
+ * attempt to switch this one off. A push costs the operator nothing.
12
+ */
13
+ export function registerInventoryAuditReferences(registry, emFactory) {
14
+ registry.register({
15
+ ownerModuleId: 'inventory',
16
+ referenceType: 'warehouse',
17
+ resolve: async (ids) => {
18
+ const rows = await emFactory().find(Warehouse, { id: { $in: [...ids] } }, { fields: ['id', 'name', 'code'] });
19
+ return rows.map((row) => ({
20
+ id: row.id,
21
+ label: row.name.length > 0 ? row.name : row.code,
22
+ url: `/warehouses/${row.id}`,
23
+ }));
24
+ },
25
+ });
26
+ }
27
+ //# sourceMappingURL=audit-references.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-references.js","sourceRoot":"","sources":["../../../src/backend/services/audit-references.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAE5D;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gCAAgC,CAC9C,QAAoC,EACpC,SAA8B;IAE9B,QAAQ,CAAC,QAAQ,CAAC;QAChB,aAAa,EAAE,WAAW;QAC1B,aAAa,EAAE,WAAW;QAC1B,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YACrB,MAAM,IAAI,GAAG,MAAM,SAAS,EAAE,CAAC,IAAI,CACjC,SAAS,EACT,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,EAAE,EACzB,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CACnC,CAAC;YACF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBACxB,EAAE,EAAE,GAAG,CAAC,EAAE;gBACV,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI;gBAChD,GAAG,EAAE,eAAe,GAAG,CAAC,EAAE,EAAE;aAC7B,CAAC,CAAC,CAAC;QACN,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,155 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type CatalogProductReadPort, type CustomerAccountReadPort, type EmailMailerPort, type ProductAudience } from '@endora-commerce/contracts';
3
+ import { type SalesChannelMembershipPort } from '@endora-commerce/platform/kernel';
4
+ import { AvailabilityNotification } from '../entities/availability-notification.entity.js';
5
+ import { StockLevel } from '../entities/stock-level.entity.js';
6
+ import type { InventoryTemplateEmailPort } from './low-stock-alert-service.js';
7
+ export interface SubscribeInput {
8
+ productId: string;
9
+ variantId?: string | null;
10
+ email: string;
11
+ customerAccountId?: string | null;
12
+ /**
13
+ * Who is asking (issue #227). Defaulted to the anonymous audience so a
14
+ * composition that has not been taught to resolve its caller refuses a
15
+ * restricted product rather than queueing a subscription for it — the
16
+ * fail-closed end, and the same default `ComparisonService.addProduct` takes
17
+ * for the same reason. Both storefront routes pass the real one.
18
+ */
19
+ audience?: ProductAudience;
20
+ }
21
+ export interface AdminListFilter {
22
+ productId?: string;
23
+ status?: 'queued' | 'notified' | 'cancelled';
24
+ page?: number;
25
+ pageSize?: number;
26
+ }
27
+ export interface AdminListRow {
28
+ id: string;
29
+ productId: string;
30
+ productName: string;
31
+ productSku: string;
32
+ customerAccountId: string | null;
33
+ email: string;
34
+ status: 'queued' | 'notified' | 'cancelled';
35
+ queuedAt: string;
36
+ notifiedAt: string | null;
37
+ }
38
+ /**
39
+ * AvailabilityNotificationService — feature 010 / US6 surface.
40
+ *
41
+ * The original foundation 001 service had a single `subscribe(customerId,
42
+ * productId, variantId?)` entry point. Feature 010 extends it for the
43
+ * three customer paths spelled out in spec.md US6:
44
+ *
45
+ * - storefront customer signed-in → email pre-filled from the account
46
+ * - storefront anonymous → caller supplies the email
47
+ * - admin browses + cancels any queued row
48
+ *
49
+ * The restock fan-out lives on `AvailabilityWorker.dispatchForStockIncrease`
50
+ * (already exists from foundation 001); the worker is rewired in US6 to
51
+ * subscribe to `inventory.adjusted.v1` events with `before === 0 &&
52
+ * after > 0` so a stock-bump fans out one email per queued row.
53
+ */
54
+ export declare class AvailabilityNotificationService {
55
+ #private;
56
+ private readonly emFactory;
57
+ /** `catalogProductReadPort`, owned by `catalog` (feature 075, Phase C). */
58
+ private readonly catalogProducts;
59
+ /**
60
+ * `customerAccountReadPort`, owned by `customer_accounts` (feature 075).
61
+ *
62
+ * Feature 087 / D-187 gives it a second reader: the organisation an owned
63
+ * subscription carries. It was already **required** here before that
64
+ * ruling and stays so — this module has exactly one construction site
65
+ * (`plugin.ts`) and it holds the gated port, so a composition that cannot
66
+ * answer "which organisation owns this account" cannot build the service
67
+ * at all. Nothing had to be argued for; it is recorded because `pwa` had
68
+ * to widen its constructor for the same read and the two modules now sit
69
+ * in the same state.
70
+ *
71
+ * Resolved through `lazyPort`, so a switched-off `customer_accounts`
72
+ * refuses at the call with the 503 `MODULE_DISABLED` envelope rather than
73
+ * at composition time.
74
+ */
75
+ private readonly customerAccounts;
76
+ /**
77
+ * `salesChannelMembershipPort` — the sanctioned bridge accessor
78
+ * (Constitution XII), through which {@link subscribe} asks whether the
79
+ * request's channel publishes the product at all.
80
+ *
81
+ * Required rather than optional, and placed before the two optional
82
+ * transports so a construction site cannot leave it off by accident: an
83
+ * optional channel gate is a composition that silently answers without one,
84
+ * which is precisely the state this parameter was added to end. It is a
85
+ * kernel registration rather than a module's, so it adds no edge to this
86
+ * module's manifest.
87
+ */
88
+ private readonly channelMembership;
89
+ private readonly mailer?;
90
+ private readonly templateEmail?;
91
+ constructor(emFactory: () => EntityManager,
92
+ /** `catalogProductReadPort`, owned by `catalog` (feature 075, Phase C). */
93
+ catalogProducts: CatalogProductReadPort,
94
+ /**
95
+ * `customerAccountReadPort`, owned by `customer_accounts` (feature 075).
96
+ *
97
+ * Feature 087 / D-187 gives it a second reader: the organisation an owned
98
+ * subscription carries. It was already **required** here before that
99
+ * ruling and stays so — this module has exactly one construction site
100
+ * (`plugin.ts`) and it holds the gated port, so a composition that cannot
101
+ * answer "which organisation owns this account" cannot build the service
102
+ * at all. Nothing had to be argued for; it is recorded because `pwa` had
103
+ * to widen its constructor for the same read and the two modules now sit
104
+ * in the same state.
105
+ *
106
+ * Resolved through `lazyPort`, so a switched-off `customer_accounts`
107
+ * refuses at the call with the 503 `MODULE_DISABLED` envelope rather than
108
+ * at composition time.
109
+ */
110
+ customerAccounts: CustomerAccountReadPort,
111
+ /**
112
+ * `salesChannelMembershipPort` — the sanctioned bridge accessor
113
+ * (Constitution XII), through which {@link subscribe} asks whether the
114
+ * request's channel publishes the product at all.
115
+ *
116
+ * Required rather than optional, and placed before the two optional
117
+ * transports so a construction site cannot leave it off by accident: an
118
+ * optional channel gate is a composition that silently answers without one,
119
+ * which is precisely the state this parameter was added to end. It is a
120
+ * kernel registration rather than a module's, so it adds no edge to this
121
+ * module's manifest.
122
+ */
123
+ channelMembership: SalesChannelMembershipPort, mailer?: EmailMailerPort | undefined, templateEmail?: InventoryTemplateEmailPort | undefined);
124
+ /**
125
+ * Subscribe a customer / anonymous email to a product's restock signal.
126
+ *
127
+ * Refuses with 404 PRODUCT_NOT_FOUND for every product this caller may not
128
+ * acquire — see {@link #mayAcquire}; with 422 PRODUCT_IN_STOCK when
129
+ * cumulative on-hand > 0; with 409 PRODUCT_UNMANAGED_STOCK when the product
130
+ * opted out of stock tracking; and idempotently returns the existing row on a
131
+ * re-subscribe.
132
+ */
133
+ subscribe(input: SubscribeInput): Promise<AvailabilityNotification>;
134
+ cancel(id: string): Promise<void>;
135
+ listForAdmin(filter?: AdminListFilter): Promise<{
136
+ items: AdminListRow[];
137
+ page: number;
138
+ pageSize: number;
139
+ total: number;
140
+ }>;
141
+ /**
142
+ * Restock fan-out — invoked by AvailabilityWorker when cumulative on-hand
143
+ * crosses from 0 to > 0 for a (product, variant) pair. Emits one mail
144
+ * per queued row and flips status → 'notified'.
145
+ */
146
+ processRestockedFanOut(input: {
147
+ productId: string;
148
+ variantId?: string | null;
149
+ }): Promise<{
150
+ notified: number;
151
+ }>;
152
+ /** Stand-in helper used to keep the unused entity import alive. */
153
+ static getStockLevelEntity(): typeof StockLevel;
154
+ }
155
+ //# sourceMappingURL=availability-notification-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"availability-notification-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/availability-notification-service.ts"],"names":[],"mappings":"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"}