@endora-commerce/mod-carts 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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/admin/components/CartApprovalPolicyPanel.d.ts +51 -0
  4. package/dist/admin/components/CartApprovalPolicyPanel.d.ts.map +1 -0
  5. package/dist/admin/components/CartApprovalPolicyPanel.js +59 -0
  6. package/dist/admin/components/CartApprovalPolicyPanel.js.map +1 -0
  7. package/dist/admin/index.d.ts +38 -0
  8. package/dist/admin/index.d.ts.map +1 -0
  9. package/dist/admin/index.js +73 -0
  10. package/dist/admin/index.js.map +1 -0
  11. package/dist/admin/pages/CartDetail.d.ts +3 -0
  12. package/dist/admin/pages/CartDetail.d.ts.map +1 -0
  13. package/dist/admin/pages/CartDetail.js +74 -0
  14. package/dist/admin/pages/CartDetail.js.map +1 -0
  15. package/dist/admin/pages/CartsList.d.ts +3 -0
  16. package/dist/admin/pages/CartsList.d.ts.map +1 -0
  17. package/dist/admin/pages/CartsList.js +90 -0
  18. package/dist/admin/pages/CartsList.js.map +1 -0
  19. package/dist/admin/zones/CartApprovalPolicy.d.ts +29 -0
  20. package/dist/admin/zones/CartApprovalPolicy.d.ts.map +1 -0
  21. package/dist/admin/zones/CartApprovalPolicy.js +11 -0
  22. package/dist/admin/zones/CartApprovalPolicy.js.map +1 -0
  23. package/dist/backend/cli/abandonment-sweep.d.ts +64 -0
  24. package/dist/backend/cli/abandonment-sweep.d.ts.map +1 -0
  25. package/dist/backend/cli/abandonment-sweep.js +17 -0
  26. package/dist/backend/cli/abandonment-sweep.js.map +1 -0
  27. package/dist/backend/entities/cart-audit-entry.entity.d.ts +29 -0
  28. package/dist/backend/entities/cart-audit-entry.entity.d.ts.map +1 -0
  29. package/dist/backend/entities/cart-audit-entry.entity.js +109 -0
  30. package/dist/backend/entities/cart-audit-entry.entity.js.map +1 -0
  31. package/dist/backend/entities/cart-item.entity.d.ts +42 -0
  32. package/dist/backend/entities/cart-item.entity.d.ts.map +1 -0
  33. package/dist/backend/entities/cart-item.entity.js +119 -0
  34. package/dist/backend/entities/cart-item.entity.js.map +1 -0
  35. package/dist/backend/entities/cart.entity.d.ts +96 -0
  36. package/dist/backend/entities/cart.entity.d.ts.map +1 -0
  37. package/dist/backend/entities/cart.entity.js +207 -0
  38. package/dist/backend/entities/cart.entity.js.map +1 -0
  39. package/dist/backend/index.d.ts +122 -0
  40. package/dist/backend/index.d.ts.map +1 -0
  41. package/dist/backend/index.js +184 -0
  42. package/dist/backend/index.js.map +1 -0
  43. package/dist/backend/routes.admin.d.ts +31 -0
  44. package/dist/backend/routes.admin.d.ts.map +1 -0
  45. package/dist/backend/routes.admin.js +95 -0
  46. package/dist/backend/routes.admin.js.map +1 -0
  47. package/dist/backend/routes.d.ts +101 -0
  48. package/dist/backend/routes.d.ts.map +1 -0
  49. package/dist/backend/routes.js +620 -0
  50. package/dist/backend/routes.js.map +1 -0
  51. package/dist/backend/routes.organization.d.ts +29 -0
  52. package/dist/backend/routes.organization.d.ts.map +1 -0
  53. package/dist/backend/routes.organization.js +150 -0
  54. package/dist/backend/routes.organization.js.map +1 -0
  55. package/dist/backend/services/cart-abandonment-settings.d.ts +34 -0
  56. package/dist/backend/services/cart-abandonment-settings.d.ts.map +1 -0
  57. package/dist/backend/services/cart-abandonment-settings.js +55 -0
  58. package/dist/backend/services/cart-abandonment-settings.js.map +1 -0
  59. package/dist/backend/services/cart-abandonment-worker.d.ts +110 -0
  60. package/dist/backend/services/cart-abandonment-worker.d.ts.map +1 -0
  61. package/dist/backend/services/cart-abandonment-worker.js +185 -0
  62. package/dist/backend/services/cart-abandonment-worker.js.map +1 -0
  63. package/dist/backend/services/cart-admin-service.d.ts +58 -0
  64. package/dist/backend/services/cart-admin-service.d.ts.map +1 -0
  65. package/dist/backend/services/cart-admin-service.js +397 -0
  66. package/dist/backend/services/cart-admin-service.js.map +1 -0
  67. package/dist/backend/services/cart-approval-service.d.ts +147 -0
  68. package/dist/backend/services/cart-approval-service.d.ts.map +1 -0
  69. package/dist/backend/services/cart-approval-service.js +242 -0
  70. package/dist/backend/services/cart-approval-service.js.map +1 -0
  71. package/dist/backend/services/cart-audit-service.d.ts +75 -0
  72. package/dist/backend/services/cart-audit-service.d.ts.map +1 -0
  73. package/dist/backend/services/cart-audit-service.js +70 -0
  74. package/dist/backend/services/cart-audit-service.js.map +1 -0
  75. package/dist/backend/services/cart-conversion-service.d.ts +104 -0
  76. package/dist/backend/services/cart-conversion-service.d.ts.map +1 -0
  77. package/dist/backend/services/cart-conversion-service.js +175 -0
  78. package/dist/backend/services/cart-conversion-service.js.map +1 -0
  79. package/dist/backend/services/cart-coupon-service.d.ts +129 -0
  80. package/dist/backend/services/cart-coupon-service.d.ts.map +1 -0
  81. package/dist/backend/services/cart-coupon-service.js +244 -0
  82. package/dist/backend/services/cart-coupon-service.js.map +1 -0
  83. package/dist/backend/services/cart-organization-visibility-service.d.ts +43 -0
  84. package/dist/backend/services/cart-organization-visibility-service.d.ts.map +1 -0
  85. package/dist/backend/services/cart-organization-visibility-service.js +98 -0
  86. package/dist/backend/services/cart-organization-visibility-service.js.map +1 -0
  87. package/dist/backend/services/cart-placement-apply-port.d.ts +38 -0
  88. package/dist/backend/services/cart-placement-apply-port.d.ts.map +1 -0
  89. package/dist/backend/services/cart-placement-apply-port.js +71 -0
  90. package/dist/backend/services/cart-placement-apply-port.js.map +1 -0
  91. package/dist/backend/services/cart-pricing-recompute.d.ts +62 -0
  92. package/dist/backend/services/cart-pricing-recompute.d.ts.map +1 -0
  93. package/dist/backend/services/cart-pricing-recompute.js +131 -0
  94. package/dist/backend/services/cart-pricing-recompute.js.map +1 -0
  95. package/dist/backend/services/cart-query-service.d.ts +29 -0
  96. package/dist/backend/services/cart-query-service.d.ts.map +1 -0
  97. package/dist/backend/services/cart-query-service.js +40 -0
  98. package/dist/backend/services/cart-query-service.js.map +1 -0
  99. package/dist/backend/services/cart-read-port.d.ts +40 -0
  100. package/dist/backend/services/cart-read-port.d.ts.map +1 -0
  101. package/dist/backend/services/cart-read-port.js +144 -0
  102. package/dist/backend/services/cart-read-port.js.map +1 -0
  103. package/dist/backend/services/cart-recompute-cache.d.ts +49 -0
  104. package/dist/backend/services/cart-recompute-cache.d.ts.map +1 -0
  105. package/dist/backend/services/cart-recompute-cache.js +56 -0
  106. package/dist/backend/services/cart-recompute-cache.js.map +1 -0
  107. package/dist/backend/services/cart-service.d.ts +165 -0
  108. package/dist/backend/services/cart-service.d.ts.map +1 -0
  109. package/dist/backend/services/cart-service.js +556 -0
  110. package/dist/backend/services/cart-service.js.map +1 -0
  111. package/dist/backend/services/cart-state-machine.d.ts +51 -0
  112. package/dist/backend/services/cart-state-machine.d.ts.map +1 -0
  113. package/dist/backend/services/cart-state-machine.js +74 -0
  114. package/dist/backend/services/cart-state-machine.js.map +1 -0
  115. package/dist/backend/services/cart-upsell-service.d.ts +57 -0
  116. package/dist/backend/services/cart-upsell-service.d.ts.map +1 -0
  117. package/dist/backend/services/cart-upsell-service.js +100 -0
  118. package/dist/backend/services/cart-upsell-service.js.map +1 -0
  119. package/dist/backend/services/customer-group-resolver.d.ts +27 -0
  120. package/dist/backend/services/customer-group-resolver.d.ts.map +1 -0
  121. package/dist/backend/services/customer-group-resolver.js +32 -0
  122. package/dist/backend/services/customer-group-resolver.js.map +1 -0
  123. package/dist/manifest.d.ts +221 -0
  124. package/dist/manifest.d.ts.map +1 -0
  125. package/dist/manifest.js +224 -0
  126. package/dist/manifest.js.map +1 -0
  127. package/dist/migrations/20260611T140352_carts_consolidation.d.ts +43 -0
  128. package/dist/migrations/20260611T140352_carts_consolidation.d.ts.map +1 -0
  129. package/dist/migrations/20260611T140352_carts_consolidation.js +237 -0
  130. package/dist/migrations/20260611T140352_carts_consolidation.js.map +1 -0
  131. package/dist/migrations/20260611T140413_carts_cart_item_packaging.d.ts +12 -0
  132. package/dist/migrations/20260611T140413_carts_cart_item_packaging.d.ts.map +1 -0
  133. package/dist/migrations/20260611T140413_carts_cart_item_packaging.js +20 -0
  134. package/dist/migrations/20260611T140413_carts_cart_item_packaging.js.map +1 -0
  135. package/dist/migrations/20260818T081253_carts_cart_completed_order_fk.d.ts +50 -0
  136. package/dist/migrations/20260818T081253_carts_cart_completed_order_fk.d.ts.map +1 -0
  137. package/dist/migrations/20260818T081253_carts_cart_completed_order_fk.js +62 -0
  138. package/dist/migrations/20260818T081253_carts_cart_completed_order_fk.js.map +1 -0
  139. package/dist/migrations/20260830T112911_carts_organization_attribution_check.d.ts +67 -0
  140. package/dist/migrations/20260830T112911_carts_organization_attribution_check.d.ts.map +1 -0
  141. package/dist/migrations/20260830T112911_carts_organization_attribution_check.js +121 -0
  142. package/dist/migrations/20260830T112911_carts_organization_attribution_check.js.map +1 -0
  143. package/dist/migrations/index.d.ts +30 -0
  144. package/dist/migrations/index.d.ts.map +1 -0
  145. package/dist/migrations/index.js +35 -0
  146. package/dist/migrations/index.js.map +1 -0
  147. package/dist/ports/index.d.ts +106 -0
  148. package/dist/ports/index.d.ts.map +1 -0
  149. package/dist/ports/index.js +2 -0
  150. package/dist/ports/index.js.map +1 -0
  151. package/docs/carts.md +232 -0
  152. package/i18n/en.json +96 -0
  153. package/i18n/pl.json +96 -0
  154. package/package.json +107 -0
  155. package/tailwind.css +14 -0
