@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,98 @@
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 CartOrganizationVisibilityService {
6
+ emFactory;
7
+ customerAccounts;
8
+ constructor(emFactory,
9
+ /**
10
+ * `customer_accounts`' read model (feature 075, Phase C). The org-admin
11
+ * cart list renders each cart's owner by e-mail, and it read the account
12
+ * row itself — out of a table deactivation leaves in place.
13
+ */
14
+ customerAccounts) {
15
+ this.emFactory = emFactory;
16
+ this.customerAccounts = customerAccounts;
17
+ }
18
+ async list(organizationId, query = {}) {
19
+ const em = this.emFactory();
20
+ const where = { organizationId };
21
+ if (query.status && query.status.length > 0)
22
+ where['status'] = { $in: query.status };
23
+ if (query.approvalStatus && query.approvalStatus.length > 0) {
24
+ where['approvalStatus'] = { $in: query.approvalStatus };
25
+ }
26
+ const page = query.page ?? 1;
27
+ const pageSize = Math.min(query.pageSize ?? 50, 100);
28
+ const orderBy = this.sortClause(query.sort ?? 'last_activity_desc');
29
+ const [rows, totalCount] = await em.findAndCount(Cart, where, {
30
+ orderBy,
31
+ limit: pageSize,
32
+ offset: (page - 1) * pageSize,
33
+ });
34
+ const ownerIds = Array.from(new Set(rows.map((r) => r.customerAccountId).filter((id) => Boolean(id))));
35
+ const owners = await this.customerAccounts.findByIds(ownerIds);
36
+ const ownerById = new Map(owners.map((o) => [o.id, o]));
37
+ // Bulk item-count + total resolution.
38
+ const cartIds = rows.map((r) => r.id);
39
+ const itemCounts = new Map();
40
+ if (cartIds.length > 0) {
41
+ const items = await em.find(CartItem, { cartId: { $in: cartIds } });
42
+ for (const it of items) {
43
+ const prev = itemCounts.get(it.cartId) ?? { count: 0, total: 0, currency: it.currency };
44
+ prev.count += 1;
45
+ prev.total += Number(it.unitPrice) * it.quantity;
46
+ itemCounts.set(it.cartId, prev);
47
+ }
48
+ }
49
+ return {
50
+ data: rows
51
+ .filter((r) => r.customerAccountId !== null && r.customerAccountId !== undefined)
52
+ .map((cart) => {
53
+ const ic = itemCounts.get(cart.id) ?? { count: 0, total: 0, currency: 'PLN' };
54
+ const owner = ownerById.get(cart.customerAccountId);
55
+ return {
56
+ id: cart.id,
57
+ ownerCustomerAccountId: cart.customerAccountId,
58
+ ownerDisplayName: owner?.email ?? cart.customerAccountId,
59
+ status: cart.status,
60
+ approvalStatus: cart.approvalStatus,
61
+ itemCount: ic.count,
62
+ total: { amount: ic.total, currency: ic.currency },
63
+ lastActivityAt: cart.lastActivityAt.toISOString(),
64
+ submittedForApprovalAt: cart.submittedForApprovalAt?.toISOString() ?? null,
65
+ };
66
+ }),
67
+ meta: { page, pageSize, totalCount },
68
+ };
69
+ }
70
+ /**
71
+ * Fetch a single cart in the caller's Organization. Refuses with 404
72
+ * when the cart belongs to a different Organization (anti-enumeration
73
+ * symmetry with the ordinary-member ownership check).
74
+ */
75
+ async getInOrganization(cartId, organizationId) {
76
+ const em = this.emFactory();
77
+ const cart = await em.findOne(Cart, { id: cartId });
78
+ if (!cart || cart.organizationId !== organizationId) {
79
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'cart_not_found');
80
+ }
81
+ return cart;
82
+ }
83
+ sortClause(sort) {
84
+ switch (sort) {
85
+ case 'last_activity_asc':
86
+ return { lastActivityAt: 'asc' };
87
+ case 'total_desc':
88
+ case 'total_asc':
89
+ return { updatedAt: sort === 'total_desc' ? 'desc' : 'asc' };
90
+ case 'created_desc':
91
+ return { createdAt: 'desc' };
92
+ case 'last_activity_desc':
93
+ default:
94
+ return { lastActivityAt: 'desc' };
95
+ }
96
+ }
97
+ }
98
+ //# sourceMappingURL=cart-organization-visibility-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-organization-visibility-service.js","sourceRoot":"","sources":["../../../src/backend/services/cart-organization-visibility-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { CartCustomerContext, CartWithItems } from '@endora-commerce/contracts';
3
+ import type { CartPlacementApplyPort } from '../../ports/index.js';
4
+ /**
5
+ * `cartPlacementApplyPort` — the basket half of order placement, run on the
6
+ * caller's `EntityManager` (feature 080, T048; D-169).
7
+ *
8
+ * `orders` held `Cart` and `CartItem` itself until T048: `placeOrder` read the
9
+ * basket on its own transaction, built the order out of it, and ended by
10
+ * deleting the lines, moving the cart to `completed` and stamping
11
+ * `completed_order_id` — four statements against this module's two tables,
12
+ * written by a module that owns neither. D-168 leaves a packaged `carts` no
13
+ * entity class for `orders` to name, so the reach had to go before this module
14
+ * moves.
15
+ *
16
+ * **The transaction is unchanged, and that is the point of the conversion.**
17
+ * The read and the completion still run on placement's own `EntityManager`;
18
+ * `carts_completed_order_fk` is untouched; a placement that fails still leaves
19
+ * the basket exactly as the buyer left it, which
20
+ * `test/integration/orders/place-order-failure-preserves-cart.test.ts` asserts
21
+ * and which a port opening its own transaction could not deliver. What changed
22
+ * is that the statements are written by the module that owns the tables, and
23
+ * what crosses the boundary is `CartWithItems` — published records — instead of
24
+ * two managed entities the consumer could move any column of (D-77's first
25
+ * narrowing).
26
+ *
27
+ * The port's own doc block, in `../ports/index.ts`, states the constraint and
28
+ * why the read takes an `EntityManager` where `CartReadPort.findActiveForCustomer`
29
+ * does not.
30
+ */
31
+ export declare class CartPlacementApplyService implements CartPlacementApplyPort {
32
+ readActiveForPlacement(em: EntityManager, ctx: CartCustomerContext): Promise<CartWithItems | null>;
33
+ completeForOrder(em: EntityManager, input: {
34
+ cartId: string;
35
+ orderId: string;
36
+ }): Promise<void>;
37
+ }
38
+ //# sourceMappingURL=cart-placement-apply-port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-placement-apply-port.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cart-placement-apply-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAGrF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAGnE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,yBAA0B,YAAW,sBAAsB;IAChE,sBAAsB,CAC1B,EAAE,EAAE,aAAa,EACjB,GAAG,EAAE,mBAAmB,GACvB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAgB1B,gBAAgB,CACpB,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GACzC,OAAO,CAAC,IAAI,CAAC;CAqBjB"}
@@ -0,0 +1,71 @@
1
+ import { Cart } from '../entities/cart.entity.js';
2
+ import { CartItem } from '../entities/cart-item.entity.js';
3
+ import { toCartItemRecord, toCartRecord } from './cart-read-port.js';
4
+ /**
5
+ * `cartPlacementApplyPort` — the basket half of order placement, run on the
6
+ * caller's `EntityManager` (feature 080, T048; D-169).
7
+ *
8
+ * `orders` held `Cart` and `CartItem` itself until T048: `placeOrder` read the
9
+ * basket on its own transaction, built the order out of it, and ended by
10
+ * deleting the lines, moving the cart to `completed` and stamping
11
+ * `completed_order_id` — four statements against this module's two tables,
12
+ * written by a module that owns neither. D-168 leaves a packaged `carts` no
13
+ * entity class for `orders` to name, so the reach had to go before this module
14
+ * moves.
15
+ *
16
+ * **The transaction is unchanged, and that is the point of the conversion.**
17
+ * The read and the completion still run on placement's own `EntityManager`;
18
+ * `carts_completed_order_fk` is untouched; a placement that fails still leaves
19
+ * the basket exactly as the buyer left it, which
20
+ * `test/integration/orders/place-order-failure-preserves-cart.test.ts` asserts
21
+ * and which a port opening its own transaction could not deliver. What changed
22
+ * is that the statements are written by the module that owns the tables, and
23
+ * what crosses the boundary is `CartWithItems` — published records — instead of
24
+ * two managed entities the consumer could move any column of (D-77's first
25
+ * narrowing).
26
+ *
27
+ * The port's own doc block, in `../ports/index.ts`, states the constraint and
28
+ * why the read takes an `EntityManager` where `CartReadPort.findActiveForCustomer`
29
+ * does not.
30
+ */
31
+ export class CartPlacementApplyService {
32
+ async readActiveForPlacement(em, ctx) {
33
+ // The same conditional org predicate `CartReadService` applies, and for the
34
+ // same reason: `carts.organization_id` is nullable for a customer account
35
+ // that has none, so a caller with no organisation must not be narrowed out
36
+ // of its own basket. `Cart` is `@CustomerScoped`, so the tenant guard is on
37
+ // the customer either way.
38
+ const cart = await em.findOne(Cart, {
39
+ customerAccountId: ctx.customerAccountId,
40
+ ...(ctx.organizationId === null ? {} : { organizationId: ctx.organizationId }),
41
+ status: 'active',
42
+ });
43
+ if (!cart)
44
+ return null;
45
+ const items = await em.find(CartItem, { cartId: cart.id });
46
+ return { cart: toCartRecord(cart), items: items.map(toCartItemRecord) };
47
+ }
48
+ async completeForOrder(em, input) {
49
+ // command-coverage-ignore: the basket half of an order placement, inside
50
+ // the caller's transaction. `orders` audits the placement as one operation
51
+ // through its own Command; a second audit entry for the basket the order
52
+ // came from would record the same decision twice, and the cart is not an
53
+ // auditable domain object in its own right — the same reasoning
54
+ // `cartWritePort.replaceItemsForCustomer` carries.
55
+ const cart = await em.findOne(Cart, { id: input.cartId });
56
+ if (!cart)
57
+ return;
58
+ await em.nativeDelete(CartItem, { cartId: cart.id });
59
+ // Feature 027's renamed status vocabulary: the cart produced an order, so
60
+ // its lifecycle terminates in `completed`.
61
+ cart.status = 'completed';
62
+ // Which order emptied this cart (D-94.1), held by `carts_completed_order_fk`.
63
+ cart.completedOrderId = input.orderId;
64
+ // Always null on an authenticated checkout today, but cleared for the edge
65
+ // case where a customer checks out from an anon-derived cart that still
66
+ // carries the token.
67
+ cart.anonymousCartToken = null;
68
+ await em.flush();
69
+ }
70
+ }
71
+ //# sourceMappingURL=cart-placement-apply-port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-placement-apply-port.js","sourceRoot":"","sources":["../../../src/backend/services/cart-placement-apply-port.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAE3D,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAErE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,OAAO,yBAAyB;IACpC,KAAK,CAAC,sBAAsB,CAC1B,EAAiB,EACjB,GAAwB;QAExB,4EAA4E;QAC5E,0EAA0E;QAC1E,2EAA2E;QAC3E,4EAA4E;QAC5E,2BAA2B;QAC3B,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE;YAClC,iBAAiB,EAAE,GAAG,CAAC,iBAAiB;YACxC,GAAG,CAAC,GAAG,CAAC,cAAc,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,GAAG,CAAC,cAAc,EAAE,CAAC;YAC9E,MAAM,EAAE,QAAQ;SACjB,CAAC,CAAC;QACH,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QACvB,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3D,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC;IAC1E,CAAC;IAED,KAAK,CAAC,gBAAgB,CACpB,EAAiB,EACjB,KAA0C;QAE1C,yEAAyE;QACzE,2EAA2E;QAC3E,yEAAyE;QACzE,yEAAyE;QACzE,gEAAgE;QAChE,mDAAmD;QACnD,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,MAAM,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QACrD,0EAA0E;QAC1E,2CAA2C;QAC3C,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC;QAC1B,8EAA8E;QAC9E,IAAI,CAAC,gBAAgB,GAAG,KAAK,CAAC,OAAO,CAAC;QACtC,2EAA2E;QAC3E,wEAAwE;QACxE,qBAAqB;QACrB,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;QAC/B,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;IACnB,CAAC;CACF"}
@@ -0,0 +1,62 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { CatalogProductReadPort, LinePricePort, OrganizationDetailsPort } from '@endora-commerce/contracts';
3
+ import type { CartRecomputeCache } from './cart-recompute-cache.js';
4
+ import type { CartItem } from '../entities/cart-item.entity.js';
5
+ /**
6
+ * Per-cart pricing-recompute helper (feature 027 §R5).
7
+ *
8
+ * Re-resolves the unit price of every cart line against the customer's
9
+ * current resolved price list. The cache (when available) lets the
10
+ * mini-cart → full-cart → checkout-entry walk reuse the same resolver
11
+ * output for 30 s.
12
+ *
13
+ * Inputs: the line set is the list of `CartItem` rows for one cart.
14
+ * Outputs: per-line `{cartItemId, amount, currency}` — `amount` is null
15
+ * when the resolver returned null (no eligible price-list bracket for
16
+ * this customer; the cart UI surfaces a `no_price_in_customer_list`
17
+ * unavailability reason for the line).
18
+ *
19
+ * Implementation note: PricingService is per-line today (no batched
20
+ * variant). The cache amortizes work across reads; a future optimization
21
+ * may batch into a single PricingService call when the resolver gains a
22
+ * batch endpoint.
23
+ */
24
+ export interface RecomputeLineRequest {
25
+ cartItemId: string;
26
+ productId: string;
27
+ variantId?: string | null;
28
+ quantity: number;
29
+ }
30
+ export interface RecomputedLinePrice {
31
+ cartItemId: string;
32
+ amount: number | null;
33
+ currency: string;
34
+ }
35
+ export interface CartPricingRecomputeContext {
36
+ cartId: string;
37
+ organizationId: string | null;
38
+ salesChannelId: string | null;
39
+ }
40
+ export declare class CartPricingRecompute {
41
+ private readonly emFactory;
42
+ /** `price_lists`' line-resolution slice (feature 075, Phase C). */
43
+ private readonly pricingService;
44
+ private readonly cache;
45
+ /** `catalog`'s product read model — the two fields the resolver reads. */
46
+ private readonly catalogProducts;
47
+ /** `organizations`' read model — the buying org and its customer group. */
48
+ private readonly organizations;
49
+ constructor(emFactory: () => EntityManager,
50
+ /** `price_lists`' line-resolution slice (feature 075, Phase C). */
51
+ pricingService: LinePricePort, cache: CartRecomputeCache,
52
+ /** `catalog`'s product read model — the two fields the resolver reads. */
53
+ catalogProducts: CatalogProductReadPort,
54
+ /** `organizations`' read model — the buying org and its customer group. */
55
+ organizations: OrganizationDetailsPort);
56
+ recompute(ctx: CartPricingRecomputeContext, lines: RecomputeLineRequest[]): Promise<RecomputedLinePrice[]>;
57
+ /** Persist the most recently recomputed prices back onto `cart_items`. */
58
+ writeBackTo(em: EntityManager, items: CartItem[], prices: RecomputedLinePrice[]): Promise<void>;
59
+ private allLinesCached;
60
+ private loadChannel;
61
+ }
62
+ //# sourceMappingURL=cart-pricing-recompute.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-pricing-recompute.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cart-pricing-recompute.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EACV,sBAAsB,EACtB,aAAa,EACb,uBAAuB,EACxB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,kBAAkB,EAAuB,MAAM,2BAA2B,CAAC;AACzF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAEhE;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,2BAA2B;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,qBAAa,oBAAoB;IAE7B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAPb,SAAS,EAAE,MAAM,aAAa;IAC/C,mEAAmE;IAClD,cAAc,EAAE,aAAa,EAC7B,KAAK,EAAE,kBAAkB;IAC1C,0EAA0E;IACzD,eAAe,EAAE,sBAAsB;IACxD,2EAA2E;IAC1D,aAAa,EAAE,uBAAuB;IAGnD,SAAS,CACb,GAAG,EAAE,2BAA2B,EAChC,KAAK,EAAE,oBAAoB,EAAE,GAC5B,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAyFjC,0EAA0E;IACpE,WAAW,CAAC,EAAE,EAAE,aAAa,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,mBAAmB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAerG,OAAO,CAAC,cAAc;YAKR,WAAW;CAM1B"}
@@ -0,0 +1,131 @@
1
+ import { SalesChannel } from '@endora-commerce/platform/kernel';
2
+ export class CartPricingRecompute {
3
+ emFactory;
4
+ pricingService;
5
+ cache;
6
+ catalogProducts;
7
+ organizations;
8
+ constructor(emFactory,
9
+ /** `price_lists`' line-resolution slice (feature 075, Phase C). */
10
+ pricingService, cache,
11
+ /** `catalog`'s product read model — the two fields the resolver reads. */
12
+ catalogProducts,
13
+ /** `organizations`' read model — the buying org and its customer group. */
14
+ organizations) {
15
+ this.emFactory = emFactory;
16
+ this.pricingService = pricingService;
17
+ this.cache = cache;
18
+ this.catalogProducts = catalogProducts;
19
+ this.organizations = organizations;
20
+ }
21
+ async recompute(ctx, lines) {
22
+ if (lines.length === 0)
23
+ return [];
24
+ // Cache hit returns the previously resolved prices in their original
25
+ // order (the cache stores keyed by cartItemId so any line removed
26
+ // since the last resolve is naturally dropped on the next read).
27
+ // `unitPrice.amount === null` in the cache means the resolver
28
+ // previously returned no eligible price; we propagate the null so
29
+ // the caller falls back to the snapshot.
30
+ const cached = await this.cache.get(ctx.cartId);
31
+ if (cached && this.allLinesCached(cached, lines)) {
32
+ return lines.map((line) => {
33
+ const hit = cached.lines.find((c) => c.cartItemId === line.cartItemId);
34
+ if (!hit) {
35
+ return { cartItemId: line.cartItemId, amount: null, currency: 'PLN' };
36
+ }
37
+ return {
38
+ cartItemId: line.cartItemId,
39
+ amount: hit.unitPrice.amount,
40
+ currency: hit.unitPrice.currency,
41
+ };
42
+ });
43
+ }
44
+ // Cache miss → resolve from scratch. We load the channel + org once
45
+ // (per-cart) rather than per-line.
46
+ const em = this.emFactory();
47
+ const channel = await this.loadChannel(em, ctx.salesChannelId);
48
+ const organization = ctx.organizationId
49
+ ? await this.organizations.findById(ctx.organizationId)
50
+ : null;
51
+ if (!channel) {
52
+ return lines.map((line) => ({ cartItemId: line.cartItemId, amount: null, currency: 'PLN' }));
53
+ }
54
+ const productIds = Array.from(new Set(lines.map((l) => l.productId)));
55
+ const products = await this.catalogProducts.findByIds(productIds);
56
+ const productById = new Map(products.map((p) => [p.id, p]));
57
+ const recomputed = [];
58
+ for (const line of lines) {
59
+ const product = productById.get(line.productId);
60
+ if (!product) {
61
+ recomputed.push({ cartItemId: line.cartItemId, amount: null, currency: 'PLN' });
62
+ continue;
63
+ }
64
+ // No `catch` (issue #84): `resolveLinePrice` already answers "no resolver
65
+ // match" with `null`, which is the `amount: null` this loop wants, and
66
+ // that answer then goes into the recompute cache below. A `catch` here
67
+ // cached "no price" for the whole cart whenever `price_lists` was
68
+ // switched off, and the TTL kept doing it after it came back.
69
+ const resolved = await this.pricingService.resolveLinePrice({
70
+ product,
71
+ variantId: line.variantId ?? null,
72
+ context: {
73
+ quantity: line.quantity,
74
+ ...(organization ? { organization } : {}),
75
+ salesChannel: channel,
76
+ },
77
+ });
78
+ if (!resolved) {
79
+ recomputed.push({ cartItemId: line.cartItemId, amount: null, currency: 'PLN' });
80
+ }
81
+ else {
82
+ recomputed.push({
83
+ cartItemId: line.cartItemId,
84
+ amount: Number(resolved.amount),
85
+ currency: resolved.currency,
86
+ });
87
+ }
88
+ }
89
+ // Write the resolved set to the cache so subsequent reads within the
90
+ // TTL skip the resolver entirely. `amount: null` is preserved so the
91
+ // cache hit correctly signals "no resolver match — use snapshot".
92
+ const payload = {
93
+ cartId: ctx.cartId,
94
+ lines: recomputed.map((r) => ({
95
+ cartItemId: r.cartItemId,
96
+ unitPrice: { amount: r.amount, currency: r.currency },
97
+ resolvedAt: new Date().toISOString(),
98
+ })),
99
+ resolvedAt: new Date().toISOString(),
100
+ };
101
+ await this.cache.put(ctx.cartId, payload);
102
+ return recomputed;
103
+ }
104
+ /** Persist the most recently recomputed prices back onto `cart_items`. */
105
+ async writeBackTo(em, items, prices) {
106
+ // command-coverage-ignore: writes derived (recomputed) line prices back to the
107
+ // ephemeral cart — transient working state, not an audited domain mutation.
108
+ const byId = new Map(prices.map((p) => [p.cartItemId, p]));
109
+ const now = new Date();
110
+ for (const item of items) {
111
+ const price = byId.get(item.id);
112
+ if (!price || price.amount === null)
113
+ continue;
114
+ item.recomputedUnitPrice = price.amount.toFixed(2);
115
+ item.recomputedAt = now;
116
+ item.recomputedCurrency = price.currency;
117
+ }
118
+ await em.flush();
119
+ }
120
+ allLinesCached(cached, lines) {
121
+ const cachedIds = new Set(cached.lines.map((l) => l.cartItemId));
122
+ return lines.every((l) => cachedIds.has(l.cartItemId));
123
+ }
124
+ async loadChannel(em, channelId) {
125
+ if (channelId) {
126
+ return em.findOne(SalesChannel, { id: channelId });
127
+ }
128
+ return em.findOne(SalesChannel, { systemDefault: true });
129
+ }
130
+ }
131
+ //# sourceMappingURL=cart-pricing-recompute.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-pricing-recompute.js","sourceRoot":"","sources":["../../../src/backend/services/cart-pricing-recompute.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AA2ChE,MAAM,OAAO,oBAAoB;IAEZ;IAEA;IACA;IAEA;IAEA;IARnB,YACmB,SAA8B;IAC/C,mEAAmE;IAClD,cAA6B,EAC7B,KAAyB;IAC1C,0EAA0E;IACzD,eAAuC;IACxD,2EAA2E;IAC1D,aAAsC;QAPtC,cAAS,GAAT,SAAS,CAAqB;QAE9B,mBAAc,GAAd,cAAc,CAAe;QAC7B,UAAK,GAAL,KAAK,CAAoB;QAEzB,oBAAe,GAAf,eAAe,CAAwB;QAEvC,kBAAa,GAAb,aAAa,CAAyB;IACtD,CAAC;IAEJ,KAAK,CAAC,SAAS,CACb,GAAgC,EAChC,KAA6B;QAE7B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAElC,qEAAqE;QACrE,kEAAkE;QAClE,iEAAiE;QACjE,8DAA8D;QAC9D,kEAAkE;QAClE,yCAAyC;QACzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAChD,IAAI,MAAM,IAAI,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC;YACjD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;gBACxB,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,IAAI,CAAC,UAAU,CAAC,CAAC;gBACvE,IAAI,CAAC,GAAG,EAAE,CAAC;oBACT,OAAO,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;gBACxE,CAAC;gBACD,OAAO;oBACL,UAAU,EAAE,IAAI,CAAC,UAAU;oBAC3B,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,MAAM;oBAC5B,QAAQ,EAAE,GAAG,CAAC,SAAS,CAAC,QAAQ;iBACjC,CAAC;YACJ,CAAC,CAAC,CAAC;QACL,CAAC;QAED,oEAAoE;QACpE,mCAAmC;QACnC,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;QAC/D,MAAM,YAAY,GAAG,GAAG,CAAC,cAAc;YACrC,CAAC,CAAC,MAAM,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,cAAc,CAAC;YACvD,CAAC,CAAC,IAAI,CAAC;QAET,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;QAC/F,CAAC;QAED,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QACtE,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QAClE,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAE5D,MAAM,UAAU,GAA0B,EAAE,CAAC;QAC7C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAChD,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,UAAU,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;gBAChF,SAAS;YACX,CAAC;YACD,0EAA0E;YAC1E,uEAAuE;YACvE,uEAAuE;YACvE,kEAAkE;YAClE,8DAA8D;YAC9D,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,gBAAgB,CAAC;gBAC1D,OAAO;gBACP,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI;gBACjC,OAAO,EAAE;oBACP,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBACzC,YAAY,EAAE,OAAO;iBACtB;aACF,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,UAAU,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;YAClF,CAAC;iBAAM,CAAC;gBACN,UAAU,CAAC,IAAI,CAAC;oBACd,UAAU,EAAE,IAAI,CAAC,UAAU;oBAC3B,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;oBAC/B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;iBAC5B,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,qEAAqE;QACrE,qEAAqE;QACrE,kEAAkE;QAClE,MAAM,OAAO,GAAwB;YACnC,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,KAAK,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC5B,UAAU,EAAE,CAAC,CAAC,UAAU;gBACxB,SAAS,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE;gBACrD,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACrC,CAAC,CAAC;YACH,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACrC,CAAC;QACF,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAE1C,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,0EAA0E;IAC1E,KAAK,CAAC,WAAW,CAAC,EAAiB,EAAE,KAAiB,EAAE,MAA6B;QACnF,+EAA+E;QAC/E,4EAA4E;QAC5E,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3D,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAChC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI;gBAAE,SAAS;YAC9C,IAAI,CAAC,mBAAmB,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YACnD,IAAI,CAAC,YAAY,GAAG,GAAG,CAAC;YACxB,IAAI,CAAC,kBAAkB,GAAG,KAAK,CAAC,QAAQ,CAAC;QAC3C,CAAC;QACD,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;IACnB,CAAC;IAEO,cAAc,CAAC,MAA2B,EAAE,KAA6B;QAC/E,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;QACjE,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;IACzD,CAAC;IAEO,KAAK,CAAC,WAAW,CAAC,EAAiB,EAAE,SAAwB;QACnE,IAAI,SAAS,EAAE,CAAC;YACd,OAAO,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;QACrD,CAAC;QACD,OAAO,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3D,CAAC;CACF"}
@@ -0,0 +1,29 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ /**
3
+ * CartQueryService — read-only cart lookups by customer (feature 040, US5 /
4
+ * FR-030/FR-031). Surfaces the customer's current (active) cart and their
5
+ * abandoned carts for the admin customer-detail panels.
6
+ */
7
+ export interface CartItemView {
8
+ productId: string;
9
+ variantId: string | null;
10
+ quantity: number;
11
+ unitPrice: string;
12
+ currency: string;
13
+ }
14
+ export interface CartView {
15
+ id: string;
16
+ status: string;
17
+ lastActivityAt: string | null;
18
+ items: CartItemView[];
19
+ }
20
+ export interface CustomerCartsView {
21
+ current: CartView | null;
22
+ abandoned: CartView[];
23
+ }
24
+ export declare class CartQueryService {
25
+ private readonly emFactory;
26
+ constructor(emFactory: () => EntityManager);
27
+ listForCustomer(customerAccountId: string): Promise<CustomerCartsView>;
28
+ }
29
+ //# sourceMappingURL=cart-query-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-query-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cart-query-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAI3D;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,QAAQ,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,QAAQ,EAAE,CAAC;CACvB;AAED,qBAAa,gBAAgB;IACf,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,eAAe,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAqC7E"}
@@ -0,0 +1,40 @@
1
+ import { Cart } from '../entities/cart.entity.js';
2
+ import { CartItem } from '../entities/cart-item.entity.js';
3
+ export class CartQueryService {
4
+ emFactory;
5
+ constructor(emFactory) {
6
+ this.emFactory = emFactory;
7
+ }
8
+ async listForCustomer(customerAccountId) {
9
+ const em = this.emFactory();
10
+ const carts = await em.find(Cart, { customerAccountId }, { orderBy: { lastActivityAt: 'desc' } });
11
+ if (carts.length === 0)
12
+ return { current: null, abandoned: [] };
13
+ const items = await em.find(CartItem, { cartId: { $in: carts.map((c) => c.id) } });
14
+ const byCart = new Map();
15
+ for (const it of items) {
16
+ const list = byCart.get(it.cartId) ?? [];
17
+ list.push({
18
+ productId: it.productId,
19
+ variantId: it.variantId ?? null,
20
+ quantity: it.quantity,
21
+ unitPrice: it.unitPrice,
22
+ currency: it.currency,
23
+ });
24
+ byCart.set(it.cartId, list);
25
+ }
26
+ const toView = (c) => ({
27
+ id: c.id,
28
+ status: c.status,
29
+ lastActivityAt: c.lastActivityAt ? c.lastActivityAt.toISOString() : null,
30
+ items: byCart.get(c.id) ?? [],
31
+ });
32
+ const current = carts.find((c) => c.status === 'active') ?? null;
33
+ const abandoned = carts.filter((c) => c.status === 'abandoned');
34
+ return {
35
+ current: current ? toView(current) : null,
36
+ abandoned: abandoned.map(toView),
37
+ };
38
+ }
39
+ }
40
+ //# sourceMappingURL=cart-query-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-query-service.js","sourceRoot":"","sources":["../../../src/backend/services/cart-query-service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AA2B3D,MAAM,OAAO,gBAAgB;IACE;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D,KAAK,CAAC,eAAe,CAAC,iBAAyB;QAC7C,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CACzB,IAAI,EACJ,EAAE,iBAAiB,EAAE,EACrB,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,CACxC,CAAC;QACF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;QAEhE,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QACnF,MAAM,MAAM,GAAG,IAAI,GAAG,EAA0B,CAAC;QACjD,KAAK,MAAM,EAAE,IAAI,KAAK,EAAE,CAAC;YACvB,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACzC,IAAI,CAAC,IAAI,CAAC;gBACR,SAAS,EAAE,EAAE,CAAC,SAAS;gBACvB,SAAS,EAAE,EAAE,CAAC,SAAS,IAAI,IAAI;gBAC/B,QAAQ,EAAE,EAAE,CAAC,QAAQ;gBACrB,SAAS,EAAE,EAAE,CAAC,SAAS;gBACvB,QAAQ,EAAE,EAAE,CAAC,QAAQ;aACtB,CAAC,CAAC;YACH,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC9B,CAAC;QAED,MAAM,MAAM,GAAG,CAAC,CAAO,EAAY,EAAE,CAAC,CAAC;YACrC,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,cAAc,EAAE,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI;YACxE,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE;SAC9B,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,IAAI,IAAI,CAAC;QACjE,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC;QAChE,OAAO;YACL,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI;YACzC,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC;SACjC,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,40 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { CartCustomerContext, CartItemRecord, CartReadPort, CartRecord, CartSeedLine, CartWithItems, CartWritePort } from '@endora-commerce/contracts';
3
+ import { Cart } from '../entities/cart.entity.js';
4
+ import { CartItem } from '../entities/cart-item.entity.js';
5
+ import type { CartService } from './cart-service.js';
6
+ /**
7
+ * The row-level read model `carts` publishes, and the seed operation that goes
8
+ * with it (feature 075, Phase P).
9
+ *
10
+ * Six inbound sites are the same pair of queries — the customer's active cart,
11
+ * then its items — written out in `orders`' checkout, `orders`' reorder and
12
+ * `quote_requests`' quote-to-cart conversion. Two of those three then
13
+ * `em.create(Cart, …)` when the lookup misses and hand-build `CartItem` rows,
14
+ * which is two modules writing this module's two tables.
15
+ *
16
+ * `replaceItemsForCustomer` is what they call instead. The clear-then-seed
17
+ * rule it implements is the single-active-cart model admin order-create,
18
+ * reorder and RFQ conversion all already assume; it was spelled out twice, in
19
+ * two modules, and `lastActivityAt` was maintained in neither.
20
+ */
21
+ export declare class CartReadService implements CartReadPort {
22
+ #private;
23
+ private readonly emFactory;
24
+ constructor(emFactory: () => EntityManager);
25
+ findActiveForCustomer(ctx: CartCustomerContext): Promise<CartWithItems | null>;
26
+ findById(cartId: string): Promise<CartWithItems | null>;
27
+ }
28
+ /**
29
+ * The write port, adapting `CartService` and adding the seed operation.
30
+ *
31
+ * The service arrives as a getter so the adapter resolves it from the
32
+ * container per call rather than capturing it — it is this module's own gated
33
+ * port, and a singleton may not hold a gate.
34
+ */
35
+ export declare function createCartWritePort(emFactory: () => EntityManager, getService: () => CartService): CartWritePort;
36
+ export declare function toCartRecord(cart: Cart): CartRecord;
37
+ export declare function toCartItemRecord(item: CartItem): CartItemRecord;
38
+ /** Kept beside the mapper so `CartSeedLine`'s shape is checked where it is used. */
39
+ export type { CartSeedLine };
40
+ //# sourceMappingURL=cart-read-port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-read-port.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cart-read-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EACV,mBAAmB,EACnB,cAAc,EACd,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,aAAa,EACb,aAAa,EACd,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;;;;;;;;;;;;GAcG;AACH,qBAAa,eAAgB,YAAW,YAAY;;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,qBAAqB,CAAC,GAAG,EAAE,mBAAmB,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAc9E,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;CAW9D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,aAAa,EAC9B,UAAU,EAAE,MAAM,WAAW,GAC5B,aAAa,CAgDf;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,UAAU,CAuBnD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,GAAG,cAAc,CAkB/D;AAED,oFAAoF;AACpF,YAAY,EAAE,YAAY,EAAE,CAAC"}