@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,58 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { AdminCartsListQuery, AdminCartsListResponse, AdminCartDetailResponse, AdminCartAuditResponse, CatalogProductReadPort, CustomerAccountReadPort, OrganizationDetailsPort } from '@endora-commerce/contracts';
3
+ import type { CartAuditService } from './cart-audit-service.js';
4
+ import type { CartSnapshot, PromotionApplication } from '@endora-commerce/contracts';
5
+ /** Narrow port over the promotion engine for the admin cart-detail discount. */
6
+ export interface AdminCartPromotionPort {
7
+ applyToCart(snapshot: CartSnapshot): Promise<PromotionApplication>;
8
+ }
9
+ /**
10
+ * Platform-admin observability + emergency-reject surface for carts
11
+ * (feature 027 US6).
12
+ *
13
+ * - list(filter, sort, page) — paginated, filterable, sortable view
14
+ * across every cart on the platform.
15
+ * - getById(id) — read-only detail with line items, discount, and
16
+ * conversion lineage.
17
+ * - getAudit(id, page) — paginated cart-detail audit feed.
18
+ * - reject(id, adminUserId, reason) — terminal `Rejected` transition
19
+ * by a platform admin. Refuses on already-terminal carts.
20
+ *
21
+ * Capability gates are enforced at the route level via `requireAdmin('carts:read')`
22
+ * and `requireAdmin('carts:reject')`.
23
+ */
24
+ export declare class CartAdminService {
25
+ #private;
26
+ private readonly emFactory;
27
+ private readonly cartAuditService;
28
+ /**
29
+ * The three read models this surface renders names from (feature 075,
30
+ * Phase C). Every one of them was an `em.find` against another module's
31
+ * table, so an operator who switched `catalog` off still saw product
32
+ * names on the admin cart detail — out of rows deactivation leaves in
33
+ * place. `SalesChannel` is the kernel's and stays a direct read.
34
+ */
35
+ private readonly customerAccounts;
36
+ private readonly organizations;
37
+ private readonly catalogProducts;
38
+ /** Feature 045 (T043) — when wired, the detail shows the real discount. */
39
+ private readonly promotion?;
40
+ constructor(emFactory: () => EntityManager, cartAuditService: CartAuditService,
41
+ /**
42
+ * The three read models this surface renders names from (feature 075,
43
+ * Phase C). Every one of them was an `em.find` against another module's
44
+ * table, so an operator who switched `catalog` off still saw product
45
+ * names on the admin cart detail — out of rows deactivation leaves in
46
+ * place. `SalesChannel` is the kernel's and stays a direct read.
47
+ */
48
+ customerAccounts: CustomerAccountReadPort, organizations: OrganizationDetailsPort, catalogProducts: CatalogProductReadPort,
49
+ /** Feature 045 (T043) — when wired, the detail shows the real discount. */
50
+ promotion?: AdminCartPromotionPort | undefined);
51
+ list(query?: AdminCartsListQuery): Promise<AdminCartsListResponse>;
52
+ getById(cartId: string): Promise<AdminCartDetailResponse>;
53
+ getAudit(cartId: string, page?: number, pageSize?: number): Promise<AdminCartAuditResponse>;
54
+ reject(cartId: string, adminUserId: string, reason: string): Promise<AdminCartDetailResponse>;
55
+ private sortClause;
56
+ private itemCountsByCart;
57
+ }
58
+ //# sourceMappingURL=cart-admin-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-admin-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cart-admin-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EACV,mBAAmB,EACnB,sBAAsB,EACtB,uBAAuB,EACvB,sBAAsB,EACtB,sBAAsB,EAEtB,uBAAuB,EACvB,uBAAuB,EACxB,MAAM,4BAA4B,CAAC;AAOpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAErF,gFAAgF;AAChF,MAAM,WAAW,sBAAsB;IACrC,WAAW,CAAC,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CACpE;AAED;;;;;;;;;;;;;;GAcG;AAEH,qBAAa,gBAAgB;;IAEzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;gBAbV,SAAS,EAAE,MAAM,aAAa,EAC9B,gBAAgB,EAAE,gBAAgB;IACnD;;;;;;OAMG;IACc,gBAAgB,EAAE,uBAAuB,EACzC,aAAa,EAAE,uBAAuB,EACtC,eAAe,EAAE,sBAAsB;IACxD,2EAA2E;IAC1D,SAAS,CAAC,EAAE,sBAAsB,YAAA;IAkF/C,IAAI,CAAC,KAAK,GAAE,mBAAwB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAqFtE,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAsGzD,QAAQ,CACZ,MAAM,EAAE,MAAM,EACd,IAAI,SAAI,EACR,QAAQ,SAAK,GACZ,OAAO,CAAC,sBAAsB,CAAC;IAuC5B,MAAM,CACV,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,uBAAuB,CAAC;IAiCnC,OAAO,CAAC,UAAU;YAoBJ,gBAAgB;CAe/B"}
@@ -0,0 +1,397 @@
1
+ import { ERROR_CODES } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ import { orgConstraintFor } from '@endora-commerce/platform/tenancy';
4
+ import { Cart } from '../entities/cart.entity.js';
5
+ import { CartItem } from '../entities/cart-item.entity.js';
6
+ import { CartAuditEntry } from '../entities/cart-audit-entry.entity.js';
7
+ import { SalesChannel } from '@endora-commerce/platform/kernel';
8
+ /**
9
+ * Platform-admin observability + emergency-reject surface for carts
10
+ * (feature 027 US6).
11
+ *
12
+ * - list(filter, sort, page) — paginated, filterable, sortable view
13
+ * across every cart on the platform.
14
+ * - getById(id) — read-only detail with line items, discount, and
15
+ * conversion lineage.
16
+ * - getAudit(id, page) — paginated cart-detail audit feed.
17
+ * - reject(id, adminUserId, reason) — terminal `Rejected` transition
18
+ * by a platform admin. Refuses on already-terminal carts.
19
+ *
20
+ * Capability gates are enforced at the route level via `requireAdmin('carts:read')`
21
+ * and `requireAdmin('carts:reject')`.
22
+ */
23
+ export class CartAdminService {
24
+ emFactory;
25
+ cartAuditService;
26
+ customerAccounts;
27
+ organizations;
28
+ catalogProducts;
29
+ promotion;
30
+ constructor(emFactory, cartAuditService,
31
+ /**
32
+ * The three read models this surface renders names from (feature 075,
33
+ * Phase C). Every one of them was an `em.find` against another module's
34
+ * table, so an operator who switched `catalog` off still saw product
35
+ * names on the admin cart detail — out of rows deactivation leaves in
36
+ * place. `SalesChannel` is the kernel's and stays a direct read.
37
+ */
38
+ customerAccounts, organizations, catalogProducts,
39
+ /** Feature 045 (T043) — when wired, the detail shows the real discount. */
40
+ promotion) {
41
+ this.emFactory = emFactory;
42
+ this.cartAuditService = cartAuditService;
43
+ this.customerAccounts = customerAccounts;
44
+ this.organizations = organizations;
45
+ this.catalogProducts = catalogProducts;
46
+ this.promotion = promotion;
47
+ }
48
+ /**
49
+ * The organization restriction this surface must apply for itself.
50
+ *
51
+ * `Cart` is `@CustomerScoped`, and `customerFilterCond` contributes **no
52
+ * predicate at all** under `allowed-set` — the mode an assignment-scoped
53
+ * administrator resolves to — because a customer-account predicate cannot
54
+ * express an organization restriction. So for a `sales_representative` this
55
+ * entity behaves exactly as if it were `@GlobalEntity`, and every method
56
+ * below would answer for every cart on the platform.
57
+ *
58
+ * The reason given for that exemption is that customer-scoped entities carry
59
+ * no org column. `Cart` does carry one, so the constraint is expressible
60
+ * here, with the helper `derived-scope.ts` exists to provide and that
61
+ * `returns` already uses for the same reason at its own list.
62
+ *
63
+ * A cart with **no** organization is an anonymous or guest cart. It belongs
64
+ * to no organization, so it is outside the reach of an administrator whose
65
+ * authority is a set of organizations, and inside a platform administrator's.
66
+ * `$in` over the assigned ids expresses exactly that — SQL `in` never matches
67
+ * NULL — so the list needs no separate clause for it and the per-row check
68
+ * below states it explicitly.
69
+ */
70
+ #orgScopeWhere(requested) {
71
+ const constraint = orgConstraintFor();
72
+ if (constraint.kind === 'all') {
73
+ return requested ? { organizationId: requested } : {};
74
+ }
75
+ // The `single` arm's null branch matches nothing since D-178, and that is
76
+ // the resolution of feature 087's R-4 rather than an answer to it: R-4 asks
77
+ // which of two shipped semantics for "a single-org actor whose organisation
78
+ // is null" is correct — this one's "matches nothing" or `orgFilterCond`'s
79
+ // "matches null-org rows". There is no such actor any more, so both arms
80
+ // die and neither semantic has to win.
81
+ //
82
+ // That holds **because the tightening landed first**, and the condition
83
+ // does not travel separately from the claim. Feature 087's Group B adds a
84
+ // nullable `organization_id` to five tables and leaves ownerless rows in
85
+ // place; in the other order those rows would exist while the null-org actor
86
+ // still did, `orgFilterCond`'s `{ organizationId: null }` would start
87
+ // matching real data, and the two semantics would begin disagreeing about
88
+ // rows a buyer can see rather than about an empty set.
89
+ const allowed = constraint.kind === 'single'
90
+ ? constraint.organizationId === null
91
+ ? []
92
+ : [constraint.organizationId]
93
+ : [...constraint.organizationIds];
94
+ // A caller-supplied filter narrows within the scope; it never widens it,
95
+ // and asking for an organization outside the scope is answered as an empty
96
+ // page rather than as an error, so the filter is no existence oracle.
97
+ if (requested)
98
+ return allowed.includes(requested) ? { organizationId: requested } : 'refuse';
99
+ return { organizationId: { $in: allowed } };
100
+ }
101
+ /**
102
+ * Refuse a cart the caller's organizations do not reach.
103
+ *
104
+ * 404 rather than 403, and the same `cart_not_found` code the absent row
105
+ * gets: an out-of-scope cart must not be distinguishable from one that does
106
+ * not exist.
107
+ */
108
+ #assertCartInScope(cart) {
109
+ const constraint = orgConstraintFor();
110
+ if (constraint.kind === 'all')
111
+ return;
112
+ // The cart's own organisation is still nullable — an anonymous visitor's
113
+ // cart has none, and feature 087's Group B is what settles that. What is
114
+ // gone since D-178 is the other half of the pairing: a `single-org` *actor*
115
+ // whose organisation is null, which is why the `single` arm can no longer
116
+ // match one of those carts by having a null of its own.
117
+ const organizationId = cart.organizationId ?? null;
118
+ const allowed = constraint.kind === 'single'
119
+ ? constraint.organizationId === organizationId
120
+ : organizationId !== null && constraint.organizationIds.includes(organizationId);
121
+ if (!allowed) {
122
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'cart_not_found');
123
+ }
124
+ }
125
+ async list(query = {}) {
126
+ const em = this.emFactory();
127
+ const where = {};
128
+ if (query.status && query.status.length > 0)
129
+ where['status'] = { $in: query.status };
130
+ if (query.approvalStatus && query.approvalStatus.length > 0) {
131
+ where['approvalStatus'] = { $in: query.approvalStatus };
132
+ }
133
+ const orgWhere = this.#orgScopeWhere(query.organizationId);
134
+ if (orgWhere === 'refuse') {
135
+ const emptyPage = query.page ?? 1;
136
+ const emptySize = Math.min(query.pageSize ?? 50, 200);
137
+ return { data: [], meta: { page: emptyPage, pageSize: emptySize, totalCount: 0 } };
138
+ }
139
+ Object.assign(where, orgWhere);
140
+ if (query.customerAccountId)
141
+ where['customerAccountId'] = query.customerAccountId;
142
+ if (query.salesChannelId)
143
+ where['salesChannelId'] = query.salesChannelId;
144
+ if (query.lastActivityFrom || query.lastActivityTo) {
145
+ const range = {};
146
+ if (query.lastActivityFrom)
147
+ range['$gte'] = new Date(query.lastActivityFrom);
148
+ if (query.lastActivityTo)
149
+ range['$lte'] = new Date(query.lastActivityTo);
150
+ where['lastActivityAt'] = range;
151
+ }
152
+ const page = query.page ?? 1;
153
+ const pageSize = Math.min(query.pageSize ?? 50, 200);
154
+ const orderBy = this.sortClause(query.sort ?? 'last_activity_desc');
155
+ const [rows, totalCount] = await em.findAndCount(Cart, where, {
156
+ orderBy,
157
+ limit: pageSize,
158
+ offset: (page - 1) * pageSize,
159
+ });
160
+ // Resolve display names + counts in bulk to avoid N+1 lookups.
161
+ const cartIds = rows.map((r) => r.id);
162
+ const customerIds = Array.from(new Set(rows.map((r) => r.customerAccountId).filter((id) => Boolean(id))));
163
+ const orgIds = Array.from(new Set(rows.map((r) => r.organizationId).filter((id) => Boolean(id))));
164
+ const channelIds = Array.from(new Set(rows.map((r) => r.salesChannelId).filter((id) => Boolean(id))));
165
+ const [customers, organizations, channels, itemCounts] = await Promise.all([
166
+ this.customerAccounts.findByIds(customerIds),
167
+ this.organizations.findByIds(orgIds),
168
+ channelIds.length > 0
169
+ ? em.find(SalesChannel, { id: { $in: channelIds } })
170
+ : Promise.resolve([]),
171
+ this.itemCountsByCart(em, cartIds),
172
+ ]);
173
+ const customerById = new Map(customers.map((c) => [c.id, c]));
174
+ const orgById = new Map(organizations.map((o) => [o.id, o]));
175
+ const channelById = new Map(channels.map((c) => [c.id, c]));
176
+ return {
177
+ data: rows.map((cart) => {
178
+ const items = itemCounts.get(cart.id) ?? { count: 0, total: 0, currency: 'PLN' };
179
+ const cust = cart.customerAccountId ? customerById.get(cart.customerAccountId) : null;
180
+ const org = cart.organizationId ? orgById.get(cart.organizationId) : null;
181
+ const channel = cart.salesChannelId ? channelById.get(cart.salesChannelId) : null;
182
+ return {
183
+ id: cart.id,
184
+ ownerCustomerAccountId: cart.customerAccountId ?? null,
185
+ ownerDisplayName: cust ? (cust.email ?? cust.id) : null,
186
+ organizationId: cart.organizationId ?? null,
187
+ organizationDisplayName: org?.name ?? null,
188
+ salesChannelId: cart.salesChannelId ?? null,
189
+ salesChannelCode: channel?.code ?? null,
190
+ status: cart.status,
191
+ approvalStatus: cart.approvalStatus,
192
+ itemCount: items.count,
193
+ total: { amount: items.total, currency: items.currency },
194
+ appliedPromotionCode: cart.appliedPromotionCode ?? null,
195
+ lastActivityAt: cart.lastActivityAt.toISOString(),
196
+ createdAt: cart.createdAt.toISOString(),
197
+ };
198
+ }),
199
+ meta: { page, pageSize, totalCount },
200
+ };
201
+ }
202
+ async getById(cartId) {
203
+ const em = this.emFactory();
204
+ const cart = await em.findOne(Cart, { id: cartId });
205
+ if (!cart) {
206
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'cart_not_found');
207
+ }
208
+ this.#assertCartInScope(cart);
209
+ const items = await em.find(CartItem, { cartId });
210
+ const cust = cart.customerAccountId
211
+ ? await this.customerAccounts.findById(cart.customerAccountId)
212
+ : null;
213
+ const org = cart.organizationId
214
+ ? await this.organizations.findById(cart.organizationId)
215
+ : null;
216
+ const channel = cart.salesChannelId
217
+ ? await em.findOne(SalesChannel, { id: cart.salesChannelId })
218
+ : null;
219
+ const productIds = Array.from(new Set(items.map((it) => it.productId)));
220
+ const products = await this.catalogProducts.findByIds(productIds);
221
+ const productById = new Map(products.map((p) => [p.id, p]));
222
+ const subtotal = items.reduce((acc, it) => acc + Number(it.unitPrice) * it.quantity, 0);
223
+ const currency = items[0]?.currency ?? 'PLN';
224
+ // Feature 045 (T043) — compute the real discount for the detail view.
225
+ //
226
+ // No `catch` (issue #84). `applyToCart` returns `discountTotal: 0` when
227
+ // nothing applies, so zero is already an answer it can give; catching one
228
+ // made "no promotion matched" and "`promotions` is switched off"
229
+ // indistinguishable, and an administrator reading this panel would have
230
+ // quoted a customer the undiscounted total as if it were the real one.
231
+ let discountTotal = 0;
232
+ if (this.promotion && items.length > 0) {
233
+ const application = await this.promotion.applyToCart({
234
+ organizationId: cart.organizationId ?? null,
235
+ // Issue #177 — the owner's effective group, from the two records this
236
+ // handler has already loaded. A literal `null` here showed the operator
237
+ // an undiscounted total for every buyer a group-targeted promotion
238
+ // applies to, which is the figure they then quote to that buyer.
239
+ customerGroupId: cust?.customerGroupId ?? org?.customerGroupId ?? null,
240
+ currency,
241
+ lines: items.map((it) => ({
242
+ productId: it.productId,
243
+ variantId: it.variantId ?? null,
244
+ categoryIds: [],
245
+ quantity: it.quantity,
246
+ unitPrice: { amount: Number(it.unitPrice), currency: it.currency },
247
+ })),
248
+ deliveryTotal: 0,
249
+ promotionCode: cart.appliedPromotionCode ?? null,
250
+ salesChannelId: cart.salesChannelId ?? null,
251
+ });
252
+ discountTotal = application.discountTotal;
253
+ }
254
+ const total = Math.max(0, Math.round((subtotal - discountTotal) * 100) / 100);
255
+ return {
256
+ data: {
257
+ id: cart.id,
258
+ ownerCustomerAccountId: cart.customerAccountId ?? null,
259
+ ownerDisplayName: cust ? (cust.email ?? cust.id) : null,
260
+ organizationId: cart.organizationId ?? null,
261
+ organizationDisplayName: org?.name ?? null,
262
+ salesChannelId: cart.salesChannelId ?? null,
263
+ salesChannelCode: channel?.code ?? null,
264
+ status: cart.status,
265
+ approvalStatus: cart.approvalStatus,
266
+ itemCount: items.length,
267
+ total: { amount: total, currency },
268
+ appliedPromotionCode: cart.appliedPromotionCode ?? null,
269
+ lastActivityAt: cart.lastActivityAt.toISOString(),
270
+ createdAt: cart.createdAt.toISOString(),
271
+ items: items.map((it) => {
272
+ const product = productById.get(it.productId);
273
+ return {
274
+ id: it.id,
275
+ productId: it.productId,
276
+ productName: product ? anyLocaleName(product) : it.productId,
277
+ variantId: it.variantId ?? null,
278
+ quantity: it.quantity,
279
+ unitPrice: { amount: Number(it.unitPrice), currency: it.currency },
280
+ lineTotal: { amount: Number(it.unitPrice) * it.quantity, currency: it.currency },
281
+ };
282
+ }),
283
+ discount: discountTotal > 0
284
+ ? { code: cart.appliedPromotionCode ?? null, amount: discountTotal, currency }
285
+ : cart.appliedPromotionCode
286
+ ? { code: cart.appliedPromotionCode, amount: 0, currency }
287
+ : null,
288
+ convertedToQuoteRequestId: cart.convertedToQuoteRequestId ?? null,
289
+ submittedForApprovalAt: cart.submittedForApprovalAt?.toISOString() ?? null,
290
+ approvedAt: cart.approvedAt?.toISOString() ?? null,
291
+ approvedByCustomerAccountId: cart.approvedByCustomerAccountId ?? null,
292
+ rejectedAt: cart.rejectedAt?.toISOString() ?? null,
293
+ rejectedByActor: cart.rejectedByActor ?? null,
294
+ rejectedReason: cart.rejectedReason ?? null,
295
+ },
296
+ };
297
+ }
298
+ async getAudit(cartId, page = 1, pageSize = 50) {
299
+ const em = this.emFactory();
300
+ const cart = await em.findOne(Cart, { id: cartId });
301
+ if (!cart) {
302
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'cart_not_found');
303
+ }
304
+ this.#assertCartInScope(cart);
305
+ const capped = Math.min(pageSize, 200);
306
+ const [rows, totalCount] = await em.findAndCount(CartAuditEntry, { cartId }, { orderBy: { occurredAt: 'desc' }, limit: capped, offset: (page - 1) * capped });
307
+ const actorIds = Array.from(new Set(rows.map((r) => r.actorId).filter((id) => Boolean(id))));
308
+ const actors = await this.customerAccounts.findByIds(actorIds);
309
+ const actorById = new Map(actors.map((a) => [a.id, a]));
310
+ return {
311
+ data: rows.map((r) => ({
312
+ id: r.id,
313
+ occurredAt: r.occurredAt.toISOString(),
314
+ actorType: r.actorType,
315
+ actorId: r.actorId ?? null,
316
+ actorDisplayName: r.actorId
317
+ ? (actorById.get(r.actorId)?.email ?? r.actorId)
318
+ : null,
319
+ action: r.action,
320
+ fromState: r.fromState ?? null,
321
+ toState: r.toState ?? null,
322
+ reason: r.reason ?? null,
323
+ metadata: r.metadata,
324
+ })),
325
+ meta: { page, pageSize: capped, totalCount },
326
+ };
327
+ }
328
+ async reject(cartId, adminUserId, reason) {
329
+ const em = this.emFactory();
330
+ const cart = await em.findOne(Cart, { id: cartId });
331
+ if (!cart) {
332
+ throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'cart_not_found');
333
+ }
334
+ this.#assertCartInScope(cart);
335
+ if (cart.status === 'completed' || cart.status === 'rejected') {
336
+ throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'already_terminal');
337
+ }
338
+ const fromStatus = cart.status;
339
+ cart.status = 'rejected';
340
+ cart.rejectedAt = new Date();
341
+ cart.rejectedByActor = `admin:${adminUserId}`;
342
+ cart.rejectedReason = reason;
343
+ cart.lastActivityAt = new Date();
344
+ await em.flush();
345
+ await this.cartAuditService.record({
346
+ cartId: cart.id,
347
+ actorType: 'platform_admin',
348
+ actorId: adminUserId,
349
+ action: 'admin_rejected',
350
+ fromState: fromStatus,
351
+ toState: 'rejected',
352
+ reason,
353
+ });
354
+ return this.getById(cartId);
355
+ }
356
+ // Internal helpers ─────────────────────────────────────────────────────
357
+ sortClause(sort) {
358
+ switch (sort) {
359
+ case 'last_activity_asc':
360
+ return { lastActivityAt: 'asc' };
361
+ case 'total_desc':
362
+ case 'total_asc':
363
+ // The grand total is computed from cart_items at read time; the
364
+ // SQL-side approximation is `updatedAt` (a write-heavy proxy). A
365
+ // true total-sort needs a subquery — kept as a follow-up.
366
+ return { updatedAt: sort === 'total_desc' ? 'desc' : 'asc' };
367
+ case 'created_desc':
368
+ return { createdAt: 'desc' };
369
+ case 'last_activity_desc':
370
+ default:
371
+ return { lastActivityAt: 'desc' };
372
+ }
373
+ }
374
+ async itemCountsByCart(em, cartIds) {
375
+ const result = new Map();
376
+ if (cartIds.length === 0)
377
+ return result;
378
+ const items = await em.find(CartItem, { cartId: { $in: cartIds } });
379
+ for (const it of items) {
380
+ const prev = result.get(it.cartId) ?? { count: 0, total: 0, currency: it.currency };
381
+ prev.count += 1;
382
+ prev.total += Number(it.unitPrice) * it.quantity;
383
+ result.set(it.cartId, prev);
384
+ }
385
+ return result;
386
+ }
387
+ }
388
+ function anyLocaleName(product) {
389
+ if (!product.name || typeof product.name !== 'object')
390
+ return product.id;
391
+ const en = product.name['en-US'] ?? product.name['en'];
392
+ if (typeof en === 'string' && en.length > 0)
393
+ return en;
394
+ const first = Object.values(product.name).find((v) => typeof v === 'string' && v.length > 0);
395
+ return first ?? product.id;
396
+ }
397
+ //# sourceMappingURL=cart-admin-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-admin-service.js","sourceRoot":"","sources":["../../../src/backend/services/cart-admin-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,147 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { Cart } from '../entities/cart.entity.js';
3
+ import type { CustomerAccountReadPort, OrganizationCartApprovalWritePort, OrganizationDetailsPort, OrganizationRecord } from '@endora-commerce/contracts';
4
+ import type { CartAuditService } from './cart-audit-service.js';
5
+ /**
6
+ * Approval-workflow state machine (feature 027 US4).
7
+ *
8
+ * Lives orthogonal to the main `status` enum: any cart may carry an
9
+ * `approval_status` of `not_required` / `pending` / `approved` /
10
+ * `rejected_by_org_admin`. Transitions:
11
+ *
12
+ * not_required → pending (buyer submits, when org policy is on)
13
+ * pending → approved (org-admin approves)
14
+ * pending → rejected_by_org_admin (org-admin rejects; main
15
+ * status also flips to `rejected`)
16
+ * approved → pending (buyer mutates cart post-approval; re-arm)
17
+ * pending|approved → not_required (org turns the policy off)
18
+ *
19
+ * Self-approval exemption: a cart whose owner is themself an Org Admin
20
+ * is born `not_required` regardless of policy.
21
+ */
22
+ export interface ApproveActor {
23
+ customerAccountId: string;
24
+ organizationId: string;
25
+ }
26
+ /**
27
+ * Email-dispatch port (feature 027 T090). The carts module hands the
28
+ * transition event + actor + cart payload to the composition root,
29
+ * which is responsible for: (a) resolving recipients (org admins for
30
+ * submit, the buyer for approve / reject), (b) rendering the i18n
31
+ * template from the buyer / admin's preferred language, and (c)
32
+ * calling the platform Mailer. Implementations are expected to be
33
+ * fire-and-forget: a thrown error MUST NOT roll back the approval
34
+ * transition (it would be a UX regression to fail an approve because
35
+ * mail delivery hiccupped). Production composition wraps each method
36
+ * in a try / catch + structured log.
37
+ */
38
+ export interface CartEmailDispatch {
39
+ onSubmittedForApproval(input: {
40
+ cart: Cart;
41
+ submitter: ApproveActor;
42
+ }): Promise<void>;
43
+ onApproved(input: {
44
+ cart: Cart;
45
+ approver: ApproveActor;
46
+ }): Promise<void>;
47
+ onRejected(input: {
48
+ cart: Cart;
49
+ rejector: ApproveActor;
50
+ reason: string;
51
+ }): Promise<void>;
52
+ }
53
+ export declare class CartApprovalService {
54
+ private readonly emFactory;
55
+ private readonly cartAuditService;
56
+ /** `organizations`' read model — the approval policy and its owner row. */
57
+ private readonly organizations;
58
+ /**
59
+ * `organizations`' write surface for the policy column (issue #175). The
60
+ * two `setPolicy*` methods below used to assign
61
+ * `organizations.requires_cart_approval` on that module's entity; the write
62
+ * belongs to its owner, which audits it through the Command Bus. What stays
63
+ * here is the cascade over this module's own carts.
64
+ */
65
+ private readonly organizationCartApproval;
66
+ /** `customer_accounts`' read model — the submitting buyer's role. */
67
+ private readonly customerAccounts;
68
+ private readonly emailDispatch?;
69
+ constructor(emFactory: () => EntityManager, cartAuditService: CartAuditService,
70
+ /** `organizations`' read model — the approval policy and its owner row. */
71
+ organizations: OrganizationDetailsPort,
72
+ /**
73
+ * `organizations`' write surface for the policy column (issue #175). The
74
+ * two `setPolicy*` methods below used to assign
75
+ * `organizations.requires_cart_approval` on that module's entity; the write
76
+ * belongs to its owner, which audits it through the Command Bus. What stays
77
+ * here is the cascade over this module's own carts.
78
+ */
79
+ organizationCartApproval: OrganizationCartApprovalWritePort,
80
+ /** `customer_accounts`' read model — the submitting buyer's role. */
81
+ customerAccounts: CustomerAccountReadPort, emailDispatch?: CartEmailDispatch | undefined);
82
+ /**
83
+ * Fire-and-forget e-mail dispatch. Wraps each call so any throw is
84
+ * swallowed — approval transitions must complete even if mail
85
+ * delivery hiccups. Production composition is responsible for
86
+ * surfacing the failure through the platform's log + alerting paths.
87
+ */
88
+ private dispatch;
89
+ /**
90
+ * Buyer submits the cart for Org-Admin approval. Refuses if:
91
+ * - the cart is empty
92
+ * - the policy is off (approval not required)
93
+ * - the buyer is themself an Org Admin (self-approval exemption)
94
+ * - approval_status is already non-`not_required`
95
+ */
96
+ submitForApproval(cart: Cart, actor: ApproveActor): Promise<Cart>;
97
+ /** Org Admin approves a pending cart. */
98
+ approve(cartId: string, actor: ApproveActor): Promise<Cart>;
99
+ /** Org Admin rejects a pending cart with a reason. */
100
+ reject(cartId: string, actor: ApproveActor, reason: string): Promise<Cart>;
101
+ /**
102
+ * The current policy, for a platform admin (feature 091, P7b;
103
+ * `contracts/admin-component-contribution.md` §10.5).
104
+ *
105
+ * The read half of {@link setPolicyByAdmin}, added because the admin
106
+ * contribution that renders the toggle has to know what to render it in.
107
+ * Until P7b that value crossed as a prop — `initialRequiresCartApproval`, out
108
+ * of `organizations`' detail payload — and a zone's props may not carry it:
109
+ * the other three contributors to `organization.detail.after` want neither
110
+ * the prop nor the request behind it.
111
+ *
112
+ * It reads through `organizations`' own port rather than this module's
113
+ * tables, which is the direction the write has taken since issue #175: the
114
+ * column belongs to that module.
115
+ */
116
+ getPolicyByAdmin(organizationId: string): Promise<OrganizationRecord>;
117
+ /**
118
+ * Platform-admin variant of {@link setPolicyForOrganization}. Same
119
+ * semantics (policy-off cascades a reset of every pending/approved
120
+ * cart in the Org) but the actor is a platform admin rather than an
121
+ * Org Admin — recorded as `actorType: 'platform_admin'` in the
122
+ * audit row.
123
+ */
124
+ setPolicyByAdmin(organizationId: string, adminUserId: string, requires: boolean): Promise<OrganizationRecord>;
125
+ /**
126
+ * Org Admin toggles the per-Organization `requires_cart_approval` policy.
127
+ * On `false`: every `pending` or `approved` cart in the Org returns to
128
+ * `not_required` and is audited as `approval_policy_reset`.
129
+ */
130
+ setPolicyForOrganization(organizationId: string, actor: ApproveActor, requires: boolean): Promise<OrganizationRecord>;
131
+ /**
132
+ * The half of a policy switch-off that belongs to this module: every
133
+ * `pending` or `approved` cart in the Organization returns to
134
+ * `not_required`, audited as `approval_policy_reset` in this module's own
135
+ * per-cart trail with the actor that switched the policy off.
136
+ */
137
+ private resetPendingApprovals;
138
+ /**
139
+ * Re-arm hook: any buyer-driven mutation on an `approved` cart drops
140
+ * it back to `pending`. Called by CartService.{addItem,updateItem,
141
+ * removeItem} and by CartCouponService.{apply,clear}.
142
+ */
143
+ maybeReArm(cart: Cart, actor: {
144
+ customerAccountId?: string;
145
+ }): Promise<void>;
146
+ }
147
+ //# sourceMappingURL=cart-approval-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cart-approval-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cart-approval-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAGlD,OAAO,KAAK,EACV,uBAAuB,EACvB,iCAAiC,EACjC,uBAAuB,EACvB,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,WAAW,YAAY;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IAChC,sBAAsB,CAAC,KAAK,EAAE;QAC5B,IAAI,EAAE,IAAI,CAAC;QACX,SAAS,EAAE,YAAY,CAAC;KACzB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,UAAU,CAAC,KAAK,EAAE;QAChB,IAAI,EAAE,IAAI,CAAC;QACX,QAAQ,EAAE,YAAY,CAAC;KACxB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,UAAU,CAAC,KAAK,EAAE;QAChB,IAAI,EAAE,IAAI,CAAC;QACX,QAAQ,EAAE,YAAY,CAAC;QACvB,MAAM,EAAE,MAAM,CAAC;KAChB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB;AAED,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,wBAAwB;IACzC,qEAAqE;IACrE,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC;gBAdd,SAAS,EAAE,MAAM,aAAa,EAC9B,gBAAgB,EAAE,gBAAgB;IACnD,2EAA2E;IAC1D,aAAa,EAAE,uBAAuB;IACvD;;;;;;OAMG;IACc,wBAAwB,EAAE,iCAAiC;IAC5E,qEAAqE;IACpD,gBAAgB,EAAE,uBAAuB,EACzC,aAAa,CAAC,EAAE,iBAAiB,YAAA;IAGpD;;;;;OAKG;YACW,QAAQ;IAStB;;;;;;OAMG;IACG,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IA2CvE,yCAAyC;IACnC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BjE,sDAAsD;IAChD,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+BhF;;;;;;;;;;;;;;OAcG;IACG,gBAAgB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAQ3E;;;;;;OAMG;IACG,gBAAgB,CACpB,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,OAAO,GAChB,OAAO,CAAC,kBAAkB,CAAC;IAS9B;;;;OAIG;IACG,wBAAwB,CAC5B,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,YAAY,EACnB,QAAQ,EAAE,OAAO,GAChB,OAAO,CAAC,kBAAkB,CAAC;IAY9B;;;;;OAKG;YACW,qBAAqB;IAyBnC;;;;OAIG;IACG,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE;QAAE,iBAAiB,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAenF"}