@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,242 @@
1
+ import { ERROR_CODES } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ import { Cart } from '../entities/cart.entity.js';
4
+ import { CartItem } from '../entities/cart-item.entity.js';
5
+ export class CartApprovalService {
6
+ emFactory;
7
+ cartAuditService;
8
+ organizations;
9
+ organizationCartApproval;
10
+ customerAccounts;
11
+ emailDispatch;
12
+ constructor(emFactory, cartAuditService,
13
+ /** `organizations`' read model — the approval policy and its owner row. */
14
+ organizations,
15
+ /**
16
+ * `organizations`' write surface for the policy column (issue #175). The
17
+ * two `setPolicy*` methods below used to assign
18
+ * `organizations.requires_cart_approval` on that module's entity; the write
19
+ * belongs to its owner, which audits it through the Command Bus. What stays
20
+ * here is the cascade over this module's own carts.
21
+ */
22
+ organizationCartApproval,
23
+ /** `customer_accounts`' read model — the submitting buyer's role. */
24
+ customerAccounts, emailDispatch) {
25
+ this.emFactory = emFactory;
26
+ this.cartAuditService = cartAuditService;
27
+ this.organizations = organizations;
28
+ this.organizationCartApproval = organizationCartApproval;
29
+ this.customerAccounts = customerAccounts;
30
+ this.emailDispatch = emailDispatch;
31
+ }
32
+ /**
33
+ * Fire-and-forget e-mail dispatch. Wraps each call so any throw is
34
+ * swallowed — approval transitions must complete even if mail
35
+ * delivery hiccups. Production composition is responsible for
36
+ * surfacing the failure through the platform's log + alerting paths.
37
+ */
38
+ async dispatch(fn) {
39
+ if (!this.emailDispatch)
40
+ return;
41
+ try {
42
+ await fn();
43
+ }
44
+ catch {
45
+ // Swallow — see contract on CartEmailDispatch.
46
+ }
47
+ }
48
+ /**
49
+ * Buyer submits the cart for Org-Admin approval. Refuses if:
50
+ * - the cart is empty
51
+ * - the policy is off (approval not required)
52
+ * - the buyer is themself an Org Admin (self-approval exemption)
53
+ * - approval_status is already non-`not_required`
54
+ */
55
+ async submitForApproval(cart, actor) {
56
+ const em = this.emFactory();
57
+ if (!cart.organizationId || cart.organizationId !== actor.organizationId) {
58
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'cart_not_found');
59
+ }
60
+ const items = await em.find(CartItem, { cartId: cart.id });
61
+ if (items.length === 0) {
62
+ throw new HttpError(422, ERROR_CODES.CART_EMPTY, 'cart_empty');
63
+ }
64
+ if (cart.approvalStatus !== 'not_required') {
65
+ throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'approval_already_initiated');
66
+ }
67
+ const org = await this.organizations.findById(actor.organizationId);
68
+ if (!org || !org.requiresCartApproval) {
69
+ throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'approval_not_required');
70
+ }
71
+ const buyer = await this.customerAccounts.findById(actor.customerAccountId);
72
+ if (buyer?.role === 'organization_admin') {
73
+ throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'org_admin_self_submit');
74
+ }
75
+ cart.approvalStatus = 'pending';
76
+ cart.submittedForApprovalAt = new Date();
77
+ cart.lastActivityAt = new Date();
78
+ await em.flush();
79
+ await this.cartAuditService.record({
80
+ cartId: cart.id,
81
+ actorType: 'customer',
82
+ actorId: actor.customerAccountId,
83
+ action: 'approval_submitted',
84
+ fromState: 'not_required',
85
+ toState: 'pending',
86
+ });
87
+ await this.dispatch(() => this.emailDispatch.onSubmittedForApproval({ cart, submitter: actor }));
88
+ return cart;
89
+ }
90
+ /** Org Admin approves a pending cart. */
91
+ async approve(cartId, actor) {
92
+ const em = this.emFactory();
93
+ const cart = await em.findOne(Cart, { id: cartId });
94
+ if (!cart || cart.organizationId !== actor.organizationId) {
95
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'cart_not_found');
96
+ }
97
+ if (cart.approvalStatus !== 'pending') {
98
+ throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'approval_not_pending');
99
+ }
100
+ cart.approvalStatus = 'approved';
101
+ cart.approvedAt = new Date();
102
+ cart.approvedByCustomerAccountId = actor.customerAccountId;
103
+ await em.flush();
104
+ await this.cartAuditService.record({
105
+ cartId: cart.id,
106
+ actorType: 'org_admin',
107
+ actorId: actor.customerAccountId,
108
+ action: 'approval_approved',
109
+ fromState: 'pending',
110
+ toState: 'approved',
111
+ });
112
+ await this.dispatch(() => this.emailDispatch.onApproved({ cart, approver: actor }));
113
+ return cart;
114
+ }
115
+ /** Org Admin rejects a pending cart with a reason. */
116
+ async reject(cartId, actor, reason) {
117
+ const em = this.emFactory();
118
+ const cart = await em.findOne(Cart, { id: cartId });
119
+ if (!cart || cart.organizationId !== actor.organizationId) {
120
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'cart_not_found');
121
+ }
122
+ if (cart.approvalStatus !== 'pending') {
123
+ throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'approval_not_pending');
124
+ }
125
+ cart.approvalStatus = 'rejected_by_org_admin';
126
+ cart.status = 'rejected';
127
+ cart.rejectedAt = new Date();
128
+ cart.rejectedByActor = `org_admin:${actor.customerAccountId}`;
129
+ cart.rejectedReason = reason;
130
+ await em.flush();
131
+ await this.cartAuditService.record({
132
+ cartId: cart.id,
133
+ actorType: 'org_admin',
134
+ actorId: actor.customerAccountId,
135
+ action: 'approval_rejected',
136
+ fromState: 'pending',
137
+ toState: 'rejected_by_org_admin',
138
+ reason,
139
+ });
140
+ await this.dispatch(() => this.emailDispatch.onRejected({ cart, rejector: actor, reason }));
141
+ return cart;
142
+ }
143
+ /**
144
+ * The current policy, for a platform admin (feature 091, P7b;
145
+ * `contracts/admin-component-contribution.md` §10.5).
146
+ *
147
+ * The read half of {@link setPolicyByAdmin}, added because the admin
148
+ * contribution that renders the toggle has to know what to render it in.
149
+ * Until P7b that value crossed as a prop — `initialRequiresCartApproval`, out
150
+ * of `organizations`' detail payload — and a zone's props may not carry it:
151
+ * the other three contributors to `organization.detail.after` want neither
152
+ * the prop nor the request behind it.
153
+ *
154
+ * It reads through `organizations`' own port rather than this module's
155
+ * tables, which is the direction the write has taken since issue #175: the
156
+ * column belongs to that module.
157
+ */
158
+ async getPolicyByAdmin(organizationId) {
159
+ const organization = await this.organizations.findById(organizationId);
160
+ if (!organization) {
161
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'organization_not_found');
162
+ }
163
+ return organization;
164
+ }
165
+ /**
166
+ * Platform-admin variant of {@link setPolicyForOrganization}. Same
167
+ * semantics (policy-off cascades a reset of every pending/approved
168
+ * cart in the Org) but the actor is a platform admin rather than an
169
+ * Org Admin — recorded as `actorType: 'platform_admin'` in the
170
+ * audit row.
171
+ */
172
+ async setPolicyByAdmin(organizationId, adminUserId, requires) {
173
+ const { organization, changed } = await this.organizationCartApproval.setCartApprovalPolicy(organizationId, requires);
174
+ if (changed && !requires) {
175
+ await this.resetPendingApprovals(organizationId, 'platform_admin', adminUserId);
176
+ }
177
+ return organization;
178
+ }
179
+ /**
180
+ * Org Admin toggles the per-Organization `requires_cart_approval` policy.
181
+ * On `false`: every `pending` or `approved` cart in the Org returns to
182
+ * `not_required` and is audited as `approval_policy_reset`.
183
+ */
184
+ async setPolicyForOrganization(organizationId, actor, requires) {
185
+ if (organizationId !== actor.organizationId) {
186
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'organization_not_found');
187
+ }
188
+ const { organization, changed } = await this.organizationCartApproval.setCartApprovalPolicy(organizationId, requires);
189
+ if (changed && !requires) {
190
+ await this.resetPendingApprovals(organizationId, 'org_admin', actor.customerAccountId);
191
+ }
192
+ return organization;
193
+ }
194
+ /**
195
+ * The half of a policy switch-off that belongs to this module: every
196
+ * `pending` or `approved` cart in the Organization returns to
197
+ * `not_required`, audited as `approval_policy_reset` in this module's own
198
+ * per-cart trail with the actor that switched the policy off.
199
+ */
200
+ async resetPendingApprovals(organizationId, actorType, actorId) {
201
+ const em = this.emFactory();
202
+ const affected = await em.find(Cart, {
203
+ organizationId,
204
+ approvalStatus: { $in: ['pending', 'approved'] },
205
+ });
206
+ for (const cart of affected) {
207
+ const prev = cart.approvalStatus;
208
+ cart.approvalStatus = 'not_required';
209
+ await em.flush();
210
+ await this.cartAuditService.record({
211
+ cartId: cart.id,
212
+ actorType,
213
+ actorId,
214
+ action: 'approval_policy_reset',
215
+ fromState: prev,
216
+ toState: 'not_required',
217
+ });
218
+ }
219
+ }
220
+ /**
221
+ * Re-arm hook: any buyer-driven mutation on an `approved` cart drops
222
+ * it back to `pending`. Called by CartService.{addItem,updateItem,
223
+ * removeItem} and by CartCouponService.{apply,clear}.
224
+ */
225
+ async maybeReArm(cart, actor) {
226
+ if (cart.approvalStatus !== 'approved')
227
+ return;
228
+ const em = this.emFactory();
229
+ cart.approvalStatus = 'pending';
230
+ cart.submittedForApprovalAt = new Date();
231
+ await em.flush();
232
+ await this.cartAuditService.record({
233
+ cartId: cart.id,
234
+ actorType: 'customer',
235
+ ...(actor.customerAccountId ? { actorId: actor.customerAccountId } : {}),
236
+ action: 'approval_resubmission_required',
237
+ fromState: 'approved',
238
+ toState: 'pending',
239
+ });
240
+ }
241
+ }
242
+ //# sourceMappingURL=cart-approval-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-approval-service.js","sourceRoot":"","sources":["../../../src/backend/services/cart-approval-service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAElD,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AA4D3D,MAAM,OAAO,mBAAmB;IAEX;IACA;IAEA;IAQA;IAEA;IACA;IAfnB,YACmB,SAA8B,EAC9B,gBAAkC;IACnD,2EAA2E;IAC1D,aAAsC;IACvD;;;;;;OAMG;IACc,wBAA2D;IAC5E,qEAAqE;IACpD,gBAAyC,EACzC,aAAiC;QAdjC,cAAS,GAAT,SAAS,CAAqB;QAC9B,qBAAgB,GAAhB,gBAAgB,CAAkB;QAElC,kBAAa,GAAb,aAAa,CAAyB;QAQtC,6BAAwB,GAAxB,wBAAwB,CAAmC;QAE3D,qBAAgB,GAAhB,gBAAgB,CAAyB;QACzC,kBAAa,GAAb,aAAa,CAAoB;IACjD,CAAC;IAEJ;;;;;OAKG;IACK,KAAK,CAAC,QAAQ,CAAI,EAAoB;QAC5C,IAAI,CAAC,IAAI,CAAC,aAAa;YAAE,OAAO;QAChC,IAAI,CAAC;YACH,MAAM,EAAE,EAAE,CAAC;QACb,CAAC;QAAC,MAAM,CAAC;YACP,+CAA+C;QACjD,CAAC;IACH,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,iBAAiB,CAAC,IAAU,EAAE,KAAmB;QACrD,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,cAAc,KAAK,KAAK,CAAC,cAAc,EAAE,CAAC;YACzE,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC;QACpE,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;QACjE,CAAC;QACD,IAAI,IAAI,CAAC,cAAc,KAAK,cAAc,EAAE,CAAC;YAC3C,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,iBAAiB,EAAE,4BAA4B,CAAC,CAAC;QACxF,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;QACpE,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,oBAAoB,EAAE,CAAC;YACtC,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,iBAAiB,EAAE,uBAAuB,CAAC,CAAC;QACnF,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;QAC5E,IAAI,KAAK,EAAE,IAAI,KAAK,oBAAoB,EAAE,CAAC;YACzC,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,iBAAiB,EAAE,uBAAuB,CAAC,CAAC;QACnF,CAAC;QAED,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC;QAChC,IAAI,CAAC,sBAAsB,GAAG,IAAI,IAAI,EAAE,CAAC;QACzC,IAAI,CAAC,cAAc,GAAG,IAAI,IAAI,EAAE,CAAC;QACjC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QAEjB,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC;YACjC,MAAM,EAAE,IAAI,CAAC,EAAE;YACf,SAAS,EAAE,UAAU;YACrB,OAAO,EAAE,KAAK,CAAC,iBAAiB;YAChC,MAAM,EAAE,oBAAoB;YAC5B,SAAS,EAAE,cAAc;YACzB,OAAO,EAAE,SAAS;SACnB,CAAC,CAAC;QACH,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CACvB,IAAI,CAAC,aAAc,CAAC,sBAAsB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CACvE,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,yCAAyC;IACzC,KAAK,CAAC,OAAO,CAAC,MAAc,EAAE,KAAmB;QAC/C,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QACpD,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,cAAc,KAAK,KAAK,CAAC,cAAc,EAAE,CAAC;YAC1D,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC;QACpE,CAAC;QACD,IAAI,IAAI,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;YACtC,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,iBAAiB,EAAE,sBAAsB,CAAC,CAAC;QAClF,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,UAAU,CAAC;QACjC,IAAI,CAAC,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC;QAC7B,IAAI,CAAC,2BAA2B,GAAG,KAAK,CAAC,iBAAiB,CAAC;QAC3D,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QAEjB,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC;YACjC,MAAM,EAAE,IAAI,CAAC,EAAE;YACf,SAAS,EAAE,WAAW;YACtB,OAAO,EAAE,KAAK,CAAC,iBAAiB;YAChC,MAAM,EAAE,mBAAmB;YAC3B,SAAS,EAAE,SAAS;YACpB,OAAO,EAAE,UAAU;SACpB,CAAC,CAAC;QACH,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CACvB,IAAI,CAAC,aAAc,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAC1D,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,sDAAsD;IACtD,KAAK,CAAC,MAAM,CAAC,MAAc,EAAE,KAAmB,EAAE,MAAc;QAC9D,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QACpD,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,cAAc,KAAK,KAAK,CAAC,cAAc,EAAE,CAAC;YAC1D,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC;QACpE,CAAC;QACD,IAAI,IAAI,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;YACtC,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,iBAAiB,EAAE,sBAAsB,CAAC,CAAC;QAClF,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,uBAAuB,CAAC;QAC9C,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC;QACzB,IAAI,CAAC,UAAU,GAAG,IAAI,IAAI,EAAE,CAAC;QAC7B,IAAI,CAAC,eAAe,GAAG,aAAa,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC9D,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC;QAC7B,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QAEjB,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC;YACjC,MAAM,EAAE,IAAI,CAAC,EAAE;YACf,SAAS,EAAE,WAAW;YACtB,OAAO,EAAE,KAAK,CAAC,iBAAiB;YAChC,MAAM,EAAE,mBAAmB;YAC3B,SAAS,EAAE,SAAS;YACpB,OAAO,EAAE,uBAAuB;YAChC,MAAM;SACP,CAAC,CAAC;QACH,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CACvB,IAAI,CAAC,aAAc,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAClE,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,gBAAgB,CAAC,cAAsB;QAC3C,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;QACvE,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,SAAS,EAAE,wBAAwB,CAAC,CAAC;QAC5E,CAAC;QACD,OAAO,YAAY,CAAC;IACtB,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,gBAAgB,CACpB,cAAsB,EACtB,WAAmB,EACnB,QAAiB;QAEjB,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,GAC7B,MAAM,IAAI,CAAC,wBAAwB,CAAC,qBAAqB,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;QACtF,IAAI,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC;YACzB,MAAM,IAAI,CAAC,qBAAqB,CAAC,cAAc,EAAE,gBAAgB,EAAE,WAAW,CAAC,CAAC;QAClF,CAAC;QACD,OAAO,YAAY,CAAC;IACtB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,wBAAwB,CAC5B,cAAsB,EACtB,KAAmB,EACnB,QAAiB;QAEjB,IAAI,cAAc,KAAK,KAAK,CAAC,cAAc,EAAE,CAAC;YAC5C,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,SAAS,EAAE,wBAAwB,CAAC,CAAC;QAC5E,CAAC;QACD,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,GAC7B,MAAM,IAAI,CAAC,wBAAwB,CAAC,qBAAqB,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;QACtF,IAAI,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC;YACzB,MAAM,IAAI,CAAC,qBAAqB,CAAC,cAAc,EAAE,WAAW,EAAE,KAAK,CAAC,iBAAiB,CAAC,CAAC;QACzF,CAAC;QACD,OAAO,YAAY,CAAC;IACtB,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,qBAAqB,CACjC,cAAsB,EACtB,SAAyC,EACzC,OAAe;QAEf,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE;YACnC,cAAc;YACd,cAAc,EAAE,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,UAAU,CAAgC,EAAE;SAChF,CAAC,CAAC;QACH,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC;YACjC,IAAI,CAAC,cAAc,GAAG,cAAc,CAAC;YACrC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;YACjB,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC;gBACjC,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,SAAS;gBACT,OAAO;gBACP,MAAM,EAAE,uBAAuB;gBAC/B,SAAS,EAAE,IAAI;gBACf,OAAO,EAAE,cAAc;aACxB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,UAAU,CAAC,IAAU,EAAE,KAAqC;QAChE,IAAI,IAAI,CAAC,cAAc,KAAK,UAAU;YAAE,OAAO;QAC/C,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC;QAChC,IAAI,CAAC,sBAAsB,GAAG,IAAI,IAAI,EAAE,CAAC;QACzC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QACjB,MAAM,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC;YACjC,MAAM,EAAE,IAAI,CAAC,EAAE;YACf,SAAS,EAAE,UAAU;YACrB,GAAG,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxE,MAAM,EAAE,gCAAgC;YACxC,SAAS,EAAE,UAAU;YACrB,OAAO,EAAE,SAAS;SACnB,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,75 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { CartAuditEntry } from '../entities/cart-audit-entry.entity.js';
3
+ import type { CartAuditAction, CartAuditActorType } from '../entities/cart-audit-entry.entity.js';
4
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
5
+ /**
6
+ * Single writer for cart audit landings (feature 027 §R10). Every call
7
+ * lands TWO rows in the same transaction:
8
+ *
9
+ * 1. `cart_audit_entries` — the typed cart-detail feed (powers the
10
+ * cart-detail UI for both Org-Admin and platform-admin views).
11
+ * 2. `audit_log_entries` — the platform-wide audit timeline
12
+ * (queryable across modules from the dashboard recent-activity
13
+ * surface of feature 024).
14
+ *
15
+ * The two writes share a single MikroORM unit-of-work so a partial
16
+ * commit is structurally impossible — either both rows land or
17
+ * neither does. No call site outside the carts module may write to
18
+ * `cart_audit_entries` directly.
19
+ *
20
+ * That claim used to hold only where `emFactory` returned the same manager
21
+ * twice (issue #152). `record()` took one fork for the cart row and
22
+ * `AuditPort.record()` took another for the platform row and flushed it
23
+ * itself, so in production — where the factory forks per call — the two rows
24
+ * were two transactions, and a failure between them landed the platform row
25
+ * without its cart-detail counterpart. Both rows are built on ONE manager now
26
+ * and land in one flush, which is what the paragraph above always said.
27
+ *
28
+ * `actorAdminUserId` on the `audit_log_entries` row is set ONLY when
29
+ * `actor_type === 'platform_admin'`. For customer / org-admin / system /
30
+ * sweep actors the column is null (the cart audit feed still records
31
+ * the actor in `cart_audit_entries.actor_id` / `.actor_type`).
32
+ */
33
+ export interface CartAuditRecordInput {
34
+ cartId: string;
35
+ actorType: CartAuditActorType;
36
+ actorId?: string | null;
37
+ action: CartAuditAction;
38
+ fromState?: string | null;
39
+ toState?: string | null;
40
+ reason?: string | null;
41
+ metadata?: Record<string, unknown>;
42
+ /** Correlation id from the inbound request (X-Request-Id). */
43
+ requestId?: string | null;
44
+ }
45
+ export declare class CartAuditService {
46
+ private readonly emFactory;
47
+ private readonly auditLog;
48
+ constructor(emFactory: () => EntityManager, auditLog: AuditPort);
49
+ /** One landing, on this service's own manager, flushed. The default form. */
50
+ record(input: CartAuditRecordInput): Promise<CartAuditEntry>;
51
+ /**
52
+ * Batch form, landing on the CALLER's EntityManager and flushing nothing
53
+ * (issue #152).
54
+ *
55
+ * The caller's next `flush()` is the unit of work, so a batch of cart writes
56
+ * and the audit rows for that batch commit together or not at all — the
57
+ * guarantee `record()` gives one write, given to a run of them. The
58
+ * abandonment sweep is the caller it exists for: it used to flush once per
59
+ * cart, which is a transaction per cart plus an identity map that never
60
+ * shrank, and at 2000 carts that cost 66 ms each.
61
+ *
62
+ * It flushes nothing on purpose. A method that both joined the caller's unit
63
+ * of work and committed it would decide, on the caller's behalf, that the
64
+ * audit rows are the end of the batch — and for the sweep they are not: the
65
+ * status flip is dirty on the same manager and belongs in the same commit.
66
+ *
67
+ * The name is {@link AuditPort.recordWithin}'s, deliberately: `Within`
68
+ * is the repository's word for "on the caller's manager, no flush", and it is
69
+ * the word `check-command-coverage` reads as an audit landing. A name of its
70
+ * own would have made this method invisible to that check while it was doing
71
+ * exactly what the check exists to find.
72
+ */
73
+ recordWithin(em: EntityManager, inputs: readonly CartAuditRecordInput[]): CartAuditEntry[];
74
+ }
75
+ //# sourceMappingURL=cart-audit-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-audit-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cart-audit-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAC;AACxE,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EACnB,MAAM,wCAAwC,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,kBAAkB,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,eAAe,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBADR,SAAS,EAAE,MAAM,aAAa,EAC9B,QAAQ,EAAE,SAAS;IAGtC,6EAA6E;IACvE,MAAM,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,cAAc,CAAC;IAQlE;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,YAAY,CACV,EAAE,EAAE,aAAa,EACjB,MAAM,EAAE,SAAS,oBAAoB,EAAE,GACtC,cAAc,EAAE;CAiCpB"}
@@ -0,0 +1,70 @@
1
+ import { CartAuditEntry } from '../entities/cart-audit-entry.entity.js';
2
+ export class CartAuditService {
3
+ emFactory;
4
+ auditLog;
5
+ constructor(emFactory, auditLog) {
6
+ this.emFactory = emFactory;
7
+ this.auditLog = auditLog;
8
+ }
9
+ /** One landing, on this service's own manager, flushed. The default form. */
10
+ async record(input) {
11
+ const em = this.emFactory();
12
+ const entries = this.recordWithin(em, [input]);
13
+ await em.flush();
14
+ // One input in, one entry out — the map below preserves the arity.
15
+ return entries[0];
16
+ }
17
+ /**
18
+ * Batch form, landing on the CALLER's EntityManager and flushing nothing
19
+ * (issue #152).
20
+ *
21
+ * The caller's next `flush()` is the unit of work, so a batch of cart writes
22
+ * and the audit rows for that batch commit together or not at all — the
23
+ * guarantee `record()` gives one write, given to a run of them. The
24
+ * abandonment sweep is the caller it exists for: it used to flush once per
25
+ * cart, which is a transaction per cart plus an identity map that never
26
+ * shrank, and at 2000 carts that cost 66 ms each.
27
+ *
28
+ * It flushes nothing on purpose. A method that both joined the caller's unit
29
+ * of work and committed it would decide, on the caller's behalf, that the
30
+ * audit rows are the end of the batch — and for the sweep they are not: the
31
+ * status flip is dirty on the same manager and belongs in the same commit.
32
+ *
33
+ * The name is {@link AuditPort.recordWithin}'s, deliberately: `Within`
34
+ * is the repository's word for "on the caller's manager, no flush", and it is
35
+ * the word `check-command-coverage` reads as an audit landing. A name of its
36
+ * own would have made this method invisible to that check while it was doing
37
+ * exactly what the check exists to find.
38
+ */
39
+ recordWithin(em, inputs) {
40
+ return inputs.map((input) => {
41
+ const entry = em.create(CartAuditEntry, {
42
+ cartId: input.cartId,
43
+ actorType: input.actorType,
44
+ ...(input.actorId !== undefined ? { actorId: input.actorId } : {}),
45
+ action: input.action,
46
+ ...(input.fromState !== undefined ? { fromState: input.fromState } : {}),
47
+ ...(input.toState !== undefined ? { toState: input.toState } : {}),
48
+ ...(input.reason !== undefined ? { reason: input.reason } : {}),
49
+ metadata: input.metadata ?? {},
50
+ });
51
+ em.persist(entry);
52
+ this.auditLog.recordWithin(em, {
53
+ action: `cart.${input.action}`,
54
+ objectType: 'cart',
55
+ objectId: input.cartId,
56
+ ...(input.actorType === 'platform_admin' && input.actorId
57
+ ? { actorAdminUserId: input.actorId }
58
+ : {}),
59
+ ...(input.actorType === 'customer' || input.actorType === 'org_admin'
60
+ ? { impersonatedCustomerAccountId: input.actorId ?? null }
61
+ : {}),
62
+ stateBefore: input.fromState ? { state: input.fromState } : null,
63
+ stateAfter: input.toState ? { state: input.toState } : null,
64
+ ...(input.requestId !== undefined ? { requestId: input.requestId } : {}),
65
+ });
66
+ return entry;
67
+ });
68
+ }
69
+ }
70
+ //# sourceMappingURL=cart-audit-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-audit-service.js","sourceRoot":"","sources":["../../../src/backend/services/cart-audit-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,104 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { Cart } from '../entities/cart.entity.js';
3
+ import type { CatalogProductReadPort, QuoteRequestReadPort, RfqCustomerContext, RfqCustomerPort } from '@endora-commerce/contracts';
4
+ import type { SalesChannelMembershipPort } from '@endora-commerce/platform/kernel';
5
+ import type { CartService, CustomerContext as CartCustomerContext } from './cart-service.js';
6
+ /**
7
+ * Three conversions around the Cart (feature 027 US3):
8
+ *
9
+ * - convertToQuoteRequest(cart, ctx, note?)
10
+ * Reads the cart's lines, hands them to `RfqCustomerPort.createForCustomer`,
11
+ * flips the source cart's status to `completed` with
12
+ * `converted_to_quote_request_id` set. The next storefront visit
13
+ * lazy-creates a fresh `active` cart for the buyer.
14
+ *
15
+ * - createCartFromQuoteRequest(qrId, ctx)
16
+ * Reads a Quote Request's lines, creates (or appends to) the buyer's
17
+ * active cart with re-resolved prices (the QR's proposed prices are
18
+ * NOT carried over — per FR-017). Skips lines whose product is no
19
+ * longer purchasable; returns the dropped-line list to the caller.
20
+ *
21
+ * - appendShoppingListToCart(listId, ctx)
22
+ * Delegated to `ShoppingListService.convertToCart`, which already
23
+ * implements the append + skip-archived-products contract from
24
+ * feature 010 / FR-018. This service just exposes a uniform return
25
+ * shape across all three conversions.
26
+ *
27
+ * The service does not know about `cart_audit_entries` directly — every
28
+ * write path bumps `cart.lastActivityAt` so the abandonment sweep treats
29
+ * the cart as fresh. Audit-trail wiring is left to a follow-up.
30
+ */
31
+ export interface CartDroppedLine {
32
+ productId: string;
33
+ productName: string;
34
+ reason: 'not_purchasable' | 'out_of_stock' | 'no_price_in_customer_list' | 'removed_by_conversion';
35
+ }
36
+ export interface ConvertToQrResult {
37
+ quoteRequestId: string;
38
+ cartId: string;
39
+ }
40
+ export interface CreateCartFromQrResult {
41
+ cartId: string;
42
+ appendedLineCount: number;
43
+ droppedLines: CartDroppedLine[];
44
+ }
45
+ export interface AppendShoppingListResult {
46
+ cartId: string;
47
+ appendedLineCount: number;
48
+ droppedLines: CartDroppedLine[];
49
+ }
50
+ export declare class CartConversionService {
51
+ private readonly emFactory;
52
+ private readonly cartService;
53
+ /** `quote_requests`' customer-facing write surface. */
54
+ private readonly rfqService;
55
+ /** `quote_requests`' row read model — the quote and its lines. */
56
+ private readonly quoteRequests;
57
+ /** `catalog`'s product read model — is the quoted product still there? */
58
+ private readonly catalogProducts;
59
+ /**
60
+ * The sanctioned bridge accessor (Constitution XII) — and on this seam it
61
+ * is what keeps the *reason* honest (issue #259). `CartService.addItem`
62
+ * refuses an out-of-channel line by itself, but this method catches every
63
+ * `HttpError` from it and reports one dropped-line reason,
64
+ * `no_price_in_customer_list`. A line the channel does not sell has nothing
65
+ * to do with the buyer's price list, so it is narrowed here instead and
66
+ * falls into the `not_purchasable` branch — where a line whose product has
67
+ * gone already lands.
68
+ */
69
+ private readonly channelMembership;
70
+ constructor(emFactory: () => EntityManager, cartService: CartService,
71
+ /** `quote_requests`' customer-facing write surface. */
72
+ rfqService: RfqCustomerPort,
73
+ /** `quote_requests`' row read model — the quote and its lines. */
74
+ quoteRequests: QuoteRequestReadPort,
75
+ /** `catalog`'s product read model — is the quoted product still there? */
76
+ catalogProducts: CatalogProductReadPort,
77
+ /**
78
+ * The sanctioned bridge accessor (Constitution XII) — and on this seam it
79
+ * is what keeps the *reason* honest (issue #259). `CartService.addItem`
80
+ * refuses an out-of-channel line by itself, but this method catches every
81
+ * `HttpError` from it and reports one dropped-line reason,
82
+ * `no_price_in_customer_list`. A line the channel does not sell has nothing
83
+ * to do with the buyer's price list, so it is narrowed here instead and
84
+ * falls into the `not_purchasable` branch — where a line whose product has
85
+ * gone already lands.
86
+ */
87
+ channelMembership: SalesChannelMembershipPort);
88
+ /**
89
+ * Cart → Quote Request. Refuses on an empty cart. Source cart flips to
90
+ * `completed` with `converted_to_quote_request_id` set.
91
+ */
92
+ convertToQuoteRequest(cart: Cart, ctx: RfqCustomerContext, note?: string): Promise<ConvertToQrResult>;
93
+ /**
94
+ * Quote Request → Cart. Creates a fresh `active` cart (or returns the
95
+ * existing one) for the buyer, appends the QR's lines with re-resolved
96
+ * prices (via `CartService.addItem`, which already routes through the
97
+ * pricing resolver when wired), and surfaces a list of dropped lines.
98
+ *
99
+ * The QR's `desiredUnitPrice` is intentionally dropped — the cart's
100
+ * unit prices come from the customer's current price list.
101
+ */
102
+ createCartFromQuoteRequest(qrId: string, ctx: CartCustomerContext): Promise<CreateCartFromQrResult>;
103
+ }
104
+ //# sourceMappingURL=cart-conversion-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-conversion-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cart-conversion-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAElD,OAAO,KAAK,EACV,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EAChB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAEnF,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,IAAI,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE7F;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,iBAAiB,GAAG,cAAc,GAAG,2BAA2B,GAAG,uBAAuB,CAAC;CACpG;AAED,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,eAAe,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,eAAe,EAAE,CAAC;CACjC;AAED,qBAAa,qBAAqB;IAE9B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,uDAAuD;IACvD,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,kEAAkE;IAClE,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ,CAAC,iBAAiB;gBAlBjB,SAAS,EAAE,MAAM,aAAa,EAC9B,WAAW,EAAE,WAAW;IACzC,uDAAuD;IACtC,UAAU,EAAE,eAAe;IAC5C,kEAAkE;IACjD,aAAa,EAAE,oBAAoB;IACpD,0EAA0E;IACzD,eAAe,EAAE,sBAAsB;IACxD;;;;;;;;;OASG;IACc,iBAAiB,EAAE,0BAA0B;IAGhE;;;OAGG;IACG,qBAAqB,CACzB,IAAI,EAAE,IAAI,EACV,GAAG,EAAE,kBAAkB,EACvB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,iBAAiB,CAAC;IAmD7B;;;;;;;;OAQG;IACG,0BAA0B,CAC9B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,mBAAmB,GACvB,OAAO,CAAC,sBAAsB,CAAC;CAqFnC"}