@@ -0,0 +1,224 @@
1
+ import { defineModuleManifest, defineModuleSettingsManifest, } from '@endora-commerce/contracts';
2
+ /**
3
+ * Carts module — feature 027 consolidation pass.
4
+ *
5
+ * Settings (`carts.abandonment.*`) declared here are seeded by the module-
6
+ * lifecycle ManifestReconciler on backend boot. No raw INSERT in the
7
+ * migration; the reconciler is the single seed path (matches the
8
+ * `inventory.*` and `organizations.*` patterns).
9
+ */
10
+ export const CARTS_SETTING_CODES = {
11
+ ABANDONMENT_INACTIVITY_MINUTES: 'carts.abandonment.inactivity_minutes',
12
+ ABANDONMENT_NOTIFICATION_RECIPIENT: 'carts.abandonment.notification_recipient',
13
+ };
14
+ /**
15
+ * The manifest defaults, exported so a read that has to degrade degrades to
16
+ * *this* binding rather than to a literal invented at the call site (feature
17
+ * 072, D-43).
18
+ *
19
+ * The two used to diverge: the manifest said 10080 while both readers' `catch`
20
+ * answered `0`, and `CartAbandonmentWorker` treats `<= 0` as "sweep nothing".
21
+ * The divergence did not merely lose the configured value — it inverted the
22
+ * feature, and it would still have been off after the channel was fixed. A
23
+ * compiled-in fallback is a second source of truth consulted only when the
24
+ * first is unreachable, i.e. exactly when nobody is watching.
25
+ */
26
+ export const DEFAULT_ABANDONMENT_INACTIVITY_MINUTES = 10080;
27
+ export const DEFAULT_ABANDONMENT_NOTIFICATION_RECIPIENT = '';
28
+ const settings = defineModuleSettingsManifest({
29
+ moduleCode: 'carts',
30
+ groups: [{ code: 'carts', name: 'Carts' }],
31
+ settings: [
32
+ {
33
+ code: CARTS_SETTING_CODES.ABANDONMENT_INACTIVITY_MINUTES,
34
+ name: 'Cart abandonment threshold (minutes)',
35
+ description: 'Minutes of inactivity before an Active cart is considered Abandoned. Default 10080 (7 days).',
36
+ groupCode: 'carts',
37
+ valueType: 'number',
38
+ defaultValue: DEFAULT_ABANDONMENT_INACTIVITY_MINUTES,
39
+ },
40
+ {
41
+ code: CARTS_SETTING_CODES.ABANDONMENT_NOTIFICATION_RECIPIENT,
42
+ name: 'Abandonment notification recipient',
43
+ description: 'Single e-mail address that receives an abandonment notification on every Active → Abandoned transition. Empty = no notification.',
44
+ groupCode: 'carts',
45
+ valueType: 'string',
46
+ defaultValue: DEFAULT_ABANDONMENT_NOTIFICATION_RECIPIENT,
47
+ },
48
+ ],
49
+ });
50
+ export const manifest = defineModuleManifest({
51
+ id: 'carts',
52
+ name: 'Carts',
53
+ description: 'Shopping cart aggregation, pricing, lifecycle, and approval gate.',
54
+ version: '2.0.0',
55
+ // Feature 075, Phase C adds `catalog`. Six files read the `Product`,
56
+ // `ProductPackagingUnit` and `ProductLink` entities directly — the cart line,
57
+ // the cart serializer, the admin detail, the up-sell strip, the pricing
58
+ // recompute and the quote-to-cart conversion — so an operator who switched
59
+ // `catalog` off still had carts naming, pricing and accepting products the
60
+ // platform had stopped serving, out of tables deactivation leaves in place.
61
+ // Binding rather than non-binding: a basket that cannot resolve what is in it
62
+ // must refuse, not quote a figure it cannot justify (issue #124's rule, which
63
+ // `price_lists` already stands on here).
64
+ //
65
+ // D-94.1 adds `orders`, for the foreign key `carts_completed_order_fk`
66
+ // (`carts.completed_order_id` -> `orders.id`, `on delete set null`) — the
67
+ // pointer this table has always had for its *other* terminal transition
68
+ // (`carts_converted_to_qr_fk`) and never for this one. AGENTS.md
69
+ // § Migrations item 4: a cross-module foreign key is declared here or the
70
+ // build fails.
71
+ //
72
+ // D-94.3 moves `promotions` out, to `acknowledgedDependencies` below. The
73
+ // edge is unchanged and still binds — what is withdrawn is the install
74
+ // ordering, which `promotion_usages_order_fk` now says runs the other way:
75
+ // `promotions` declares `orders`, `orders` is declared here, so keeping
76
+ // `promotions` in `dependencies` would close `carts -> promotions -> orders
77
+ // -> carts`.
78
+ dependencies: [
79
+ 'catalog',
80
+ 'customer_accounts',
81
+ 'orders',
82
+ 'organizations',
83
+ 'price_lists',
84
+ 'quote_requests',
85
+ 'sales_channels',
86
+ 'settings',
87
+ ],
88
+ // Feature 073, Amendment A1 — a real port edge whose `dependencies` entry
89
+ // would close a cycle. Read by the flip-time refusals and by
90
+ // `check-port-dependencies`; read by neither the install order nor the
91
+ // migration order.
92
+ acknowledgedDependencies: [
93
+ {
94
+ moduleId: 'promotions',
95
+ port: 'promotionService',
96
+ reason: 'The basket prices through `PromotionApplyPort.applyToCart`, which is a real bind: a ' +
97
+ 'cart that cannot resolve its discount must not quote a figure it cannot justify. ' +
98
+ 'The edge cannot go in `dependencies` since D-94.1, because ' +
99
+ '`promotion_usages_order_fk` obliges `promotions` to declare `orders`, this module ' +
100
+ 'declares `orders` for `carts_completed_order_fk`, and the three together close ' +
101
+ '`carts -> promotions -> orders -> carts`. Acknowledging it withdraws the install ' +
102
+ 'ordering the constraint says is backwards and withdraws nothing else — `promotions` ' +
103
+ 'stays exactly as (un)deactivatable under this module as it was.',
104
+ },
105
+ {
106
+ moduleId: 'promotions',
107
+ port: 'promotionCodePort',
108
+ reason: 'The coupon a buyer typed, resolved through the three-step lookup `promotions` owns — ' +
109
+ 'the legacy inline code, then the coupon table, then the promotion behind it, each ' +
110
+ 'filtered on `isActive`. It shares the cycle of `promotionService` above and is ' +
111
+ 'acknowledged for the same reason and with the same effect: the bind is kept, the ' +
112
+ 'ordering claim the foreign key contradicts is dropped.',
113
+ },
114
+ ],
115
+ /**
116
+ * The three error codes this module owns — feature 090 Phase 3
117
+ * (`specs/090-module-owned-error-codes/contracts/error-code-declaration.md`
118
+ * §1.1). This is where each sentence is looked up from: `errors.<CODE>` in this
119
+ * module's own `i18n/{en,pl}.json`, which already holds all three in both
120
+ * languages.
121
+ *
122
+ * The list is answer-preserving, not a judgement (§6.2 and §6.5): it is exactly
123
+ * what the prefix chain in `@endora-commerce/mod-i18n` routes here today,
124
+ * copied from the frozen capture at
125
+ * `backend/test/fixtures/error-code-routing/chain-answers.ts` rather than
126
+ * re-derived. Re-routing a code to a better owner is
127
+ * `specs/082-error-code-ownership/rulings.md` §9's remaining work and is
128
+ * deliberately not done here.
129
+ *
130
+ * **Two codes a reader will look for here and not find.** `MIN_NOT_MET` reads
131
+ * like a cart minimum and is `catalog`'s: it is one of three outcomes of bundle
132
+ * option validation (`bundle.service.ts`), and the chain routes it there.
133
+ * `PROMOTION_INVALID` reads like the coupon path below it and falls through the
134
+ * chain to the platform block; nothing in the tree raises it at all.
135
+ *
136
+ * **The tokens are the enum, not the bundle.** `CART_COUPON_REJECTED` is the
137
+ * one code in this module that carries refusal discriminators
138
+ * (`specs/082-error-code-ownership/contracts/error-code-ownership.md` §1.4):
139
+ * `routes.ts` puts `result.reason` in `details.code`, which the envelope reads
140
+ * as the tail of `errors.CART_COUPON_REJECTED.<token>`. That value's type is
141
+ * `CouponApplyDropped.reason` = `CouponDropReason` = `couponDropReasonSchema` in
142
+ * `packages/contracts/src/carts.ts`, and the seven members below are that
143
+ * enum's, in its order. The bundle happens to hold a sentence for each of the
144
+ * seven today, so reading the bundle instead would give the same answer — but
145
+ * only by coincidence: the bundle is a lower bound, and a reason added to the
146
+ * enum before its sentence is written would be missing from it. The enum is the
147
+ * authority because it is what the raise site can actually produce.
148
+ */
149
+ errorCodes: [
150
+ {
151
+ code: 'CART_COUPON_REJECTED',
152
+ tokens: [
153
+ 'invalid_code',
154
+ 'expired',
155
+ 'below_min_spend',
156
+ 'wrong_channel',
157
+ 'wrong_customer_group',
158
+ 'wrong_organization',
159
+ 'coupon_format_invalid',
160
+ ],
161
+ },
162
+ { code: 'CART_EMPTY' },
163
+ { code: 'CART_LINE_CAP_EXCEEDED' },
164
+ ],
165
+ settings,
166
+ // Feature 074 (Constitution XVII), test C2 — functional base. This reverses
167
+ // the reading 073 took. The quote-only deployment that argued for a switch
168
+ // here is real, but it does not want the *basket* gone: checkout, quick order
169
+ // and the quote flow all assemble one, so switching this off removes the
170
+ // structure every ordering path is built on rather than one path among
171
+ // several. A deployment that quotes and never checks out simply does not use
172
+ // checkout; `quote_requests` is where that choice belongs, and it stays
173
+ // operator-controlled.
174
+ //
175
+ // `carts.enabled` goes with the control it backed: one of the nineteen that
176
+ // never accepted a deactivation. The existing rows are removed by a core data
177
+ // migration (feature 074, FR-010a).
178
+ activation: {
179
+ nonDeactivatable: true,
180
+ reason: 'The basket every ordering path assembles — checkout, quick order and the quote flow all ' +
181
+ 'go through it.',
182
+ },
183
+ // Feature 026 checklist — `routes.admin.ts` gates on both codes and this
184
+ // manifest declared neither, so until T136 only a role holding `'*'` could
185
+ // reach the admin cart screens. The permission-inventory scanner that should
186
+ // have caught it read one call shape out of the several the tree writes; it
187
+ // now reads every shape, blanks comments and regex literals, and reports an
188
+ // argument it cannot resolve instead of dropping it.
189
+ //
190
+ // D-173 adds the third, and it is not this module's own code: the admin
191
+ // proxy for the per-Organization `requires_cart_approval` policy is gated on
192
+ // `customers:manage`, whose core `PERMISSION_CATALOGUE` row names
193
+ // `customers` — a module an operator can switch off, while this one declares
194
+ // itself non-deactivatable. Declaring it here makes this module a second
195
+ // *owner*, in the shape issue #213 gave `integrations:manage`, so the code
196
+ // stays grantable on `/admin-roles` while this surface is on. The label —
197
+ // *"Manage customer organizations"* — reads as a sentence about that
198
+ // endpoint, so the code is shared rather than replaced.
199
+ permissions: [
200
+ { code: 'carts:read', label: 'View customer carts' },
201
+ { code: 'carts:reject', label: 'Reject a cart pending organization approval' },
202
+ { code: 'customers:manage', label: 'Manage customer organizations' },
203
+ ],
204
+ i18n: { bundlesDir: 'i18n' },
205
+ docs: { dir: 'docs' },
206
+ });
207
+ /** Legacy export retained for backward compatibility. */
208
+ export const cartsManifest = settings;
209
+ /**
210
+ * The operator command this module declares — feature 080, T042b / D-160.9.
211
+ *
212
+ * It was `scripts/abandonment-sweep.ts`, the tree's one call site of
213
+ * `requireModuleEnabled` and the worked example D-157 was written around. The
214
+ * host asks that question now, at the declaration seam, for every command; see
215
+ * `cli/abandonment-sweep.ts` for what the move changed and what it did not.
216
+ */
217
+ export const cliCommands = [
218
+ {
219
+ name: 'abandonment-sweep',
220
+ summary: 'Run one pass of the cart abandonment sweep against the live database.',
221
+ run: async (context) => (await import('./backend/cli/abandonment-sweep.js')).abandonmentSweep(context),
222
+ },
223
+ ];
224
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,4BAA4B,GAE7B,MAAM,4BAA4B,CAAC;AAGpC;;;;;;;GAOG;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,8BAA8B,EAAE,sCAAsC;IACtE,kCAAkC,EAAE,0CAA0C;CACtE,CAAC;AAEX;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,sCAAsC,GAAG,KAAK,CAAC;AAC5D,MAAM,CAAC,MAAM,0CAA0C,GAAG,EAAE,CAAC;AAE7D,MAAM,QAAQ,GAAG,4BAA4B,CAAC;IAC5C,UAAU,EAAE,OAAO;IACnB,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAC1C,QAAQ,EAAE;QACR;YACE,IAAI,EAAE,mBAAmB,CAAC,8BAA8B;YACxD,IAAI,EAAE,sCAAsC;YAC5C,WAAW,EACT,8FAA8F;YAChG,SAAS,EAAE,OAAO;YAClB,SAAS,EAAE,QAAQ;YACnB,YAAY,EAAE,sCAAsC;SACrD;QACD;YACE,IAAI,EAAE,mBAAmB,CAAC,kCAAkC;YAC5D,IAAI,EAAE,oCAAoC;YAC1C,WAAW,EACT,kIAAkI;YACpI,SAAS,EAAE,OAAO;YAClB,SAAS,EAAE,QAAQ;YACnB,YAAY,EAAE,0CAA0C;SACzD;KACF;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,oBAAoB,CAAC;IAC3C,EAAE,EAAE,OAAO;IACX,IAAI,EAAE,OAAO;IACb,WAAW,EAAE,mEAAmE;IAChF,OAAO,EAAE,OAAO;IAChB,qEAAqE;IACrE,8EAA8E;IAC9E,wEAAwE;IACxE,2EAA2E;IAC3E,2EAA2E;IAC3E,4EAA4E;IAC5E,8EAA8E;IAC9E,8EAA8E;IAC9E,yCAAyC;IACzC,EAAE;IACF,uEAAuE;IACvE,0EAA0E;IAC1E,wEAAwE;IACxE,iEAAiE;IACjE,0EAA0E;IAC1E,eAAe;IACf,EAAE;IACF,0EAA0E;IAC1E,uEAAuE;IACvE,2EAA2E;IAC3E,wEAAwE;IACxE,4EAA4E;IAC5E,aAAa;IACb,YAAY,EAAE;QACZ,SAAS;QACT,mBAAmB;QACnB,QAAQ;QACR,eAAe;QACf,aAAa;QACb,gBAAgB;QAChB,gBAAgB;QAChB,UAAU;KACX;IACD,0EAA0E;IAC1E,6DAA6D;IAC7D,uEAAuE;IACvE,mBAAmB;IACnB,wBAAwB,EAAE;QACxB;YACE,QAAQ,EAAE,YAAY;YACtB,IAAI,EAAE,kBAAkB;YACxB,MAAM,EACJ,sFAAsF;gBACtF,mFAAmF;gBACnF,6DAA6D;gBAC7D,oFAAoF;gBACpF,iFAAiF;gBACjF,mFAAmF;gBACnF,sFAAsF;gBACtF,iEAAiE;SACpE;QACD;YACE,QAAQ,EAAE,YAAY;YACtB,IAAI,EAAE,mBAAmB;YACzB,MAAM,EACJ,uFAAuF;gBACvF,oFAAoF;gBACpF,iFAAiF;gBACjF,mFAAmF;gBACnF,wDAAwD;SAC3D;KACF;IACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;IACH,UAAU,EAAE;QACV;YACE,IAAI,EAAE,sBAAsB;YAC5B,MAAM,EAAE;gBACN,cAAc;gBACd,SAAS;gBACT,iBAAiB;gBACjB,eAAe;gBACf,sBAAsB;gBACtB,oBAAoB;gBACpB,uBAAuB;aACxB;SACF;QACD,EAAE,IAAI,EAAE,YAAY,EAAE;QACtB,EAAE,IAAI,EAAE,wBAAwB,EAAE;KACnC;IACD,QAAQ;IACR,4EAA4E;IAC5E,2EAA2E;IAC3E,8EAA8E;IAC9E,yEAAyE;IACzE,uEAAuE;IACvE,6EAA6E;IAC7E,wEAAwE;IACxE,uBAAuB;IACvB,EAAE;IACF,4EAA4E;IAC5E,8EAA8E;IAC9E,oCAAoC;IACpC,UAAU,EAAE;QACV,gBAAgB,EAAE,IAAI;QACtB,MAAM,EACJ,0FAA0F;YAC1F,gBAAgB;KACnB;IACD,yEAAyE;IACzE,2EAA2E;IAC3E,6EAA6E;IAC7E,4EAA4E;IAC5E,4EAA4E;IAC5E,qDAAqD;IACrD,EAAE;IACF,wEAAwE;IACxE,6EAA6E;IAC7E,kEAAkE;IAClE,6EAA6E;IAC7E,yEAAyE;IACzE,2EAA2E;IAC3E,0EAA0E;IAC1E,qEAAqE;IACrE,wDAAwD;IACxD,WAAW,EAAE;QACX,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,qBAAqB,EAAE;QACpD,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,6CAA6C,EAAE;QAC9E,EAAE,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAE,+BAA+B,EAAE;KACrE;IACD,IAAI,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE;IAC5B,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE;CACtB,CAAC,CAAC;AAEH,yDAAyD;AACzD,MAAM,CAAC,MAAM,aAAa,GAAG,QAAQ,CAAC;AAEtC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAmD;IACzE;QACE,IAAI,EAAE,mBAAmB;QACzB,OAAO,EAAE,uEAAuE;QAChF,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CACrB,CAAC,MAAM,MAAM,CAAC,oCAAoC,CAAC,CAAC,CAAC,gBAAgB,CAAC,OAAO,CAAC;KACjF;CACF,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Carts consolidation (feature 027).
4
+ *
5
+ * Brings the foundation `carts` table up to the spec defined in feature
6
+ * 027-carts: status lifecycle (active / abandoned / completed / rejected),
7
+ * sales-channel scoping, last-activity bookkeeping for the abandonment
8
+ * sweep, the orthogonal approval sub-state (not_required / pending /
9
+ * approved / rejected_by_org_admin), a coupon-code column wired to the
10
+ * Promotions module, a conversion-lineage column to the Quote Requests
11
+ * module, an abandonment-notification idempotency timestamp, and an
12
+ * optimistic-lock `version` column shared with feature 003 / 026.
13
+ *
14
+ * `cart_items` gains a two-layer pricing snapshot: the existing
15
+ * `unit_price` remains the price the line was added at, while
16
+ * `recomputed_unit_price` / `recomputed_at` / `recomputed_currency` cache
17
+ * the most recent re-resolved price (per feature 011's PricingService).
18
+ *
19
+ * `organizations` gains `requires_cart_approval` — the per-Organization
20
+ * policy flag that gates checkout behind an Organization Administrator's
21
+ * approval. Default false preserves the prior behaviour.
22
+ *
23
+ * Introduces one new table:
24
+ * - cart_audit_entries — typed per-cart audit feed for the cart-detail
25
+ * UI (the platform-wide audit timeline still uses audit_log_entries).
26
+ *
27
+ * Data migration: legacy `status='converted'` rows are remapped to
28
+ * `status='completed'`. The legacy unique partial index
29
+ * `carts_one_active_per_customer` (introduced by feature 001) is dropped
30
+ * and replaced with `carts_one_active_per_customer_per_channel` so the
31
+ * "one active cart per (Customer, Sales Channel)" rule from the spec is
32
+ * enforced at the database layer.
33
+ *
34
+ * Settings (`carts.abandonment.inactivity_minutes`,
35
+ * `carts.abandonment.notification_recipient`) are seeded by the module
36
+ * lifecycle reconciler from the `settings` declaration in
37
+ * `carts/manifest.ts` — not from this migration.
38
+ */
39
+ export declare class Migration20260611T140352CartsConsolidation extends Migration {
40
+ up(): Promise<void>;
41
+ down(): Promise<void>;
42
+ }
43
+ //# sourceMappingURL=20260611T140352_carts_consolidation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260611T140352_carts_consolidation.d.ts","sourceRoot":"","sources":["../../src/migrations/20260611T140352_carts_consolidation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,qBAAa,0CAA2C,SAAQ,SAAS;IACxD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IA2JnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CA0DrC"}
@@ -0,0 +1,237 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Carts consolidation (feature 027).
4
+ *
5
+ * Brings the foundation `carts` table up to the spec defined in feature
6
+ * 027-carts: status lifecycle (active / abandoned / completed / rejected),
7
+ * sales-channel scoping, last-activity bookkeeping for the abandonment
8
+ * sweep, the orthogonal approval sub-state (not_required / pending /
9
+ * approved / rejected_by_org_admin), a coupon-code column wired to the
10
+ * Promotions module, a conversion-lineage column to the Quote Requests
11
+ * module, an abandonment-notification idempotency timestamp, and an
12
+ * optimistic-lock `version` column shared with feature 003 / 026.
13
+ *
14
+ * `cart_items` gains a two-layer pricing snapshot: the existing
15
+ * `unit_price` remains the price the line was added at, while
16
+ * `recomputed_unit_price` / `recomputed_at` / `recomputed_currency` cache
17
+ * the most recent re-resolved price (per feature 011's PricingService).
18
+ *
19
+ * `organizations` gains `requires_cart_approval` — the per-Organization
20
+ * policy flag that gates checkout behind an Organization Administrator's
21
+ * approval. Default false preserves the prior behaviour.
22
+ *
23
+ * Introduces one new table:
24
+ * - cart_audit_entries — typed per-cart audit feed for the cart-detail
25
+ * UI (the platform-wide audit timeline still uses audit_log_entries).
26
+ *
27
+ * Data migration: legacy `status='converted'` rows are remapped to
28
+ * `status='completed'`. The legacy unique partial index
29
+ * `carts_one_active_per_customer` (introduced by feature 001) is dropped
30
+ * and replaced with `carts_one_active_per_customer_per_channel` so the
31
+ * "one active cart per (Customer, Sales Channel)" rule from the spec is
32
+ * enforced at the database layer.
33
+ *
34
+ * Settings (`carts.abandonment.inactivity_minutes`,
35
+ * `carts.abandonment.notification_recipient`) are seeded by the module
36
+ * lifecycle reconciler from the `settings` declaration in
37
+ * `carts/manifest.ts` — not from this migration.
38
+ */
39
+ export class Migration20260611T140352CartsConsolidation extends Migration {
40
+ async up() {
41
+ // 1) Data backfill: rename legacy 'converted' → 'completed' before the
42
+ // new CHECK constraint is installed. Foundation code that wrote
43
+ // 'converted' (CartService.clearForCustomer) is updated to write
44
+ // 'completed' in the same PR.
45
+ this.addSql(`update "carts" set "status" = 'completed' where "status" = 'converted';`);
46
+ // 2) New columns on `carts`.
47
+ this.addSql(`
48
+ alter table "carts"
49
+ add column "sales_channel_id" uuid null,
50
+ add column "last_activity_at" timestamptz not null default now(),
51
+ add column "approval_status" varchar(32) not null default 'not_required',
52
+ add column "submitted_for_approval_at" timestamptz null,
53
+ add column "approved_at" timestamptz null,
54
+ add column "approved_by_customer_account_id" uuid null,
55
+ add column "rejected_at" timestamptz null,
56
+ add column "rejected_by_actor" varchar(48) null,
57
+ add column "rejected_reason" text null,
58
+ add column "applied_promotion_code" varchar(64) null,
59
+ add column "converted_to_quote_request_id" uuid null,
60
+ add column "abandonment_notified_at" timestamptz null,
61
+ add column "version" int not null default 0;
62
+ `);
63
+ // 3) CHECK constraints for the two enum-like columns.
64
+ this.addSql(`
65
+ alter table "carts"
66
+ add constraint "carts_status_check"
67
+ check ("status" in ('active', 'abandoned', 'completed', 'rejected'));
68
+ `);
69
+ this.addSql(`
70
+ alter table "carts"
71
+ add constraint "carts_approval_status_check"
72
+ check ("approval_status" in (
73
+ 'not_required', 'pending', 'approved', 'rejected_by_org_admin'
74
+ ));
75
+ `);
76
+ // 4) Foreign keys on the new columns.
77
+ // sales_channel_id → sales_channels(id). ON DELETE SET NULL keeps the
78
+ // cart record alive if a channel is deleted (rare; admin operation).
79
+ this.addSql(`
80
+ alter table "carts"
81
+ add constraint "carts_sales_channel_fk"
82
+ foreign key ("sales_channel_id")
83
+ references "sales_channels" ("id")
84
+ on delete set null;
85
+ `);
86
+ // approved_by_customer_account_id → customer_accounts(id). ON DELETE
87
+ // SET NULL because the approval record still matters even if the
88
+ // Org-Admin's Customer account is later deleted.
89
+ this.addSql(`
90
+ alter table "carts"
91
+ add constraint "carts_approved_by_customer_fk"
92
+ foreign key ("approved_by_customer_account_id")
93
+ references "customer_accounts" ("id")
94
+ on delete set null;
95
+ `);
96
+ // converted_to_quote_request_id → quote_requests(id). ON DELETE
97
+ // SET NULL because deleting a QR (rare) does not invalidate the
98
+ // fact that the cart was once converted.
99
+ this.addSql(`
100
+ alter table "carts"
101
+ add constraint "carts_converted_to_qr_fk"
102
+ foreign key ("converted_to_quote_request_id")
103
+ references "quote_requests" ("id")
104
+ on delete set null;
105
+ `);
106
+ // 5) Backfill `sales_channel_id` to the system-default sales channel
107
+ // so existing rows participate in the new (customer, channel)
108
+ // uniqueness rule. If no default exists (test fixture), leave
109
+ // NULL — the read path treats NULL as "system-default channel"
110
+ // until the next consolidation tightens it (see research.md §R3).
111
+ this.addSql(`
112
+ update "carts"
113
+ set "sales_channel_id" = (
114
+ select "id" from "sales_channels" where "system_default" = true limit 1
115
+ )
116
+ where "sales_channel_id" is null;
117
+ `);
118
+ // 6) Indexes.
119
+ this.addSql(`
120
+ create index "carts_sales_channel_customer_status_idx"
121
+ on "carts" ("sales_channel_id", "customer_account_id", "status");
122
+ `);
123
+ this.addSql(`
124
+ create index "carts_status_last_activity_idx"
125
+ on "carts" ("status", "last_activity_at");
126
+ `);
127
+ this.addSql(`
128
+ create index "carts_applied_promotion_code_idx"
129
+ on "carts" ("applied_promotion_code")
130
+ where "applied_promotion_code" is not null;
131
+ `);
132
+ this.addSql(`
133
+ create index "carts_org_status_updated_idx"
134
+ on "carts" ("organization_id", "status", "updated_at" desc);
135
+ `);
136
+ // 7) Replace the foundation's "one active cart per Customer" partial
137
+ // unique index with a (Customer, Sales Channel) variant so the spec's
138
+ // one-active-cart-per-(Customer, Sales Channel) rule is enforced at
139
+ // the DB layer. Anonymous carts (customer_account_id is null) are
140
+ // not constrained by this index — anon ownership is enforced by the
141
+ // existing `anonymous_cart_token` unique index.
142
+ this.addSql(`drop index if exists "carts_one_active_per_customer";`);
143
+ this.addSql(`
144
+ create unique index "carts_one_active_per_customer_per_channel"
145
+ on "carts" ("customer_account_id", "sales_channel_id")
146
+ where "status" = 'active' and "customer_account_id" is not null;
147
+ `);
148
+ // 8) New columns on `cart_items` — two-layer pricing snapshot.
149
+ this.addSql(`
150
+ alter table "cart_items"
151
+ add column "recomputed_unit_price" numeric(12,2) null,
152
+ add column "recomputed_at" timestamptz null,
153
+ add column "recomputed_currency" varchar(3) null;
154
+ `);
155
+ // 9) New column on `organizations` — per-org cart-approval policy.
156
+ this.addSql(`
157
+ alter table "organizations"
158
+ add column "requires_cart_approval" boolean not null default false;
159
+ `);
160
+ // 10) Cart audit feed.
161
+ this.addSql(`
162
+ create table "cart_audit_entries" (
163
+ "id" uuid not null,
164
+ "cart_id" uuid not null,
165
+ "occurred_at" timestamptz not null default now(),
166
+ "actor_type" varchar(32) not null,
167
+ "actor_id" uuid null,
168
+ "action" varchar(64) not null,
169
+ "from_state" varchar(32) null,
170
+ "to_state" varchar(32) null,
171
+ "reason" text null,
172
+ "metadata" jsonb not null default '{}'::jsonb,
173
+ constraint "cart_audit_entries_pkey" primary key ("id"),
174
+ constraint "cart_audit_entries_cart_fk"
175
+ foreign key ("cart_id") references "carts" ("id") on delete cascade,
176
+ constraint "cart_audit_entries_actor_type_check"
177
+ check ("actor_type" in ('customer', 'org_admin', 'platform_admin', 'system', 'sweep'))
178
+ );
179
+ `);
180
+ this.addSql(`
181
+ create index "cart_audit_entries_cart_occurred_idx"
182
+ on "cart_audit_entries" ("cart_id", "occurred_at" desc);
183
+ `);
184
+ }
185
+ async down() {
186
+ // Mirror order: drop the cart audit feed first, then the organization
187
+ // column, then cart_items columns, then re-instate the foundation
188
+ // partial unique index, then drop the new carts columns / constraints
189
+ // / indexes.
190
+ this.addSql(`drop index if exists "cart_audit_entries_cart_occurred_idx";`);
191
+ this.addSql(`drop table if exists "cart_audit_entries" cascade;`);
192
+ this.addSql(`alter table "organizations" drop column if exists "requires_cart_approval";`);
193
+ this.addSql(`
194
+ alter table "cart_items"
195
+ drop column if exists "recomputed_unit_price",
196
+ drop column if exists "recomputed_at",
197
+ drop column if exists "recomputed_currency";
198
+ `);
199
+ // Restore foundation's partial unique index.
200
+ this.addSql(`drop index if exists "carts_one_active_per_customer_per_channel";`);
201
+ this.addSql(`
202
+ create unique index "carts_one_active_per_customer"
203
+ on "carts" ("customer_account_id")
204
+ where "status" = 'active' and "customer_account_id" is not null;
205
+ `);
206
+ this.addSql(`drop index if exists "carts_org_status_updated_idx";`);
207
+ this.addSql(`drop index if exists "carts_applied_promotion_code_idx";`);
208
+ this.addSql(`drop index if exists "carts_status_last_activity_idx";`);
209
+ this.addSql(`drop index if exists "carts_sales_channel_customer_status_idx";`);
210
+ this.addSql(`alter table "carts" drop constraint if exists "carts_converted_to_qr_fk";`);
211
+ this.addSql(`alter table "carts" drop constraint if exists "carts_approved_by_customer_fk";`);
212
+ this.addSql(`alter table "carts" drop constraint if exists "carts_sales_channel_fk";`);
213
+ this.addSql(`alter table "carts" drop constraint if exists "carts_approval_status_check";`);
214
+ this.addSql(`alter table "carts" drop constraint if exists "carts_status_check";`);
215
+ this.addSql(`
216
+ alter table "carts"
217
+ drop column if exists "sales_channel_id",
218
+ drop column if exists "last_activity_at",
219
+ drop column if exists "approval_status",
220
+ drop column if exists "submitted_for_approval_at",
221
+ drop column if exists "approved_at",
222
+ drop column if exists "approved_by_customer_account_id",
223
+ drop column if exists "rejected_at",
224
+ drop column if exists "rejected_by_actor",
225
+ drop column if exists "rejected_reason",
226
+ drop column if exists "applied_promotion_code",
227
+ drop column if exists "converted_to_quote_request_id",
228
+ drop column if exists "abandonment_notified_at",
229
+ drop column if exists "version";
230
+ `);
231
+ // Restore legacy 'converted' state for any rows the up() touched.
232
+ // (We cannot perfectly distinguish "converted at the time of migration"
233
+ // from "completed later", so the down() leaves them as 'completed';
234
+ // this is documented as accepted asymmetry.)
235
+ }
236
+ }
237
+ //# sourceMappingURL=20260611T140352_carts_consolidation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260611T140352_carts_consolidation.js","sourceRoot":"","sources":["../../src/migrations/20260611T140352_carts_consolidation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Feature 043 — packaging-unit snapshot on cart lines. When a buyer orders a
4
+ * packaging unit (e.g. a pallet), the line records which unit it came from so
5
+ * the cart/order/RFQ can display the unit name appended to the product name.
6
+ * All nullable — plain single-piece lines leave them empty.
7
+ */
8
+ export declare class Migration20260611T140413CartsCartItemPackaging extends Migration {
9
+ up(): Promise<void>;
10
+ down(): Promise<void>;
11
+ }
12
+ //# sourceMappingURL=20260611T140413_carts_cart_item_packaging.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260611T140413_carts_cart_item_packaging.d.ts","sourceRoot":"","sources":["../../src/migrations/20260611T140413_carts_cart_item_packaging.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;GAKG;AACH,qBAAa,8CAA+C,SAAQ,SAAS;IAC5D,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAMnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAKrC"}
@@ -0,0 +1,20 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Feature 043 — packaging-unit snapshot on cart lines. When a buyer orders a
4
+ * packaging unit (e.g. a pallet), the line records which unit it came from so
5
+ * the cart/order/RFQ can display the unit name appended to the product name.
6
+ * All nullable — plain single-piece lines leave them empty.
7
+ */
8
+ export class Migration20260611T140413CartsCartItemPackaging extends Migration {
9
+ async up() {
10
+ this.addSql('alter table "cart_items" add column "packaging_unit_id" uuid null;');
11
+ this.addSql('alter table "cart_items" add column "packaging_unit_name" varchar(160) null;');
12
+ this.addSql('alter table "cart_items" add column "packaging_unit_base_quantity" integer null;');
13
+ }
14
+ async down() {
15
+ this.addSql('alter table "cart_items" drop column "packaging_unit_base_quantity";');
16
+ this.addSql('alter table "cart_items" drop column "packaging_unit_name";');
17
+ this.addSql('alter table "cart_items" drop column "packaging_unit_id";');
18
+ }
19
+ }
20
+ //# sourceMappingURL=20260611T140413_carts_cart_item_packaging.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260611T140413_carts_cart_item_packaging.js","sourceRoot":"","sources":["../../src/migrations/20260611T140413_carts_cart_item_packaging.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;GAKG;AACH,MAAM,OAAO,8CAA+C,SAAQ,SAAS;IAClE,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC,oEAAoE,CAAC,CAAC;QAClF,IAAI,CAAC,MAAM,CAAC,8EAA8E,CAAC,CAAC;QAC5F,IAAI,CAAC,MAAM,CAAC,kFAAkF,CAAC,CAAC;IAClG,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,sEAAsE,CAAC,CAAC;QACpF,IAAI,CAAC,MAAM,CAAC,6DAA6D,CAAC,CAAC;QAC3E,IAAI,CAAC,MAAM,CAAC,2DAA2D,CAAC,CAAC;IAC3E,CAAC;CACF"}
@@ -0,0 +1,50 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * `carts.completed_order_id` — the pointer this table has never had for one of
4
+ * its two terminal transitions (D-94.1, site 2 / D-94.4).
5
+ *
6
+ * `placeOrder` ends by deleting the cart's items and setting
7
+ * `status = 'completed'`, on the same `Cart` object the totals were read from
8
+ * and inside the placement transaction, because a placement that then fails
9
+ * must leave the basket exactly as the buyer left it
10
+ * (`test/integration/orders/place-order-failure-preserves-cart.test.ts`).
11
+ * D-78 point 2 rules such a seam permanent and *declared*, and there was
12
+ * nothing to declare it with: no foreign key existed in either direction.
13
+ *
14
+ * **The pointer goes on the cart, not on the order.** An `orders.cart_id`
15
+ * would satisfy the letter of D-78 point 2 and force nothing — `orders`
16
+ * already declares `carts`, and an implementer could still complete the cart
17
+ * in a separate transaction and satisfy the constraint, because the cart is
18
+ * already committed when the order row is written. The cart-side pointer
19
+ * cannot be written before the order exists, which is precisely the property
20
+ * `payments_order_fk`, `invoices_order_fk` and `shipments_order_fk` rely on.
21
+ * This table already carries exactly this shape for its *other* terminal
22
+ * transition — `carts_converted_to_qr_fk`, added by
23
+ * `20260611T140352_carts_consolidation.ts:100-109`.
24
+ *
25
+ * `null`, and `on delete set null`, for the reason that migration wrote down
26
+ * for its twin: deleting the target does not invalidate the fact that the cart
27
+ * was once completed. The completion pointer is provenance, not a dependency.
28
+ * The column is nullable because it cannot be backfilled — no surviving record
29
+ * says which order emptied which historical cart — and because three of the
30
+ * four paths to `completed` are not placements (`CartService.clearForCustomer`,
31
+ * the anonymous-merge source, and the quote-request conversion), so
32
+ * `status = 'completed'` will never imply it and no check constraint can be
33
+ * added.
34
+ *
35
+ * **No orphan report, and the asymmetry is the point.** The other three
36
+ * constraints of this family need one because their columns have been
37
+ * accepting unverified values for months. This column is new: every existing
38
+ * row starts `null`, so there is nothing to orphan.
39
+ *
40
+ * `carts` declares `orders` in its manifest `dependencies` for this edge
41
+ * (AGENTS.md § Migrations item 4). The reverse port edge — `orders` resolving
42
+ * `cartWritePort` — moves to `acknowledgedDependencies`, which drops the
43
+ * install ordering the constraint says is backwards and keeps the bind and
44
+ * every refusal exactly as they were (D-94.3).
45
+ */
46
+ export declare class Migration20260818T081253CartsCartCompletedOrderFk extends Migration {
47
+ up(): Promise<void>;
48
+ down(): Promise<void>;
49
+ }
50
+ //# sourceMappingURL=20260818T081253_carts_cart_completed_order_fk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260818T081253_carts_cart_completed_order_fk.d.ts","sourceRoot":"","sources":["../../src/migrations/20260818T081253_carts_cart_completed_order_fk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,qBAAa,iDAAkD,SAAQ,SAAS;IAC/D,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAYnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAKrC"}