@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
package/docs/carts.md ADDED
@@ -0,0 +1,232 @@
1
+ ---
2
+ title: carts
3
+ description: Customer shopping cart with anonymous→logged-in merge
4
+ ---
5
+
6
+ # `carts`
7
+
8
+ Anonymous and customer-bound shopping carts. An anonymous cart is identified
9
+ by a long-lived cookie token; on login it merges into the Customer's cart
10
+ deterministically.
11
+
12
+ The carts consolidation of May 2026 extended the foundation cart
13
+ module with a full lifecycle (`active` / `abandoned` / `completed` /
14
+ `rejected`), an orthogonal approval sub-state, sales-channel scoping,
15
+ last-activity bookkeeping, coupon application against the Promotions module,
16
+ up-sell suggestions, three conversions (Cart ↔ Quote Request, Shopping List
17
+ → Cart), Organization-Administrator visibility + approval gate, an
18
+ abandonment sweep, and a platform-admin observability surface.
19
+
20
+ ## Lifecycle
21
+
22
+ ```text
23
+ ┌──────────────────────────────────────────────────────┐
24
+ │ │
25
+ [ active / not_required ] ──(qty/add/remove/coupon)──┐ │
26
+ │ ▼ │
27
+ │ [ active / pending ] │
28
+ │ │ │
29
+ │ ├──(org-admin │
30
+ │ │ approve)──▶ │
31
+ │ │ [ active / │
32
+ │ │ approved ] │
33
+ │ │ │ │
34
+ │ ▼ ▼ │
35
+ │ (org-admin reject) [ rejected / rejected_by_org_admin ]
36
+ │
37
+ ▼
38
+ [ abandoned ] ◀──(sweep: last_activity_at < threshold)── [ active / * ]
39
+ │ ▲
40
+ │ │
41
+ └────────────────(any buyer activity)──────────────────┘
42
+
43
+ [ active / approved ] ──(checkout success)──▶ [ completed / approved ]
44
+ [ active / * ] ──(Cart→QR conversion)─▶ [ completed / *; converted_to_quote_request_id set ]
45
+ ```
46
+
47
+ Re-arm rule: any buyer-driven mutation (add/remove/quantity/coupon) on an
48
+ `approved` cart silently drops `approval_status` back to `pending`.
49
+
50
+ Self-approval exemption: a cart created by an Organization Administrator
51
+ is born with `approval_status='not_required'` regardless of the per-Org
52
+ policy flag.
53
+
54
+ ## Public surface
55
+
56
+ ### Storefront (buyer)
57
+
58
+ | Verb + Path | Purpose |
59
+ | --- | --- |
60
+ | `GET /api/v1/cart` | Active cart (lazy-create); the payload includes `status`, `approvalStatus`, `salesChannelId`, `grandTotal`, `discount`, `primaryCta`, `droppedLines`, `couponDroppedThisRead`, `lastActivityAt` |
61
+ | `POST /api/v1/cart/items` | Add line item (200-line cap enforced) |
62
+ | `PATCH /api/v1/cart/items/:itemId` | Update quantity (accepts `0` to delete) |
63
+ | `DELETE /api/v1/cart/items/:itemId` | Remove line |
64
+ | `POST /api/v1/cart/items/:itemId/save-to-shopping-list` | Push line to a Purchase List |
65
+ | `POST /api/v1/cart/touch` | Explicit page-open ping; bumps `last_activity_at` and reactivates an abandoned cart |
66
+ | `GET /api/v1/cart/upsells?limit=N` | Up-sell strip from Catalog `product_links` (kind = `up_sell`) |
67
+ | `POST /api/v1/cart/coupon` | Apply (or replace, or clear) the active coupon code; 422 `CART_COUPON_REJECTED` with reason on rejection |
68
+ | `DELETE /api/v1/cart/coupon` | Alias for `POST {code: null}` |
69
+ | `POST /api/v1/cart/convert-to-quote-request` | Cart → Quote Request; source cart becomes `completed` with `converted_to_quote_request_id` set |
70
+ | `POST /api/v1/cart/from-quote-request/:qrId` | Quote Request → Cart; lines re-priced from the customer's current price list |
71
+ | `POST /api/v1/cart/from-shopping-list/:listId` | Shopping List → Cart; lines re-priced |
72
+
73
+ ### Storefront (Organization Administrator)
74
+
75
+ | Verb + Path | Purpose |
76
+ | --- | --- |
77
+ | `GET /api/v1/organization/carts` | List every cart in the caller's Organization |
78
+ | `GET /api/v1/organization/carts/:id` | Detail (member-owned cart) |
79
+ | `PATCH /api/v1/organization/policies/cart-approval` | Toggle `requires_cart_approval` for the Organization |
80
+ | `POST /api/v1/cart/submit-for-approval` | Buyer submits cart for Org-Admin approval (no-op when policy off; refused for Org-Admins themselves) |
81
+ | `POST /api/v1/organization/carts/:id/approve` | Approve a pending cart |
82
+ | `POST /api/v1/organization/carts/:id/reject` | Reject a pending cart with a required reason |
83
+
84
+ Role gate: `CustomerAccount.role === 'organization_admin'` for every
85
+ `organization/*` route. Foreign-organization carts return 404
86
+ (anti-enumeration symmetry).
87
+
88
+ ### Admin
89
+
90
+ | Verb + Path | Capability | Purpose |
91
+ | --- | --- | --- |
92
+ | `GET /api/v1/admin/carts` | `carts:read` | Paginated platform-wide list (status / approvalStatus / org / customer / channel / date filters) |
93
+ | `GET /api/v1/admin/carts/:id` | `carts:read` | Read-only cart detail |
94
+ | `GET /api/v1/admin/carts/:id/audit` | `carts:read` | Cart-detail audit feed |
95
+ | `POST /api/v1/admin/carts/:id/reject` | `carts:reject` | Emergency terminal reject (requires reason) |
96
+
97
+ ## Entities
98
+
99
+ - `Cart` (one active per `(customer_account_id, sales_channel_id)`; partial
100
+ unique index enforces this in PostgreSQL).
101
+ - `CartItem` (per-line with `unit_price` snapshot at add + cached
102
+ `recomputed_*` columns).
103
+ - `CartAuditEntry` (typed per-cart feed; indexed on `(cart_id, occurred_at
104
+ DESC)`). Every state transition also lands a row in the existing
105
+ `audit_log_entries` table via `AuditPort` for the platform-wide
106
+ audit timeline.
107
+
108
+ ## Settings
109
+
110
+ | Key | Default | Description |
111
+ | --- | --- | --- |
112
+ | `carts.abandonment.inactivity_minutes` | `10080` (7 days) | Minutes of inactivity before an `active` cart is considered `abandoned`. `0` disables the sweep. |
113
+ | `carts.abandonment.notification_recipient` | `""` (empty) | Single e-mail address to notify on abandonment. Empty = no notification e-mail (the status flip still occurs). |
114
+
115
+ Seeded by the module-lifecycle ManifestReconciler on backend boot (see
116
+ `packages/modules/carts/src/manifest.ts`).
117
+
118
+ ## Merge-on-login
119
+
120
+ `cart-service.ts#mergeAnonymousIntoCustomer()` is invoked by the
121
+ `organizations` module's `onLogin` hook. Conflicting line items combine
122
+ quantities; the anonymous cart cookie is invalidated atomically. The source
123
+ anonymous cart's `status` flips to `'completed'` (not `'abandoned'`) so the
124
+ sweep does not later fire a stray abandonment notification for it.
125
+
126
+ ## Abandonment sweep
127
+
128
+ `cart-abandonment-worker.ts` exposes a plain async `sweep(now?)` mirroring
129
+ the `RfqExpiryWorker` pattern. The sweep:
130
+
131
+ 1. Reads `carts.abandonment.inactivity_minutes` (≤ 0 disables).
132
+ 2. Selects `active` carts whose `last_activity_at < now - threshold` AND
133
+ `abandonment_notified_at IS NULL` (idempotency).
134
+ 3. Flips status to `abandoned`, stamps `abandonment_notified_at = now`.
135
+ 4. Writes one `cart_audit_entries.abandonment_swept` row per affected cart.
136
+ 5. Dispatches an e-mail to the configured recipient **only** for non-empty
137
+ carts (empty carts flip status but suppress the notification per the
138
+ spec edge case).
139
+ 6. Notification dispatch failures do not roll back the status flip.
140
+
141
+ Reactivation: any buyer activity (touch, add/remove/qty/coupon) on an
142
+ `abandoned` cart returns it to `active` and clears
143
+ `abandonment_notified_at` so the next abandonment cycle can re-notify.
144
+
145
+ ## Conversions
146
+
147
+ - **Cart → Quote Request**: `RfqService.createForCustomer` is reused; the
148
+ cart's buyer-snapshotted unit prices are carried over as
149
+ `desired_unit_price` so sales sees the cart's reference price. The source
150
+ cart's items are deleted and the cart flips to `'completed'` with
151
+ `converted_to_quote_request_id` set.
152
+ - **Quote Request → Cart**: iterates the QR's items through
153
+ `CartService.addItem`, which already routes through `PricingService` for
154
+ re-pricing. The QR's `desired_unit_price` is intentionally dropped — the
155
+ buyer's current contractual prices are the source of truth. Unavailable /
156
+ no-price / not-purchasable lines are skipped and returned in
157
+ `droppedLines[]` with typed reasons.
158
+ - **Shopping List → Cart**: delegated to the existing
159
+ `ShoppingListService.convertToCart`; the carts module
160
+ exposes a port that composition wires to it.
161
+
162
+ ## Coupon application
163
+
164
+ The carts module never owns promotion logic — it plumbs the buyer's coupon
165
+ code through `PromotionService.applyToCart(snapshot)`. The
166
+ `CartCouponService.apply(cart, code)` method:
167
+
168
+ 1. Looks up the named `Promotion` row.
169
+ 2. Runs the local pre-checks (existence, validity window, org match, min
170
+ cart subtotal) and emits a typed `CouponDropReason` on the first failure.
171
+ 3. If pre-checks pass, calls `applyToCart` and verifies the named promotion
172
+ is in the returned `appliedPromotions[]`.
173
+ 4. On success: writes `cart.applied_promotion_code = code`, audits
174
+ `coupon_applied`, re-arms approval if needed.
175
+ 5. On any failure: returns `{outcome: 'dropped', reason, shortfall?}` and
176
+ does not change the cart.
177
+
178
+ `reevaluateOnRead(cart, items)` is intended to be called from the GET-cart
179
+ read path so a previously-applied code that no longer fits (e.g. cart fell
180
+ below min spend after a line removal) is silently dropped and the response
181
+ surfaces `couponDroppedThisRead`. Wiring into the read path is a follow-up.
182
+
183
+ ## Extension points
184
+
185
+ - **Re-pricing-on-read** — `CartPricingRecompute` + `CartRecomputeCache`
186
+ (30 s Redis TTL) exist and are tested but not yet wired into `GET
187
+ /api/v1/cart`; the snapshotted `unit_price` is returned today.
188
+ - **Email dispatch** — submit-for-approval / approve / reject / abandonment
189
+ e-mails are scaffolded in the service surfaces; dedicated templates are a
190
+ follow-up.
191
+ - **Abandonment scheduler** — the worker is constructed in production
192
+ composition but not yet wired to a cron / BullMQ schedule. The ops CLI
193
+ `pnpm --filter backend run cart:abandonment-sweep` runs one tick by hand.
194
+ It is a manifest-declared command the host runs, so it sweeps through the
195
+ composition's own `cartAbandonmentWorker` — see
196
+ `packages/modules/carts/src/backend/cli/abandonment-sweep.ts` for the body and
197
+ the `cliCommands` export in `packages/modules/carts/src/manifest.ts` for the
198
+ declaration.
199
+
200
+ ## Audit retention
201
+
202
+ `cart_audit_entries` is the **per-cart action log** (line added/removed,
203
+ quantity changed, coupon applied/cleared/dropped, approval submitted /
204
+ approved / rejected, sweep marked the cart abandoned, conversion to /
205
+ from a Quote Request, conversion from a Shopping List). Every row also
206
+ lands in the platform-wide `audit_log_entries` table via
207
+ `CartAuditService.record`, so the carts table is the canonical
208
+ denormalised view used by the Org-Admin and platform-admin "Cart history"
209
+ panel.
210
+
211
+ Retention policy:
212
+
213
+ - **Never auto-purge `cart_audit_entries`.** Org admins and the platform
214
+ admin rely on a complete history to defend approval decisions and
215
+ reproduce buyer disputes. The table is append-only — services never
216
+ `UPDATE` or `DELETE` rows.
217
+ - **Cart deletion cascade.** `cart_audit_entries.cart_id` is `ON DELETE
218
+ CASCADE`. We do not currently delete carts in production; if a future
219
+ GDPR / right-to-erasure workflow ever does, the audit trail follows.
220
+ When that workflow lands, mirror the carts row plus its audit entries
221
+ into a long-term audit-only archive table *before* the cascade fires —
222
+ do not silently lose history.
223
+ - **Production data-purges must exclude `cart_audit_entries`.** Any
224
+ scheduled job that prunes carts, customer accounts, or organizations
225
+ for storage hygiene MUST either skip `cart_audit_entries` or archive
226
+ it first. Reviewers: add this table to the exclusion list in
227
+ `packages/modules/audit_logs/src/backend/retention-policy.ts` when that policy
228
+ is introduced.
229
+ - **The platform-wide `audit_log_entries` table follows the audit_logs
230
+ module's retention policy** (controlled outside this feature). The
231
+ carts-side mirror in `cart_audit_entries` is the source of truth for
232
+ the carts UI even when the platform-wide table has aged out.
package/i18n/en.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "errors.CART_EMPTY": "Your cart is empty.",
3
+ "errors.CART_LINE_CAP_EXCEEDED": "You have reached the maximum number of lines on this cart.",
4
+ "errors.CART_COUPON_REJECTED": "This coupon code could not be applied.",
5
+ "errors.CART_COUPON_REJECTED.invalid_code": "This coupon code is invalid.",
6
+ "errors.CART_COUPON_REJECTED.expired": "This coupon code has expired.",
7
+ "errors.CART_COUPON_REJECTED.below_min_spend": "Add more to your cart to unlock this promotion.",
8
+ "errors.CART_COUPON_REJECTED.wrong_channel": "This coupon does not apply in this sales channel.",
9
+ "errors.CART_COUPON_REJECTED.wrong_customer_group": "This coupon is not available for your customer group.",
10
+ "errors.CART_COUPON_REJECTED.wrong_organization": "This coupon is not available for your organization.",
11
+ "errors.CART_COUPON_REJECTED.coupon_format_invalid": "Coupon codes use only uppercase letters, digits, '-' and '_'.",
12
+
13
+ "lines.unavailable.out_of_stock": "Out of stock.",
14
+ "lines.unavailable.not_purchasable": "No longer available.",
15
+ "lines.unavailable.no_price_in_customer_list": "Pricing unavailable for your account.",
16
+ "lines.qty.label": "Quantity",
17
+ "lines.unit_price.label": "Unit price",
18
+ "lines.line_total.label": "Line total",
19
+ "lines.remove.label": "Remove",
20
+ "lines.save_to_list.label": "Save to shopping list",
21
+
22
+ "totals.subtotal": "Subtotal",
23
+ "totals.discount": "Discount",
24
+ "totals.grand_total": "Total",
25
+ "totals.empty": "Your cart is empty.",
26
+
27
+ "minicart.heading": "Cart",
28
+ "minicart.empty": "Your cart is empty.",
29
+ "minicart.view_full": "View cart",
30
+ "minicart.checkout": "Checkout",
31
+ "minicart.item_count_one": "{count} item",
32
+ "minicart.item_count_other": "{count} items",
33
+
34
+ "dropped.not_purchasable": "Removed because it is no longer available.",
35
+ "dropped.out_of_stock": "Removed because it is out of stock.",
36
+ "dropped.no_price_in_customer_list": "Removed because pricing is unavailable for your account.",
37
+ "dropped.removed_by_conversion": "Skipped during conversion.",
38
+ "dropped.banner.heading": "Some lines were skipped",
39
+ "dropped.banner.intro": "These products were not added because:",
40
+
41
+ "cta.checkout": "Proceed to checkout",
42
+ "cta.submit_for_approval": "Submit for approval",
43
+ "cta.awaiting_approval": "Awaiting approval",
44
+ "cta.blocked_by_organization": "Checkout unavailable",
45
+ "cta.convert_to_qr": "Convert to quote request",
46
+ "cta.convert_to_qr.hint": "Send this cart to sales as a quote request.",
47
+ "cta.convert_from_qr": "Convert to cart",
48
+ "cta.convert_from_qr.hint": "Start a new cart pre-filled from this quote request.",
49
+ "cta.add_list_to_cart": "Add list to cart",
50
+ "cta.add_list_to_cart.hint": "Pre-fill the cart with the items from this list.",
51
+ "cta.save_to_list": "Save to a shopping list",
52
+ "cta.apply_coupon": "Apply coupon",
53
+ "cta.clear_coupon": "Remove coupon",
54
+
55
+ "coupon.placeholder": "Enter coupon code",
56
+ "coupon.applied": "Coupon {code} is active.",
57
+ "coupon.dropped_on_read": "The coupon {code} is no longer valid for this cart and has been removed.",
58
+
59
+ "conversion.banner.qr_created": "We've sent your cart to sales as a new quote request.",
60
+ "conversion.banner.qr_converted": "We've started a new cart from quote request {reference}.",
61
+ "conversion.banner.list_added": "We've added the contents of \"{listName}\" to your cart.",
62
+ "conversion.banner.lines_dropped_during_conversion": "Some lines were skipped during conversion — see the panel below.",
63
+
64
+ "approval.banner.pending": "Your cart is awaiting approval by an organization administrator.",
65
+ "approval.banner.approved": "Your cart has been approved and is ready for checkout.",
66
+ "approval.banner.rejected": "Your cart was rejected by an organization administrator.",
67
+ "approval.banner.policy_on": "Your organization now requires cart approval before checkout.",
68
+ "approval.banner.policy_off": "Cart approval is no longer required by your organization.",
69
+ "approval.reject_reason.prompt": "Why are you rejecting this cart?",
70
+ "approval.reject_reason.placeholder": "Short explanation visible to the buyer",
71
+ "approval.reject_reason.required": "Please provide a rejection reason.",
72
+
73
+ "approval.email.submit.subject": "A cart was submitted for your approval",
74
+ "approval.email.submit.body": "{submitterDisplayName} submitted a cart for approval. Visit your dashboard to review or reject it.",
75
+ "approval.email.approve.subject": "Your cart was approved",
76
+ "approval.email.approve.body": "Your cart was approved by {approverDisplayName} and is ready for checkout.",
77
+
78
+ "abandonment.email.subject": "A cart was abandoned",
79
+ "abandonment.email.body": "A cart belonging to {ownerDisplayName} has been inactive for the configured threshold and has been marked Abandoned.",
80
+
81
+ "reject.email.subject": "Your cart was rejected",
82
+ "reject.email.body": "An administrator rejected your cart. Reason: {reason}",
83
+
84
+ "upsells.heading": "You might also need",
85
+ "upsells.empty": "No suggestions for this cart.",
86
+ "policy.title": "Cart approval",
87
+ "policy.description": "When this is on, ordinary members of this organization must submit their cart for an Organization Administrator's approval before they can check out.",
88
+ "policy.on": "Approval required before checkout",
89
+ "policy.off": "No approval required",
90
+ "policy.hint": "Turning the policy off returns every pending or approved cart in this organization to “not required”, and each change is recorded in the audit trail.",
91
+ "policy.saving": "Saving…",
92
+ "policy.turnedOn": "Cart approval is now required for this organization.",
93
+ "policy.turnedOff": "Cart approval is no longer required; pending carts were released.",
94
+ "policy.loadError": "Failed to load the cart-approval policy.",
95
+ "policy.saveError": "Failed to update the policy."
96
+ }
package/i18n/pl.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "errors.CART_EMPTY": "Twój koszyk jest pusty.",
3
+ "errors.CART_LINE_CAP_EXCEEDED": "Osiągnięto maksymalną liczbę pozycji w koszyku.",
4
+ "errors.CART_COUPON_REJECTED": "Nie udało się zastosować kodu kuponu.",
5
+ "errors.CART_COUPON_REJECTED.invalid_code": "Nieprawidłowy kod kuponu.",
6
+ "errors.CART_COUPON_REJECTED.expired": "Kod kuponu wygasł.",
7
+ "errors.CART_COUPON_REJECTED.below_min_spend": "Dodaj więcej produktów, aby odblokować tę promocję.",
8
+ "errors.CART_COUPON_REJECTED.wrong_channel": "Ten kupon nie obowiązuje w tym kanale sprzedaży.",
9
+ "errors.CART_COUPON_REJECTED.wrong_customer_group": "Ten kupon nie jest dostępny dla Twojej grupy klientów.",
10
+ "errors.CART_COUPON_REJECTED.wrong_organization": "Ten kupon nie jest dostępny dla Twojej organizacji.",
11
+ "errors.CART_COUPON_REJECTED.coupon_format_invalid": "Kody kuponów mogą zawierać tylko duże litery, cyfry, '-' i '_'.",
12
+
13
+ "lines.unavailable.out_of_stock": "Brak na stanie.",
14
+ "lines.unavailable.not_purchasable": "Niedostępny.",
15
+ "lines.unavailable.no_price_in_customer_list": "Brak ceny dla Twojego konta.",
16
+ "lines.qty.label": "Ilość",
17
+ "lines.unit_price.label": "Cena jednostkowa",
18
+ "lines.line_total.label": "Wartość pozycji",
19
+ "lines.remove.label": "Usuń",
20
+ "lines.save_to_list.label": "Zapisz na liście zakupowej",
21
+
22
+ "totals.subtotal": "Suma częściowa",
23
+ "totals.discount": "Rabat",
24
+ "totals.grand_total": "Razem",
25
+ "totals.empty": "Twój koszyk jest pusty.",
26
+
27
+ "minicart.heading": "Koszyk",
28
+ "minicart.empty": "Twój koszyk jest pusty.",
29
+ "minicart.view_full": "Otwórz koszyk",
30
+ "minicart.checkout": "Do kasy",
31
+ "minicart.item_count_one": "{count} pozycja",
32
+ "minicart.item_count_other": "{count} pozycji",
33
+
34
+ "dropped.not_purchasable": "Pominięto — produkt jest niedostępny.",
35
+ "dropped.out_of_stock": "Pominięto — produkt jest niedostępny w magazynie.",
36
+ "dropped.no_price_in_customer_list": "Pominięto — brak ceny dla Twojego konta.",
37
+ "dropped.removed_by_conversion": "Pominięto podczas konwersji.",
38
+ "dropped.banner.heading": "Niektóre pozycje zostały pominięte",
39
+ "dropped.banner.intro": "Następujące produkty nie zostały dodane, ponieważ:",
40
+
41
+ "cta.checkout": "Przejdź do kasy",
42
+ "cta.submit_for_approval": "Zgłoś do akceptacji",
43
+ "cta.awaiting_approval": "Oczekuje na akceptację",
44
+ "cta.blocked_by_organization": "Kasa niedostępna",
45
+ "cta.convert_to_qr": "Zamień na zapytanie ofertowe",
46
+ "cta.convert_to_qr.hint": "Wyślij ten koszyk do działu sprzedaży jako zapytanie ofertowe.",
47
+ "cta.convert_from_qr": "Zamień na koszyk",
48
+ "cta.convert_from_qr.hint": "Utwórz nowy koszyk wypełniony pozycjami z tego zapytania ofertowego.",
49
+ "cta.add_list_to_cart": "Dodaj listę do koszyka",
50
+ "cta.add_list_to_cart.hint": "Wypełnij koszyk pozycjami z tej listy.",
51
+ "cta.save_to_list": "Zapisz na liście zakupowej",
52
+ "cta.apply_coupon": "Zastosuj kupon",
53
+ "cta.clear_coupon": "Usuń kupon",
54
+
55
+ "coupon.placeholder": "Wpisz kod kuponu",
56
+ "coupon.applied": "Kupon {code} jest aktywny.",
57
+ "coupon.dropped_on_read": "Kupon {code} nie obowiązuje już dla tego koszyka i został usunięty.",
58
+
59
+ "conversion.banner.qr_created": "Twój koszyk został wysłany do działu sprzedaży jako nowe zapytanie ofertowe.",
60
+ "conversion.banner.qr_converted": "Utworzono nowy koszyk na podstawie zapytania ofertowego {reference}.",
61
+ "conversion.banner.list_added": "Zawartość listy \"{listName}\" została dodana do koszyka.",
62
+ "conversion.banner.lines_dropped_during_conversion": "Niektóre pozycje zostały pominięte podczas konwersji — zobacz panel poniżej.",
63
+
64
+ "approval.banner.pending": "Twój koszyk oczekuje na akceptację administratora organizacji.",
65
+ "approval.banner.approved": "Twój koszyk został zaakceptowany i jest gotowy do realizacji.",
66
+ "approval.banner.rejected": "Twój koszyk został odrzucony przez administratora organizacji.",
67
+ "approval.banner.policy_on": "Twoja organizacja wymaga teraz akceptacji koszyka przed realizacją.",
68
+ "approval.banner.policy_off": "Twoja organizacja nie wymaga już akceptacji koszyka.",
69
+ "approval.reject_reason.prompt": "Dlaczego odrzucasz ten koszyk?",
70
+ "approval.reject_reason.placeholder": "Krótkie wyjaśnienie widoczne dla kupującego",
71
+ "approval.reject_reason.required": "Podaj powód odrzucenia.",
72
+
73
+ "approval.email.submit.subject": "Koszyk zgłoszony do Twojej akceptacji",
74
+ "approval.email.submit.body": "{submitterDisplayName} zgłosił(a) koszyk do akceptacji. Otwórz panel, aby go zaakceptować lub odrzucić.",
75
+ "approval.email.approve.subject": "Twój koszyk został zaakceptowany",
76
+ "approval.email.approve.body": "Twój koszyk został zaakceptowany przez {approverDisplayName} i jest gotowy do realizacji.",
77
+
78
+ "abandonment.email.subject": "Porzucony koszyk",
79
+ "abandonment.email.body": "Koszyk klienta {ownerDisplayName} został oznaczony jako Porzucony po przekroczeniu progu nieaktywności.",
80
+
81
+ "reject.email.subject": "Twój koszyk został odrzucony",
82
+ "reject.email.body": "Administrator odrzucił Twój koszyk. Powód: {reason}",
83
+
84
+ "upsells.heading": "Możesz też potrzebować",
85
+ "upsells.empty": "Brak sugestii dla tego koszyka.",
86
+ "policy.title": "Zatwierdzanie koszyka",
87
+ "policy.description": "Gdy opcja jest włączona, zwykli członkowie tej organizacji muszą przekazać koszyk do zatwierdzenia przez Administratora Organizacji, zanim będą mogli złożyć zamówienie.",
88
+ "policy.on": "Zatwierdzenie wymagane przed złożeniem zamówienia",
89
+ "policy.off": "Zatwierdzenie niewymagane",
90
+ "policy.hint": "Wyłączenie zasady przywraca każdy oczekujący lub zatwierdzony koszyk w tej organizacji do stanu „niewymagane”, a każda zmiana trafia do dziennika audytu.",
91
+ "policy.saving": "Zapisywanie…",
92
+ "policy.turnedOn": "Zatwierdzanie koszyka jest teraz wymagane w tej organizacji.",
93
+ "policy.turnedOff": "Zatwierdzanie koszyka nie jest już wymagane; oczekujące koszyki zostały zwolnione.",
94
+ "policy.loadError": "Nie udało się wczytać zasady zatwierdzania koszyka.",
95
+ "policy.saveError": "Nie udało się zaktualizować zasady."
96
+ }
package/package.json ADDED
@@ -0,0 +1,107 @@
1
+ {
2
+ "name": "@endora-commerce/mod-carts",
3
+ "version": "0.100.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Shopping cart aggregation, pricing, lifecycle, and approval gate.",
7
+ "license": "MIT",
8
+ "endora": {
9
+ "type": "module",
10
+ "id": "carts"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/endora-commerce/endora-commerce.git",
15
+ "directory": "packages/modules/carts"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/manifest.d.ts",
23
+ "default": "./dist/manifest.js"
24
+ },
25
+ "./backend": {
26
+ "types": "./dist/backend/index.d.ts",
27
+ "default": "./dist/backend/index.js"
28
+ },
29
+ "./migrations": {
30
+ "types": "./dist/migrations/index.d.ts",
31
+ "default": "./dist/migrations/index.js"
32
+ },
33
+ "./ports": {
34
+ "types": "./dist/ports/index.d.ts",
35
+ "default": "./dist/ports/index.js"
36
+ },
37
+ "./admin": {
38
+ "types": "./dist/admin/index.d.ts",
39
+ "default": "./dist/admin/index.js"
40
+ },
41
+ "./tailwind.css": "./tailwind.css",
42
+ "./package.json": "./package.json"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "i18n",
47
+ "docs",
48
+ "tailwind.css"
49
+ ],
50
+ "engines": {
51
+ "node": ">=22.18.0"
52
+ },
53
+ "peerDependencies": {
54
+ "@fastify/cookie": "^11",
55
+ "@mikro-orm/core": "^6",
56
+ "@mikro-orm/migrations": "^6",
57
+ "@mikro-orm/postgresql": "^6",
58
+ "fastify": "^5",
59
+ "ioredis": "^5",
60
+ "lucide-react": "^1",
61
+ "react": "^19",
62
+ "react-router-dom": "^7",
63
+ "zod": "^4",
64
+ "@endora-commerce/admin-kit": "0.100.0",
65
+ "@endora-commerce/contracts": "0.100.0",
66
+ "@endora-commerce/platform": "0.100.0"
67
+ },
68
+ "peerDependenciesMeta": {
69
+ "@endora-commerce/admin-kit": {
70
+ "optional": true
71
+ },
72
+ "lucide-react": {
73
+ "optional": true
74
+ },
75
+ "react": {
76
+ "optional": true
77
+ },
78
+ "react-router-dom": {
79
+ "optional": true
80
+ }
81
+ },
82
+ "devDependencies": {
83
+ "@fastify/cookie": "^11.0.2",
84
+ "@mikro-orm/core": "^6.6.13",
85
+ "@mikro-orm/migrations": "^6.6.13",
86
+ "@mikro-orm/postgresql": "^6.6.13",
87
+ "@types/node": "^22.9.0",
88
+ "@types/react": "^19.2.14",
89
+ "fastify": "^5.12.5",
90
+ "ioredis": "^5.10.1",
91
+ "lucide-react": "^1.11.0",
92
+ "react": "^19.2.5",
93
+ "react-router-dom": "^7.18.2",
94
+ "typescript": "^5.9.3",
95
+ "vitest": "^4.1.11",
96
+ "zod": "^4.2.0",
97
+ "@endora-commerce/admin-kit": "0.100.0",
98
+ "@endora-commerce/contracts": "0.100.0",
99
+ "@endora-commerce/platform": "0.100.0"
100
+ },
101
+ "scripts": {
102
+ "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.ui.json",
103
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.ui.json --noEmit",
104
+ "lint": "eslint src",
105
+ "test": "vitest run"
106
+ }
107
+ }
package/tailwind.css ADDED
@@ -0,0 +1,14 @@
1
+ /* @endora-commerce/mod-carts — AUTO-GENERATED by `pnpm --filter backend run manifests:generate`.
2
+ *
3
+ * The `@source` directives this package asks its host to scan
4
+ * (`specs/110-instance-repository/contracts/admin-stylesheet-composition.md` R1).
5
+ * They resolve relative to **this file**, so they hold wherever the package is
6
+ * installed — a workspace link here, `node_modules` in a client's instance.
7
+ *
8
+ * The `dist` line is what a published tarball ships and is what an instance
9
+ * scans; the `src` line is inert there and is what keeps `pnpm --filter admin
10
+ * run dev` reading source in this repository. Do not edit: run
11
+ * `pnpm --filter backend run manifests:generate`.
12
+ */
13
+ @source "./dist/admin";
14
+ @source "./src/admin";