@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,178 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * D-187 — `availability_notifications` gains its organisation, and a row that
4
+ * names a customer account carries one.
5
+ *
6
+ * Feature 087 Group B, class 3 of 4 (`specs/087-tenant-scope-enforcement/`).
7
+ * `AvailabilityNotification` is `@CustomerScoped`, and until this migration the
8
+ * `allowed-set` arm of `customerFilterCond` had no column to grant on: a sales
9
+ * representative assigned to the buyer's own organisation was shown **none** of
10
+ * their back-in-stock subscriptions and told so, through the
11
+ * `ORGANIZATION_ATTRIBUTION_PENDING` notice the refusing arm records. The
12
+ * column is what turns that into an answer.
13
+ *
14
+ * ## The column and the read arrive together, on purpose
15
+ *
16
+ * `customerOrganizationColumn` asks the ORM's own metadata whether the filtered
17
+ * entity carries `organizationId`, **per query**, so the entity property in
18
+ * this same merge request is what switches the grant on — there is no third
19
+ * artefact, no flag and no staging. At the same instant the notice retires
20
+ * itself: it is keyed on the column's *absence*.
21
+ *
22
+ * That pairing is why this migration is not "column and backfill". From the
23
+ * moment the grant is live, a row inserted without an organisation is invisible
24
+ * to the representative who serves that organisation, on a screen that has just
25
+ * stopped explaining itself — and MikroORM applies no filter to `INSERT`
26
+ * (`r1-spike.md` §4, measured), so the filter cannot refuse it. The `CHECK`
27
+ * below is the only refusal an `INSERT` has.
28
+ *
29
+ * ## An implication, not an equivalence
30
+ *
31
+ * `customer_account_id is null or organization_id is not null` is FR-010 and
32
+ * FR-011 together and nothing wider: an **owned** row has an organisation, an
33
+ * **ownerless** row need not. This table is ownerless by construction for the
34
+ * storefront's anonymous "notify me when in stock" dialog — feature 010 dropped
35
+ * `NOT NULL` from `customer_account_id` and added `an_recipient_check`
36
+ * (`20260503T182812_inventory_workflow.ts`), whose whole purpose is to admit a
37
+ * row whose only recipient is an e-mail address — and who such a row belongs to
38
+ * is R-6's open question. The equivalence would answer it in the schema, which
39
+ * D-187 declines to do.
40
+ *
41
+ * Unlike `pwa`, nothing here can produce the opposite shape. There is no
42
+ * association write on this table: `customer_account_id` is written once, in
43
+ * `AvailabilityNotificationService.subscribe`'s `em.create`, and never assigned
44
+ * on an existing row (grep of `packages/modules/inventory/src` for
45
+ * `customerAccountId`: one write, the rest reads). So an ownerless row carrying
46
+ * an organisation is not reachable, and the shape the implication leaves open
47
+ * is empty here rather than held by a test.
48
+ *
49
+ * ## No foreign key
50
+ *
51
+ * Matching `Cart`, `Comparison` and `PushSubscription`, none of which carries
52
+ * one. D-187 withdraws `r1-spike.md` §8's `on delete set null` recommendation
53
+ * as contradicting FR-011: it produces a row that names an account and no
54
+ * organisation, which is precisely the state this migration makes unreachable,
55
+ * and under the constraint below it would abort an unrelated organisation
56
+ * delete with a message about a table the operator was not touching. If one is
57
+ * ever taken here it must be `restrict`, and it is not this migration's
58
+ * decision.
59
+ *
60
+ * ## Derive, then count, then refuse — never delete
61
+ *
62
+ * The derivation is the owning account's own organisation. **On this table the
63
+ * refusal is a real branch, not a formality**, and that is why it is written
64
+ * with as much care as the derivation: like `push_subscriptions` and unlike
65
+ * `comparisons`, `availability_notifications` has **no foreign key** on
66
+ * `customer_account_id` — the core foundation migration
67
+ * (`20260424T165847_core_foundation_init.ts`) declares the column and an index
68
+ * and constrains nothing — so a row naming an account that is gone is
69
+ * representable here.
70
+ *
71
+ * It is nevertheless empty today, and the reason is worth knowing rather than
72
+ * hoping: a customer account is never hard-deleted in this tree.
73
+ * `customer-account-lifecycle-ports.ts` soft-deletes, restores and
74
+ * **anonymises** — the last one rewrites the e-mail and the name and keeps the
75
+ * row — and every one of those still satisfies `organization_id NOT NULL`
76
+ * (D-178), so it still derives. Change any of that and the count below is the
77
+ * only thing that would notice.
78
+ *
79
+ * It raises with the count and up to twenty ids and deletes nothing (D-184), in
80
+ * the shape `20260825T141659_customer_accounts_organization_required.ts`
81
+ * established and `20260830T112911_carts_organization_attribution_check.ts` and
82
+ * `20260830T172022_pwa_organization_attribution.ts` repeated. Deleting would be
83
+ * particularly wrong here: a queued subscription is a promise to a customer
84
+ * that they will be told when the product returns, and a migration is not the
85
+ * place to break one.
86
+ *
87
+ * ## Ownership, and nothing to declare in the manifest
88
+ *
89
+ * `availability_notifications` is the one Group B table whose `create table` is
90
+ * not its module's — it comes from the core foundation migration. This column
91
+ * migration is still `inventory`'s, because the entity is, and module ownership
92
+ * is what the registry and a hard uninstall key on (D-142).
93
+ *
94
+ * `inventory` already declares both `customer_accounts` and `organizations` in
95
+ * its manifest `dependencies`, so the table this reads is created before this
96
+ * runs. No foreign key is added, so `fk-dependency-drift` has nothing to say
97
+ * either, and a migration naming another module's table is outside
98
+ * `check:module-boundary`'s population by that check's own rule.
99
+ *
100
+ * The standing guard is
101
+ * `backend/test/integration/tenancy/customer-scoped-organization-completeness.test.ts`,
102
+ * which derives its population from the ORM's metadata rather than from a list
103
+ * — so this class is covered by gaining the column, with no edit to that file.
104
+ */
105
+ export class Migration20260830T182139InventoryOrganizationAttribution extends Migration {
106
+ async up() {
107
+ // 1. The column. Nullable, because an anonymous subscriber legitimately has
108
+ // none (FR-011) and `an_recipient_check` exists to admit exactly that
109
+ // row.
110
+ this.addSql(`alter table "availability_notifications" add column "organization_id" uuid null;`);
111
+ // 2. Derive the organisation from the account that owns the subscription.
112
+ // Every account carries one since D-178, including a soft-deleted or
113
+ // anonymised one, so this is total for every row whose account is still
114
+ // there. Step 3 is about the rows for which it is not.
115
+ this.addSql(`
116
+ update "availability_notifications" an
117
+ set "organization_id" = ca."organization_id"
118
+ from "customer_accounts" ca
119
+ where ca."id" = an."customer_account_id"
120
+ and an."organization_id" is null;
121
+ `);
122
+ // 3. Count what is left and refuse. This table has no foreign key on
123
+ // `customer_account_id`, so a dangling account id is representable and
124
+ // this branch is real. It deletes nothing (D-184).
125
+ this.addSql(`
126
+ do $$
127
+ declare
128
+ remaining bigint;
129
+ sample text;
130
+ begin
131
+ select count(*) into remaining
132
+ from "availability_notifications"
133
+ where "customer_account_id" is not null
134
+ and "organization_id" is null;
135
+ if remaining > 0 then
136
+ select string_agg(id::text, ', ') into sample from (
137
+ select "id" from "availability_notifications"
138
+ where "customer_account_id" is not null
139
+ and "organization_id" is null
140
+ order by "id" limit 20
141
+ ) s;
142
+ raise exception
143
+ 'D-187: % availability_notifications row(s) still name a customer account with organization_id IS NULL after derivation. First ids: %. Every customer account has an organization (D-178) and none is ever hard-deleted, so these rows name an account that is gone. Do not delete them - find out how they got here.',
144
+ remaining, sample;
145
+ end if;
146
+ end $$;
147
+ `);
148
+ // 4. The constraint.
149
+ this.addSql(`
150
+ alter table "availability_notifications"
151
+ add constraint "availability_notifications_organization_attribution_chk"
152
+ check ("customer_account_id" is null or "organization_id" is not null);
153
+ `);
154
+ // 5. The index the entity property declares. Whole rather than partial, and
155
+ // named `<table>_<column>_index`, because that is this table's own style
156
+ // for an entity-declared single-column index:
157
+ // `availability_notifications_customer_account_id_index` is what
158
+ // `@Index()` on `customerAccountId` produced and it is unfiltered. The
159
+ // `an_*` names on this table belong to the hand-written composite index
160
+ // and the two check constraints, which are a different kind of object.
161
+ // `pwa`'s index is partial for the same reason in reverse — it followed
162
+ // `push_subscriptions_customer_idx`, which is partial.
163
+ this.addSql(`
164
+ create index "availability_notifications_organization_id_index"
165
+ on "availability_notifications" ("organization_id");
166
+ `);
167
+ }
168
+ async down() {
169
+ // The constraint, the index and the column. The organisations step 2
170
+ // derived go with the column they were written into — there is nothing to
171
+ // preserve, because each of them was already implied by the account its row
172
+ // names.
173
+ this.addSql(`alter table "availability_notifications" drop constraint if exists "availability_notifications_organization_attribution_chk";`);
174
+ this.addSql(`drop index if exists "availability_notifications_organization_id_index";`);
175
+ this.addSql(`alter table "availability_notifications" drop column if exists "organization_id";`);
176
+ }
177
+ }
178
+ //# sourceMappingURL=20260830T182139_inventory_organization_attribution.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260830T182139_inventory_organization_attribution.js","sourceRoot":"","sources":["../../src/migrations/20260830T182139_inventory_organization_attribution.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsGG;AACH,MAAM,OAAO,wDAAyD,SAAQ,SAAS;IAC5E,KAAK,CAAC,EAAE;QACf,4EAA4E;QAC5E,yEAAyE;QACzE,UAAU;QACV,IAAI,CAAC,MAAM,CACT,kFAAkF,CACnF,CAAC;QAEF,0EAA0E;QAC1E,wEAAwE;QACxE,2EAA2E;QAC3E,0DAA0D;QAC1D,IAAI,CAAC,MAAM,CAAC;;;;;;KAMX,CAAC,CAAC;QAEH,qEAAqE;QACrE,0EAA0E;QAC1E,sDAAsD;QACtD,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;KAsBX,CAAC,CAAC;QAEH,qBAAqB;QACrB,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAC;QAEH,4EAA4E;QAC5E,4EAA4E;QAC5E,iDAAiD;QACjD,oEAAoE;QACpE,0EAA0E;QAC1E,2EAA2E;QAC3E,0EAA0E;QAC1E,2EAA2E;QAC3E,0DAA0D;QAC1D,IAAI,CAAC,MAAM,CAAC;;;KAGX,CAAC,CAAC;IACL,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,qEAAqE;QACrE,0EAA0E;QAC1E,4EAA4E;QAC5E,SAAS;QACT,IAAI,CAAC,MAAM,CACT,+HAA+H,CAChI,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,0EAA0E,CAAC,CAAC;QACxF,IAAI,CAAC,MAAM,CACT,mFAAmF,CACpF,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,34 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * The `organization_warehouses` bridge — the warehouses an organization is
4
+ * assigned (feature 026).
5
+ *
6
+ * It was created by `organizations`' frozen
7
+ * `Migration20260611T140349OrganizationsConsolidation` until
8
+ * `specs/120-migration-closure-bridge-ownership/` Phase 3, where its
9
+ * `warehouses` foreign key named a table `organizations` neither owns nor
10
+ * declares. That is D-226's bridge rule one namespace over: a bridge between
11
+ * an always-present near side (`organizations`) and a switchable far side
12
+ * (`inventory`) belongs to the far side, because only the far side can be
13
+ * ordered after both tables — `inventory` declares `organizations`, and
14
+ * `organizations` cannot declare `inventory` without inverting an edge that
15
+ * already runs the other way.
16
+ *
17
+ * So an instance that does not install `inventory` no longer carries a
18
+ * migration whose foreign key names `warehouses`, and one that does gets the
19
+ * bridge with its far side. The payoff is the one the rule promises:
20
+ * `module:uninstall --hard inventory` now reverts this table, because a hard
21
+ * uninstall reverts by registry `moduleId`.
22
+ *
23
+ * `if not exists`, because every database that has already applied the
24
+ * frozen migration has this table. The storage keys on the class name and
25
+ * holds no checksum, so the reduced frozen body is not re-offered there and
26
+ * this migration is the no-op it reads as; on a fresh database it is the
27
+ * creation. The statements are the frozen ones verbatim — same columns, same
28
+ * primary key, same two foreign keys — so the two paths reach one schema.
29
+ */
30
+ export declare class Migration20260912T125716InventoryOrganizationWarehouses extends Migration {
31
+ up(): Promise<void>;
32
+ down(): Promise<void>;
33
+ }
34
+ //# sourceMappingURL=20260912T125716_inventory_organization_warehouses.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260912T125716_inventory_organization_warehouses.d.ts","sourceRoot":"","sources":["../../src/migrations/20260912T125716_inventory_organization_warehouses.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,uDAAwD,SAAQ,SAAS;IACrE,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAGrC"}
@@ -0,0 +1,50 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * The `organization_warehouses` bridge — the warehouses an organization is
4
+ * assigned (feature 026).
5
+ *
6
+ * It was created by `organizations`' frozen
7
+ * `Migration20260611T140349OrganizationsConsolidation` until
8
+ * `specs/120-migration-closure-bridge-ownership/` Phase 3, where its
9
+ * `warehouses` foreign key named a table `organizations` neither owns nor
10
+ * declares. That is D-226's bridge rule one namespace over: a bridge between
11
+ * an always-present near side (`organizations`) and a switchable far side
12
+ * (`inventory`) belongs to the far side, because only the far side can be
13
+ * ordered after both tables — `inventory` declares `organizations`, and
14
+ * `organizations` cannot declare `inventory` without inverting an edge that
15
+ * already runs the other way.
16
+ *
17
+ * So an instance that does not install `inventory` no longer carries a
18
+ * migration whose foreign key names `warehouses`, and one that does gets the
19
+ * bridge with its far side. The payoff is the one the rule promises:
20
+ * `module:uninstall --hard inventory` now reverts this table, because a hard
21
+ * uninstall reverts by registry `moduleId`.
22
+ *
23
+ * `if not exists`, because every database that has already applied the
24
+ * frozen migration has this table. The storage keys on the class name and
25
+ * holds no checksum, so the reduced frozen body is not re-offered there and
26
+ * this migration is the no-op it reads as; on a fresh database it is the
27
+ * creation. The statements are the frozen ones verbatim — same columns, same
28
+ * primary key, same two foreign keys — so the two paths reach one schema.
29
+ */
30
+ export class Migration20260912T125716InventoryOrganizationWarehouses extends Migration {
31
+ async up() {
32
+ this.addSql(`
33
+ create table if not exists "organization_warehouses" (
34
+ "organization_id" uuid not null,
35
+ "warehouse_id" uuid not null,
36
+ "created_at" timestamptz not null default now(),
37
+ constraint "organization_warehouses_pkey"
38
+ primary key ("organization_id", "warehouse_id"),
39
+ constraint "organization_warehouses_organization_fk"
40
+ foreign key ("organization_id") references "organizations" ("id") on delete cascade,
41
+ constraint "organization_warehouses_warehouse_fk"
42
+ foreign key ("warehouse_id") references "warehouses" ("id") on delete cascade
43
+ );
44
+ `);
45
+ }
46
+ async down() {
47
+ this.addSql('drop table if exists "organization_warehouses" cascade;');
48
+ }
49
+ }
50
+ //# sourceMappingURL=20260912T125716_inventory_organization_warehouses.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260912T125716_inventory_organization_warehouses.js","sourceRoot":"","sources":["../../src/migrations/20260912T125716_inventory_organization_warehouses.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,OAAO,uDAAwD,SAAQ,SAAS;IAC3E,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;KAYX,CAAC,CAAC;IACL,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,yDAAyD,CAAC,CAAC;IACzE,CAAC;CACF"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * Listed in ascending timestamp, which orders **this module's own** migrations
10
+ * and nothing else (feature 081). Where the block sits relative to every other
11
+ * module's is decided by the manifest `dependencies` graph.
12
+ *
13
+ * The **named** exports stay, and the asymmetry with `./backend` — which
14
+ * publishes an array and no entity class by name (D-168) — is deliberate.
15
+ * `db/migrations-registry.generated.ts` imports each class by name from this
16
+ * specifier, and a migration class name is contract in a way an entity class
17
+ * name is not: `mikro_orm_migrations` persists it, so it is a string every
18
+ * already-migrated database holds.
19
+ *
20
+ * A class that is in neither the array nor the barrel is a migration that does
21
+ * not run: `migration:pending` reports nothing pending and the first symptom
22
+ * is a query against a table nobody created.
23
+ */
24
+ import { Migration20260503T182812InventoryWorkflow } from './20260503T182812_inventory_workflow.js';
25
+ import { Migration20260611T140347InventoryWarehouseDefaultLowStockThreshold } from './20260611T140347_inventory_warehouse_default_low_stock_threshold.js';
26
+ import { Migration20260611T140348InventoryPerWarehouseLowStockThresholds } from './20260611T140348_inventory_per_warehouse_low_stock_thresholds.js';
27
+ import { Migration20260818T081243InventoryStockAllocationOrderItemFk } from './20260818T081243_inventory_stock_allocation_order_item_fk.js';
28
+ import { Migration20260830T182139InventoryOrganizationAttribution } from './20260830T182139_inventory_organization_attribution.js';
29
+ import { Migration20260912T125716InventoryOrganizationWarehouses } from './20260912T125716_inventory_organization_warehouses.js';
30
+ export declare const migrations: (typeof Migration20260503T182812InventoryWorkflow)[];
31
+ export { Migration20260503T182812InventoryWorkflow, Migration20260611T140347InventoryWarehouseDefaultLowStockThreshold, Migration20260611T140348InventoryPerWarehouseLowStockThresholds, Migration20260818T081243InventoryStockAllocationOrderItemFk, Migration20260830T182139InventoryOrganizationAttribution, Migration20260912T125716InventoryOrganizationWarehouses, };
32
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,yCAAyC,EAAE,MAAM,yCAAyC,CAAC;AACpG,OAAO,EAAE,kEAAkE,EAAE,MAAM,sEAAsE,CAAC;AAC1J,OAAO,EAAE,+DAA+D,EAAE,MAAM,mEAAmE,CAAC;AACpJ,OAAO,EAAE,2DAA2D,EAAE,MAAM,+DAA+D,CAAC;AAC5I,OAAO,EAAE,wDAAwD,EAAE,MAAM,yDAAyD,CAAC;AACnI,OAAO,EAAE,uDAAuD,EAAE,MAAM,wDAAwD,CAAC;AAEjI,eAAO,MAAM,UAAU,sDAOtB,CAAC;AAEF,OAAO,EACL,yCAAyC,EACzC,kEAAkE,EAClE,+DAA+D,EAC/D,2DAA2D,EAC3D,wDAAwD,EACxD,uDAAuD,GACxD,CAAC"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The `./migrations` subpath — every migration class this module owns, as one
3
+ * ordered `migrations` array.
4
+ *
5
+ * The array is what the platform reads when this module is **installed**:
6
+ * `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
7
+ * the package outright when it is absent (D-168).
8
+ *
9
+ * Listed in ascending timestamp, which orders **this module's own** migrations
10
+ * and nothing else (feature 081). Where the block sits relative to every other
11
+ * module's is decided by the manifest `dependencies` graph.
12
+ *
13
+ * The **named** exports stay, and the asymmetry with `./backend` — which
14
+ * publishes an array and no entity class by name (D-168) — is deliberate.
15
+ * `db/migrations-registry.generated.ts` imports each class by name from this
16
+ * specifier, and a migration class name is contract in a way an entity class
17
+ * name is not: `mikro_orm_migrations` persists it, so it is a string every
18
+ * already-migrated database holds.
19
+ *
20
+ * A class that is in neither the array nor the barrel is a migration that does
21
+ * not run: `migration:pending` reports nothing pending and the first symptom
22
+ * is a query against a table nobody created.
23
+ */
24
+ import { Migration20260503T182812InventoryWorkflow } from './20260503T182812_inventory_workflow.js';
25
+ import { Migration20260611T140347InventoryWarehouseDefaultLowStockThreshold } from './20260611T140347_inventory_warehouse_default_low_stock_threshold.js';
26
+ import { Migration20260611T140348InventoryPerWarehouseLowStockThresholds } from './20260611T140348_inventory_per_warehouse_low_stock_thresholds.js';
27
+ import { Migration20260818T081243InventoryStockAllocationOrderItemFk } from './20260818T081243_inventory_stock_allocation_order_item_fk.js';
28
+ import { Migration20260830T182139InventoryOrganizationAttribution } from './20260830T182139_inventory_organization_attribution.js';
29
+ import { Migration20260912T125716InventoryOrganizationWarehouses } from './20260912T125716_inventory_organization_warehouses.js';
30
+ export const migrations = [
31
+ Migration20260503T182812InventoryWorkflow,
32
+ Migration20260611T140347InventoryWarehouseDefaultLowStockThreshold,
33
+ Migration20260611T140348InventoryPerWarehouseLowStockThresholds,
34
+ Migration20260818T081243InventoryStockAllocationOrderItemFk,
35
+ Migration20260830T182139InventoryOrganizationAttribution,
36
+ Migration20260912T125716InventoryOrganizationWarehouses,
37
+ ];
38
+ export { Migration20260503T182812InventoryWorkflow, Migration20260611T140347InventoryWarehouseDefaultLowStockThreshold, Migration20260611T140348InventoryPerWarehouseLowStockThresholds, Migration20260818T081243InventoryStockAllocationOrderItemFk, Migration20260830T182139InventoryOrganizationAttribution, Migration20260912T125716InventoryOrganizationWarehouses, };
39
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,yCAAyC,EAAE,MAAM,yCAAyC,CAAC;AACpG,OAAO,EAAE,kEAAkE,EAAE,MAAM,sEAAsE,CAAC;AAC1J,OAAO,EAAE,+DAA+D,EAAE,MAAM,mEAAmE,CAAC;AACpJ,OAAO,EAAE,2DAA2D,EAAE,MAAM,+DAA+D,CAAC;AAC5I,OAAO,EAAE,wDAAwD,EAAE,MAAM,yDAAyD,CAAC;AACnI,OAAO,EAAE,uDAAuD,EAAE,MAAM,wDAAwD,CAAC;AAEjI,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,yCAAyC;IACzC,kEAAkE;IAClE,+DAA+D;IAC/D,2DAA2D;IAC3D,wDAAwD;IACxD,uDAAuD;CACxD,CAAC;AAEF,OAAO,EACL,yCAAyC,EACzC,kEAAkE,EAClE,+DAA+D,EAC/D,2DAA2D,EAC3D,wDAAwD,EACxD,uDAAuD,GACxD,CAAC"}
@@ -0,0 +1,183 @@
1
+ /**
2
+ * The port interfaces `inventory` publishes whose signature carries a MikroORM
3
+ * `EntityManager`, and **nothing that exists at runtime** (feature 080, T048;
4
+ * D-169, D-171).
5
+ *
6
+ * `tsc` compiles this file to `export {};`. That is the property D-171 makes
7
+ * the boundary decision on — *a subpath is contract surface iff the module it
8
+ * resolves to exports no runtime binding* — and it is why this declaration has
9
+ * its own file rather than sitting on top of a service module, which exports
10
+ * the class beside it. A consumer naming that file names the owner's
11
+ * implementation, whatever `import type` erases; a consumer naming this one
12
+ * names a declaration and can name nothing else.
13
+ *
14
+ * **This is not yet a supported specifier and the ledger still counts it.**
15
+ * `inventory` is not a workspace package, so there is no `exports` map for this
16
+ * to be a subpath of, and `check:module-boundary` reads `orders`' relative
17
+ * import exactly as it read the two entity imports it replaces — D-171 says so
18
+ * in as many words: `resolveModulePackage` returns `null` for any specifier
19
+ * starting with `.`, so an unconverted reach has no subpath for the exemption
20
+ * to apply to, and reaching the exempt state takes three separable edits
21
+ * (package the owner, publish the interface, rewrite the specifier). This file
22
+ * is the second of those three, taken early because it is the one that does not
23
+ * need the module to move.
24
+ *
25
+ * The rest of this module's surface is in `@endora-commerce/contracts` and
26
+ * belongs there — `InventoryStockReadPort`, `InventoryFulfilmentPlanningPort`,
27
+ * `InventoryStockImportPort` and `InventoryProductThresholdWritePort` are
28
+ * contract DTOs end to end, and the planning port is pure over its arguments.
29
+ * This one qualifies for `./ports` on D-171's own test — *"does this signature
30
+ * stop the interface living in `packages/contracts`"* — and the answer is its
31
+ * `EntityManager` parameter, which that package may not name because `admin`
32
+ * and `storefront` both compile it (FR-034).
33
+ *
34
+ * No entity class leaves by this door, type-only included (D-168).
35
+ */
36
+ import type { EntityManager } from '@mikro-orm/postgresql';
37
+ /**
38
+ * What one candidate warehouse holds for one order line, under the write lock
39
+ * {@link InventoryReservationApplyPort.lockAvailabilityForPlacement} took — a
40
+ * published record and never the managed `stock_levels` row (D-77's first
41
+ * narrowing).
42
+ *
43
+ * `available` is `onHand - reserved` at the moment of the lock, and is `0` for
44
+ * a warehouse that holds no row for the line at all: an absent row and a row at
45
+ * zero are the same answer to *"how many can I promise"*, and the difference —
46
+ * whether an `insert` or an `update` follows — is this module's to know.
47
+ */
48
+ export interface PlacementStockSnapshot {
49
+ readonly warehouseId: string;
50
+ readonly available: number;
51
+ }
52
+ /**
53
+ * Container name: `inventoryReservationApplyPort`. Owner: `inventory`.
54
+ *
55
+ * The stock reservation order placement performs, run on the **caller's**
56
+ * `EntityManager` (D-169). `orders` is the one consumer, and the four methods
57
+ * are the four steps of one protocol: lock the candidates, apply the plan,
58
+ * record the allocations once the order items exist, and release them if the
59
+ * order is later cancelled.
60
+ *
61
+ * **The seam is not a defect in the design, it *is* the design.**
62
+ * `stock_allocations_order_item_fk` (`stock_allocations.order_item_id` ->
63
+ * `order_items.id`, `on delete restrict`,
64
+ * `inventory/migrations/20260818T081243_inventory_stock_allocation_order_item_fk.ts`)
65
+ * means an allocation row cannot exist before its order item does, and the
66
+ * order items are not committed until placement returns — so a port that opened
67
+ * its own transaction could not satisfy a foreign key against rows it cannot
68
+ * see. The `PESSIMISTIC_WRITE` on `stock_levels` is the other half of the same
69
+ * fact: it has to be held by the transaction that writes the order, or two
70
+ * placements allocate the same unit
71
+ * (`test/contract/orders/place-stock-race.test.ts`), and a `reserved` increment
72
+ * committed separately would survive a placement that then rolled back. A
73
+ * foreign key needs the **table** and never the class (D-169), so that
74
+ * constraint stands while this module publishes no entity class by name.
75
+ *
76
+ * **The policy half is not here, and neither is the read half.** Which
77
+ * warehouse a line is allocated to is `InventoryFulfilmentPlanningPort`, pure
78
+ * over its arguments; the channel → warehouse binding and every standalone
79
+ * stock read are `InventoryStockReadPort`, on this module's own
80
+ * `EntityManager`. Both live in `@endora-commerce/contracts` and stay there. A
81
+ * read handed an `EntityManager` is a write seam re-opened to serve a read
82
+ * (D-169), and `candidatesFor` is the worked example of the difference: it
83
+ * answers the richer question and answers it outside the caller's transaction,
84
+ * which is precisely why it cannot take the lock this port's first method
85
+ * takes.
86
+ *
87
+ * **Owner off:** this module is switchable (`inventory.enabled`), and `orders`
88
+ * declares the edge `degrades-without` rather than binding it —
89
+ * `stock_allocations_order_item_fk` obliges `inventory` to declare `orders`, so
90
+ * the edge cannot be declared back, and an acknowledged edge would keep the
91
+ * bind and make that control unusable, because `orders` is non-deactivatable.
92
+ * So placement asks the effective-state seam for this module's own id once,
93
+ * before the reservation block, and skips it whole; the cancellation path asks
94
+ * the same question before releasing. The literal is deliberately not spelled
95
+ * here: `check-entry-presence`'s tree proof blanks that expression out of the
96
+ * one file of each module that carries it, and a second file carrying it in
97
+ * prose makes that proof ambiguous. With this module off an order is placed without
98
+ * reserving stock and a cancellation releases nothing — which is what
99
+ * `inventory.enabled`'s own description promises an operator, and what the
100
+ * platform did **not** do before D-94.4, when every placement locked
101
+ * `stock_levels`, incremented `reserved` and inserted `stock_allocations` rows
102
+ * with the module switched off (issue #188).
103
+ */
104
+ export interface InventoryReservationApplyPort {
105
+ /**
106
+ * Lock the line's row in each candidate warehouse under
107
+ * `PESSIMISTIC_WRITE` and answer what each holds.
108
+ *
109
+ * `em` is **required** (D-169). Its one caller is `placeOrder`, which always
110
+ * passes the `EntityManager` its own transaction runs on; an optional
111
+ * parameter is what lets the same method double as a standalone read, and
112
+ * that is a lie about a lock whose whole content is that it is held until the
113
+ * order commits. The standalone read is a different method on a different
114
+ * port and takes no `EntityManager`: `InventoryStockReadPort.candidatesFor`.
115
+ *
116
+ * One entry per requested warehouse, in the order requested, so the caller
117
+ * can zip it against the channel binding it already holds.
118
+ */
119
+ lockAvailabilityForPlacement(em: EntityManager, input: {
120
+ productId: string;
121
+ variantId: string | null;
122
+ warehouseIds: readonly string[];
123
+ }): Promise<PlacementStockSnapshot[]>;
124
+ /**
125
+ * Apply the allocation plan: raise `reserved` by the planned quantity in each
126
+ * named warehouse, opening a row at `onHand = 0` where the line has none.
127
+ *
128
+ * Runs on the same `em` as the lock above, and is the write that lock exists
129
+ * to protect. It is not flushed here: the caller's transaction flushes it
130
+ * beside the order items whose foreign key holds the allocation rows, which
131
+ * is the statement order the reservation has always had.
132
+ */
133
+ reserveForPlacement(em: EntityManager, input: {
134
+ productId: string;
135
+ variantId: string | null;
136
+ allocations: ReadonlyArray<{
137
+ warehouseId: string;
138
+ quantity: number;
139
+ }>;
140
+ }): Promise<void>;
141
+ /**
142
+ * Record one allocation row per `(order item, warehouse)` pair, so an admin
143
+ * can trace fulfilment provenance and so cancellation has something to
144
+ * release.
145
+ *
146
+ * Called **after** the order items are flushed and required to be:
147
+ * `stock_allocations_order_item_fk` is `on delete restrict`, so the row
148
+ * cannot exist before its order item does.
149
+ */
150
+ recordAllocationsForOrderItems(em: EntityManager, input: {
151
+ allocations: ReadonlyArray<{
152
+ orderItemId: string;
153
+ warehouseId: string;
154
+ quantity: number;
155
+ isBackorder: boolean;
156
+ }>;
157
+ }): Promise<void>;
158
+ /**
159
+ * Release every un-released allocation held by the named order items,
160
+ * decrementing each affected `reserved` counter and stamping `released_at`.
161
+ *
162
+ * Idempotent: a second run over the same items releases nothing, because
163
+ * already-released rows are filtered out by `released_at is null`. `em` is
164
+ * the transaction the caller opened for the cancellation — a different
165
+ * transaction from placement's, and required for the same reason: the
166
+ * counter decrement and the `released_at` stamp are one operation.
167
+ *
168
+ * The caller passes the line's product and variant because `stock_allocations`
169
+ * records the warehouse and the order item and not the product; resolving it
170
+ * here would mean this module reading `order_items`, which belongs to
171
+ * `orders`.
172
+ */
173
+ releaseForOrderItems(em: EntityManager, input: {
174
+ items: ReadonlyArray<{
175
+ orderItemId: string;
176
+ productId: string;
177
+ variantId: string | null;
178
+ }>;
179
+ }): Promise<{
180
+ released: number;
181
+ }>;
182
+ }
183
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ports/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,MAAM,WAAW,6BAA6B;IAC5C;;;;;;;;;;;;;OAaG;IACH,4BAA4B,CAC1B,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;KACjC,GACA,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAC;IACrC;;;;;;;;OAQG;IACH,mBAAmB,CACjB,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,WAAW,EAAE,aAAa,CAAC;YAAE,WAAW,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACvE,GACA,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB;;;;;;;;OAQG;IACH,8BAA8B,CAC5B,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE;QACL,WAAW,EAAE,aAAa,CAAC;YACzB,WAAW,EAAE,MAAM,CAAC;YACpB,WAAW,EAAE,MAAM,CAAC;YACpB,QAAQ,EAAE,MAAM,CAAC;YACjB,WAAW,EAAE,OAAO,CAAC;SACtB,CAAC,CAAC;KACJ,GACA,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB;;;;;;;;;;;;;;OAcG;IACH,oBAAoB,CAClB,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE;QACL,KAAK,EAAE,aAAa,CAAC;YACnB,WAAW,EAAE,MAAM,CAAC;YACpB,SAAS,EAAE,MAAM,CAAC;YAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;SAC1B,CAAC,CAAC;KACJ,GACA,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ports/index.ts"],"names":[],"mappings":""}