@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.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/admin/components/CartApprovalPolicyPanel.d.ts +51 -0
- package/dist/admin/components/CartApprovalPolicyPanel.d.ts.map +1 -0
- package/dist/admin/components/CartApprovalPolicyPanel.js +59 -0
- package/dist/admin/components/CartApprovalPolicyPanel.js.map +1 -0
- package/dist/admin/index.d.ts +38 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +73 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/CartDetail.d.ts +3 -0
- package/dist/admin/pages/CartDetail.d.ts.map +1 -0
- package/dist/admin/pages/CartDetail.js +74 -0
- package/dist/admin/pages/CartDetail.js.map +1 -0
- package/dist/admin/pages/CartsList.d.ts +3 -0
- package/dist/admin/pages/CartsList.d.ts.map +1 -0
- package/dist/admin/pages/CartsList.js +90 -0
- package/dist/admin/pages/CartsList.js.map +1 -0
- package/dist/admin/zones/CartApprovalPolicy.d.ts +29 -0
- package/dist/admin/zones/CartApprovalPolicy.d.ts.map +1 -0
- package/dist/admin/zones/CartApprovalPolicy.js +11 -0
- package/dist/admin/zones/CartApprovalPolicy.js.map +1 -0
- package/dist/backend/cli/abandonment-sweep.d.ts +64 -0
- package/dist/backend/cli/abandonment-sweep.d.ts.map +1 -0
- package/dist/backend/cli/abandonment-sweep.js +17 -0
- package/dist/backend/cli/abandonment-sweep.js.map +1 -0
- package/dist/backend/entities/cart-audit-entry.entity.d.ts +29 -0
- package/dist/backend/entities/cart-audit-entry.entity.d.ts.map +1 -0
- package/dist/backend/entities/cart-audit-entry.entity.js +109 -0
- package/dist/backend/entities/cart-audit-entry.entity.js.map +1 -0
- package/dist/backend/entities/cart-item.entity.d.ts +42 -0
- package/dist/backend/entities/cart-item.entity.d.ts.map +1 -0
- package/dist/backend/entities/cart-item.entity.js +119 -0
- package/dist/backend/entities/cart-item.entity.js.map +1 -0
- package/dist/backend/entities/cart.entity.d.ts +96 -0
- package/dist/backend/entities/cart.entity.d.ts.map +1 -0
- package/dist/backend/entities/cart.entity.js +207 -0
- package/dist/backend/entities/cart.entity.js.map +1 -0
- package/dist/backend/index.d.ts +122 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +184 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +31 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +95 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.d.ts +101 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +620 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/routes.organization.d.ts +29 -0
- package/dist/backend/routes.organization.d.ts.map +1 -0
- package/dist/backend/routes.organization.js +150 -0
- package/dist/backend/routes.organization.js.map +1 -0
- package/dist/backend/services/cart-abandonment-settings.d.ts +34 -0
- package/dist/backend/services/cart-abandonment-settings.d.ts.map +1 -0
- package/dist/backend/services/cart-abandonment-settings.js +55 -0
- package/dist/backend/services/cart-abandonment-settings.js.map +1 -0
- package/dist/backend/services/cart-abandonment-worker.d.ts +110 -0
- package/dist/backend/services/cart-abandonment-worker.d.ts.map +1 -0
- package/dist/backend/services/cart-abandonment-worker.js +185 -0
- package/dist/backend/services/cart-abandonment-worker.js.map +1 -0
- package/dist/backend/services/cart-admin-service.d.ts +58 -0
- package/dist/backend/services/cart-admin-service.d.ts.map +1 -0
- package/dist/backend/services/cart-admin-service.js +397 -0
- package/dist/backend/services/cart-admin-service.js.map +1 -0
- package/dist/backend/services/cart-approval-service.d.ts +147 -0
- package/dist/backend/services/cart-approval-service.d.ts.map +1 -0
- package/dist/backend/services/cart-approval-service.js +242 -0
- package/dist/backend/services/cart-approval-service.js.map +1 -0
- package/dist/backend/services/cart-audit-service.d.ts +75 -0
- package/dist/backend/services/cart-audit-service.d.ts.map +1 -0
- package/dist/backend/services/cart-audit-service.js +70 -0
- package/dist/backend/services/cart-audit-service.js.map +1 -0
- package/dist/backend/services/cart-conversion-service.d.ts +104 -0
- package/dist/backend/services/cart-conversion-service.d.ts.map +1 -0
- package/dist/backend/services/cart-conversion-service.js +175 -0
- package/dist/backend/services/cart-conversion-service.js.map +1 -0
- package/dist/backend/services/cart-coupon-service.d.ts +129 -0
- package/dist/backend/services/cart-coupon-service.d.ts.map +1 -0
- package/dist/backend/services/cart-coupon-service.js +244 -0
- package/dist/backend/services/cart-coupon-service.js.map +1 -0
- package/dist/backend/services/cart-organization-visibility-service.d.ts +43 -0
- package/dist/backend/services/cart-organization-visibility-service.d.ts.map +1 -0
- package/dist/backend/services/cart-organization-visibility-service.js +98 -0
- package/dist/backend/services/cart-organization-visibility-service.js.map +1 -0
- package/dist/backend/services/cart-placement-apply-port.d.ts +38 -0
- package/dist/backend/services/cart-placement-apply-port.d.ts.map +1 -0
- package/dist/backend/services/cart-placement-apply-port.js +71 -0
- package/dist/backend/services/cart-placement-apply-port.js.map +1 -0
- package/dist/backend/services/cart-pricing-recompute.d.ts +62 -0
- package/dist/backend/services/cart-pricing-recompute.d.ts.map +1 -0
- package/dist/backend/services/cart-pricing-recompute.js +131 -0
- package/dist/backend/services/cart-pricing-recompute.js.map +1 -0
- package/dist/backend/services/cart-query-service.d.ts +29 -0
- package/dist/backend/services/cart-query-service.d.ts.map +1 -0
- package/dist/backend/services/cart-query-service.js +40 -0
- package/dist/backend/services/cart-query-service.js.map +1 -0
- package/dist/backend/services/cart-read-port.d.ts +40 -0
- package/dist/backend/services/cart-read-port.d.ts.map +1 -0
- package/dist/backend/services/cart-read-port.js +144 -0
- package/dist/backend/services/cart-read-port.js.map +1 -0
- package/dist/backend/services/cart-recompute-cache.d.ts +49 -0
- package/dist/backend/services/cart-recompute-cache.d.ts.map +1 -0
- package/dist/backend/services/cart-recompute-cache.js +56 -0
- package/dist/backend/services/cart-recompute-cache.js.map +1 -0
- package/dist/backend/services/cart-service.d.ts +165 -0
- package/dist/backend/services/cart-service.d.ts.map +1 -0
- package/dist/backend/services/cart-service.js +556 -0
- package/dist/backend/services/cart-service.js.map +1 -0
- package/dist/backend/services/cart-state-machine.d.ts +51 -0
- package/dist/backend/services/cart-state-machine.d.ts.map +1 -0
- package/dist/backend/services/cart-state-machine.js +74 -0
- package/dist/backend/services/cart-state-machine.js.map +1 -0
- package/dist/backend/services/cart-upsell-service.d.ts +57 -0
- package/dist/backend/services/cart-upsell-service.d.ts.map +1 -0
- package/dist/backend/services/cart-upsell-service.js +100 -0
- package/dist/backend/services/cart-upsell-service.js.map +1 -0
- package/dist/backend/services/customer-group-resolver.d.ts +27 -0
- package/dist/backend/services/customer-group-resolver.d.ts.map +1 -0
- package/dist/backend/services/customer-group-resolver.js +32 -0
- package/dist/backend/services/customer-group-resolver.js.map +1 -0
- package/dist/manifest.d.ts +221 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +224 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260611T140352_carts_consolidation.d.ts +43 -0
- package/dist/migrations/20260611T140352_carts_consolidation.d.ts.map +1 -0
- package/dist/migrations/20260611T140352_carts_consolidation.js +237 -0
- package/dist/migrations/20260611T140352_carts_consolidation.js.map +1 -0
- package/dist/migrations/20260611T140413_carts_cart_item_packaging.d.ts +12 -0
- package/dist/migrations/20260611T140413_carts_cart_item_packaging.d.ts.map +1 -0
- package/dist/migrations/20260611T140413_carts_cart_item_packaging.js +20 -0
- package/dist/migrations/20260611T140413_carts_cart_item_packaging.js.map +1 -0
- package/dist/migrations/20260818T081253_carts_cart_completed_order_fk.d.ts +50 -0
- package/dist/migrations/20260818T081253_carts_cart_completed_order_fk.d.ts.map +1 -0
- package/dist/migrations/20260818T081253_carts_cart_completed_order_fk.js +62 -0
- package/dist/migrations/20260818T081253_carts_cart_completed_order_fk.js.map +1 -0
- package/dist/migrations/20260830T112911_carts_organization_attribution_check.d.ts +67 -0
- package/dist/migrations/20260830T112911_carts_organization_attribution_check.d.ts.map +1 -0
- package/dist/migrations/20260830T112911_carts_organization_attribution_check.js +121 -0
- package/dist/migrations/20260830T112911_carts_organization_attribution_check.js.map +1 -0
- package/dist/migrations/index.d.ts +30 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +35 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/ports/index.d.ts +106 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +2 -0
- package/dist/ports/index.js.map +1 -0
- package/docs/carts.md +232 -0
- package/i18n/en.json +96 -0
- package/i18n/pl.json +96 -0
- package/package.json +107 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { ANONYMOUS_PRODUCT_AUDIENCE, ERROR_CODES, isProductVisibleTo, } from '@endora-commerce/contracts';
|
|
3
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
4
|
+
import { Cart } from '../entities/cart.entity.js';
|
|
5
|
+
import { CartItem } from '../entities/cart-item.entity.js';
|
|
6
|
+
import { SalesChannel } from '@endora-commerce/platform/kernel';
|
|
7
|
+
import { outOfRequestChannel } from '@endora-commerce/platform/kernel';
|
|
8
|
+
/**
|
|
9
|
+
* Cart-line hard cap (feature 027 FR-001 / spec edge case "Cart contents
|
|
10
|
+
* cap"). The 201st distinct line is refused with HTTP 422.
|
|
11
|
+
*/
|
|
12
|
+
export const CART_MAX_LINES = 200;
|
|
13
|
+
export class CartService {
|
|
14
|
+
emFactory;
|
|
15
|
+
pricingService;
|
|
16
|
+
catalogProducts;
|
|
17
|
+
organizations;
|
|
18
|
+
channelMembership;
|
|
19
|
+
approvalService;
|
|
20
|
+
auditService;
|
|
21
|
+
recomputeCache;
|
|
22
|
+
/**
|
|
23
|
+
* `pricingService` is **required** since issue #124. It used to be optional
|
|
24
|
+
* "for test rigs", and the cart then read the legacy
|
|
25
|
+
* `attributeValues['defaultPrice']` whenever it was missing — so a rig, a
|
|
26
|
+
* wiring slip and `price_lists` switched off all produced the same thing: a
|
|
27
|
+
* cart line priced from a catalogue attribute that no price list supports.
|
|
28
|
+
* Absence is now a `lazyPort` gate that throws `MODULE_DISABLED` at the call,
|
|
29
|
+
* which nothing downstream can mistake for a price.
|
|
30
|
+
*
|
|
31
|
+
* Cart entities don't currently track which sales channel they were
|
|
32
|
+
* created on (multi-channel cart attribution is a separate
|
|
33
|
+
* follow-up), so the resolver runs against the system-default
|
|
34
|
+
* channel — matching the behaviour the foundation cart already had.
|
|
35
|
+
* Lists with a `salesChannel` criterion targeting other channels
|
|
36
|
+
* therefore won't apply on the cart line until cart-side channel
|
|
37
|
+
* tracking lands.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* `approvalService` and `auditService` are optional — when wired
|
|
41
|
+
* (production composition), any buyer-driven mutation on an
|
|
42
|
+
* `approved` cart re-arms approval to `pending` and a typed audit
|
|
43
|
+
* row is landed (line_added / line_qty_changed / line_removed).
|
|
44
|
+
* Foundation tests and legacy test rigs that don't wire either still
|
|
45
|
+
* construct the service without them; both hooks no-op when absent.
|
|
46
|
+
*/
|
|
47
|
+
constructor(emFactory,
|
|
48
|
+
/** `price_lists`' line-resolution slice (feature 075, Phase C). */
|
|
49
|
+
pricingService,
|
|
50
|
+
/** `catalog`'s product read model — products, variants, packaging units. */
|
|
51
|
+
catalogProducts,
|
|
52
|
+
/** `organizations`' read model — the buying org the price resolves against. */
|
|
53
|
+
organizations,
|
|
54
|
+
/**
|
|
55
|
+
* The sanctioned bridge accessor (Constitution XII), for the assortment
|
|
56
|
+
* gate `addItem` owes (issue #259). Required, like `catalogProducts`: an
|
|
57
|
+
* optional one would make "this rig did not wire it" and "this channel
|
|
58
|
+
* publishes the product" the same answer, which is the fail-open shape the
|
|
59
|
+
* whole issue is about.
|
|
60
|
+
*/
|
|
61
|
+
channelMembership, approvalService, auditService, recomputeCache) {
|
|
62
|
+
this.emFactory = emFactory;
|
|
63
|
+
this.pricingService = pricingService;
|
|
64
|
+
this.catalogProducts = catalogProducts;
|
|
65
|
+
this.organizations = organizations;
|
|
66
|
+
this.channelMembership = channelMembership;
|
|
67
|
+
this.approvalService = approvalService;
|
|
68
|
+
this.auditService = auditService;
|
|
69
|
+
this.recomputeCache = recomputeCache;
|
|
70
|
+
}
|
|
71
|
+
/** Invalidate the per-cart price-recompute cache on every cart-side write. */
|
|
72
|
+
async #invalidateRecomputeCache(cartId) {
|
|
73
|
+
if (!this.recomputeCache)
|
|
74
|
+
return;
|
|
75
|
+
try {
|
|
76
|
+
await this.recomputeCache.invalidate(cartId);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Cache invalidation is best-effort — never block a write.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async getOrCreateForCustomer(ctx) {
|
|
83
|
+
const em = this.emFactory();
|
|
84
|
+
return this.#getOrCreateForCustomerOn(em, ctx);
|
|
85
|
+
}
|
|
86
|
+
async #getOrCreateForCustomerOn(em, ctx) {
|
|
87
|
+
let cart = await em.findOne(Cart, {
|
|
88
|
+
customerAccountId: ctx.customerAccountId,
|
|
89
|
+
status: 'active',
|
|
90
|
+
});
|
|
91
|
+
if (!cart) {
|
|
92
|
+
cart = em.create(Cart, {
|
|
93
|
+
customerAccountId: ctx.customerAccountId,
|
|
94
|
+
organizationId: ctx.organizationId,
|
|
95
|
+
status: 'active',
|
|
96
|
+
salesChannelId: await this.#defaultSalesChannelId(em),
|
|
97
|
+
});
|
|
98
|
+
await em.persistAndFlush(cart);
|
|
99
|
+
}
|
|
100
|
+
return cart;
|
|
101
|
+
}
|
|
102
|
+
async getOrCreateForAnon(em, token) {
|
|
103
|
+
let cart = await em.findOne(Cart, { anonymousCartToken: token, status: 'active' });
|
|
104
|
+
if (!cart) {
|
|
105
|
+
cart = em.create(Cart, {
|
|
106
|
+
anonymousCartToken: token,
|
|
107
|
+
status: 'active',
|
|
108
|
+
salesChannelId: await this.#defaultSalesChannelId(em),
|
|
109
|
+
});
|
|
110
|
+
await em.persistAndFlush(cart);
|
|
111
|
+
}
|
|
112
|
+
return cart;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Feature 052 — a cart MUST carry a resolved sales channel so promotion and
|
|
116
|
+
* pricing evaluation is channel-scoped (the promotion channel gate fails closed
|
|
117
|
+
* on a null-channel cart, FR-005). Until request-driven channel resolution is
|
|
118
|
+
* unified (spec 03), a new cart defaults to the system-default channel. Returns
|
|
119
|
+
* `null` only if no system-default channel exists (a misconfigured store).
|
|
120
|
+
*/
|
|
121
|
+
async #defaultSalesChannelId(em) {
|
|
122
|
+
const channel = await em.findOne(SalesChannel, { systemDefault: true }, { fields: ['id'] });
|
|
123
|
+
return channel?.id ?? null;
|
|
124
|
+
}
|
|
125
|
+
async getItems(cartId) {
|
|
126
|
+
const em = this.emFactory();
|
|
127
|
+
return em.find(CartItem, { cartId });
|
|
128
|
+
}
|
|
129
|
+
async addItem(actor, input) {
|
|
130
|
+
const em = this.emFactory();
|
|
131
|
+
const cart = actor.customer
|
|
132
|
+
? await this.#getOrCreateForCustomerOn(em, actor.customer)
|
|
133
|
+
: actor.anonymousToken
|
|
134
|
+
? await this.getOrCreateForAnon(em, actor.anonymousToken)
|
|
135
|
+
: (() => {
|
|
136
|
+
throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'Cart requires either a customer session or an anonymous cart token.');
|
|
137
|
+
})();
|
|
138
|
+
if (input.quantity <= 0) {
|
|
139
|
+
throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'Quantity must be > 0.');
|
|
140
|
+
}
|
|
141
|
+
const product = await this.catalogProducts.findById(input.productId);
|
|
142
|
+
// Issue #227 — a product this shopper may not see is a product they may not
|
|
143
|
+
// put in a cart. The read port is the row-level one and applies no policy of
|
|
144
|
+
// its own, deliberately: an order line has to resolve its product long after
|
|
145
|
+
// the operator restricted it. That makes enforcement the caller's, and this
|
|
146
|
+
// caller is holding a `productId` the buyer chose.
|
|
147
|
+
//
|
|
148
|
+
// 404 and not 403, matching the product detail: a caller who may not see the
|
|
149
|
+
// row may not learn it exists, and the two answers are indistinguishable to
|
|
150
|
+
// an honest client because they are the same answer.
|
|
151
|
+
if (!product || !isProductVisibleTo(product, cartAudience(actor))) {
|
|
152
|
+
throw new HttpError(404, ERROR_CODES.PRODUCT_NOT_FOUND, 'Product not found.');
|
|
153
|
+
}
|
|
154
|
+
// Issue #259 — the second filter the predicate above says it is not. The
|
|
155
|
+
// channel is a property of the request, so it is read off the request scope
|
|
156
|
+
// and never re-resolved here; outside a request there is no channel to be
|
|
157
|
+
// out of and `outOfRequestChannel` answers `false`.
|
|
158
|
+
//
|
|
159
|
+
// The same 404 as the two lines above, deliberately: "sold on another
|
|
160
|
+
// channel", "restricted to another organisation" and "does not exist" have
|
|
161
|
+
// to be one answer, or the pair of them is an enumeration oracle over an
|
|
162
|
+
// operator's private assortment — the defect issue #174 found in the
|
|
163
|
+
// type-ahead next door.
|
|
164
|
+
if (await outOfRequestChannel(this.channelMembership, product.id)) {
|
|
165
|
+
throw new HttpError(404, ERROR_CODES.PRODUCT_NOT_FOUND, 'Product not found.');
|
|
166
|
+
}
|
|
167
|
+
// Feature 043 — ordering by a packaging unit (e.g. a pallet): the line is
|
|
168
|
+
// measured in base pieces (`baseQuantity × units`) and snapshots the unit
|
|
169
|
+
// name so the cart/order/RFQ can append it to the product name.
|
|
170
|
+
//
|
|
171
|
+
// `findPackagingUnitInProduct` is the two-argument form of the same read:
|
|
172
|
+
// the "does this unit belong to this product?" test was the second half of
|
|
173
|
+
// the `where` clause and stays inside one query.
|
|
174
|
+
let packagingUnit = null;
|
|
175
|
+
if (input.packagingUnitId) {
|
|
176
|
+
packagingUnit = await this.catalogProducts.findPackagingUnitInProduct(product.id, input.packagingUnitId);
|
|
177
|
+
if (!packagingUnit) {
|
|
178
|
+
throw new HttpError(404, ERROR_CODES.PACKAGING_UNIT_NOT_FOUND, 'Packaging unit not found for this product.');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const effectiveQuantity = packagingUnit
|
|
182
|
+
? packagingUnit.baseQuantity * input.quantity
|
|
183
|
+
: input.quantity;
|
|
184
|
+
// If the same product+variant+packaging-unit is already in the cart, bump
|
|
185
|
+
// quantity. A packaging-unit line and a single-piece line of the same
|
|
186
|
+
// product stay distinct (different `packagingUnitId`).
|
|
187
|
+
const existing = await em.findOne(CartItem, {
|
|
188
|
+
cartId: cart.id,
|
|
189
|
+
productId: product.id,
|
|
190
|
+
variantId: input.variantId ?? null,
|
|
191
|
+
packagingUnitId: input.packagingUnitId ?? null,
|
|
192
|
+
});
|
|
193
|
+
if (existing) {
|
|
194
|
+
existing.quantity += effectiveQuantity;
|
|
195
|
+
cart.lastActivityAt = new Date();
|
|
196
|
+
await em.flush();
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
// Feature 027 FR-001: enforce the 200-line cap on the 201st distinct
|
|
200
|
+
// (productId, variantId) pair. Foundation duplicate-summation above
|
|
201
|
+
// does not trigger the cap.
|
|
202
|
+
const currentLineCount = await em.count(CartItem, { cartId: cart.id });
|
|
203
|
+
if (currentLineCount >= CART_MAX_LINES) {
|
|
204
|
+
throw new HttpError(422, ERROR_CODES.CART_LINE_CAP_EXCEEDED, 'cart_line_cap_exceeded');
|
|
205
|
+
}
|
|
206
|
+
const resolved = await this.#resolveLineUnitPrice(em, {
|
|
207
|
+
product,
|
|
208
|
+
organizationId: actor.customer?.organizationId ?? null,
|
|
209
|
+
quantity: effectiveQuantity,
|
|
210
|
+
variantId: input.variantId ?? null,
|
|
211
|
+
});
|
|
212
|
+
// T084 — defence-in-depth: refuse the line when the resolver says
|
|
213
|
+
// this product is quote-only for the (org, channel) tuple.
|
|
214
|
+
if (resolved && resolved.displayMode === 'none') {
|
|
215
|
+
throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, 'product_quote_only');
|
|
216
|
+
}
|
|
217
|
+
const item = em.create(CartItem, {
|
|
218
|
+
cartId: cart.id,
|
|
219
|
+
productId: product.id,
|
|
220
|
+
...(input.variantId ? { variantId: input.variantId } : {}),
|
|
221
|
+
...(packagingUnit
|
|
222
|
+
? {
|
|
223
|
+
packagingUnitId: packagingUnit.id,
|
|
224
|
+
packagingUnitName: packagingUnit.name,
|
|
225
|
+
packagingUnitBaseQuantity: packagingUnit.baseQuantity,
|
|
226
|
+
}
|
|
227
|
+
: {}),
|
|
228
|
+
quantity: effectiveQuantity,
|
|
229
|
+
// `resolved === null` is `price_lists` answering "no list applies to
|
|
230
|
+
// this line"; the catalogue attribute is the legacy stand-in for that
|
|
231
|
+
// case and is reported as remaining debt (issue #124). What can no
|
|
232
|
+
// longer land here is an *absent* `price_lists`: resolving the port
|
|
233
|
+
// throws `MODULE_DISABLED` before this expression runs.
|
|
234
|
+
unitPrice: resolved
|
|
235
|
+
? Number(resolved.amount).toFixed(2)
|
|
236
|
+
: (Number(product.attributeValues['defaultPrice'] ??
|
|
237
|
+
product.attributeValues['price'] ??
|
|
238
|
+
0)).toFixed(2),
|
|
239
|
+
currency: resolved?.currency ?? 'PLN',
|
|
240
|
+
});
|
|
241
|
+
em.persist(item);
|
|
242
|
+
cart.lastActivityAt = new Date();
|
|
243
|
+
await em.flush();
|
|
244
|
+
}
|
|
245
|
+
if (this.approvalService && actor.customer) {
|
|
246
|
+
await this.approvalService.maybeReArm(cart, {
|
|
247
|
+
customerAccountId: actor.customer.customerAccountId,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (this.auditService) {
|
|
251
|
+
await this.auditService.record({
|
|
252
|
+
cartId: cart.id,
|
|
253
|
+
actorType: actor.customer ? 'customer' : 'system',
|
|
254
|
+
...(actor.customer ? { actorId: actor.customer.customerAccountId } : {}),
|
|
255
|
+
action: 'line_added',
|
|
256
|
+
metadata: {
|
|
257
|
+
productId: input.productId,
|
|
258
|
+
...(input.variantId ? { variantId: input.variantId } : {}),
|
|
259
|
+
quantity: input.quantity,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
await this.#invalidateRecomputeCache(cart.id);
|
|
264
|
+
const items = await em.find(CartItem, { cartId: cart.id });
|
|
265
|
+
return { cart, items };
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Look up the line's unit price via the resolver.
|
|
269
|
+
*
|
|
270
|
+
* `null` means "the price-list engine answered, and nothing applies" —
|
|
271
|
+
* `resolveLinePrice`'s own documented answer — so there is no `catch` here
|
|
272
|
+
* (issue #84). There used to be one, and it made the two answers
|
|
273
|
+
* indistinguishable: a `price_lists` switched off, or a resolver bug, both
|
|
274
|
+
* came back as "this line has no price", and the cart quietly re-priced from
|
|
275
|
+
* the catalogue default. An absent `price_lists` now throws through this
|
|
276
|
+
* method to the route (issue #124).
|
|
277
|
+
*/
|
|
278
|
+
async #resolveLineUnitPrice(em, input) {
|
|
279
|
+
const channel = await em.findOne(SalesChannel, { systemDefault: true });
|
|
280
|
+
if (!channel)
|
|
281
|
+
return null;
|
|
282
|
+
const organization = input.organizationId
|
|
283
|
+
? await this.organizations.findById(input.organizationId)
|
|
284
|
+
: null;
|
|
285
|
+
const resolved = await this.pricingService.resolveLinePrice({
|
|
286
|
+
product: input.product,
|
|
287
|
+
variantId: input.variantId,
|
|
288
|
+
context: {
|
|
289
|
+
quantity: input.quantity,
|
|
290
|
+
...(organization ? { organization } : {}),
|
|
291
|
+
salesChannel: channel,
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
if (!resolved)
|
|
295
|
+
return null;
|
|
296
|
+
return {
|
|
297
|
+
amount: resolved.amount,
|
|
298
|
+
currency: resolved.currency,
|
|
299
|
+
priceListId: resolved.priceListId,
|
|
300
|
+
displayMode: resolved.displayMode,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async updateItem(actor, itemId, quantity) {
|
|
304
|
+
const em = this.emFactory();
|
|
305
|
+
const item = await em.findOne(CartItem, { id: itemId });
|
|
306
|
+
if (!item)
|
|
307
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Cart item not found.');
|
|
308
|
+
const cart = await em.findOne(Cart, { id: item.cartId });
|
|
309
|
+
if (!cart)
|
|
310
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Cart not found.');
|
|
311
|
+
this.#assertOwnership(cart, actor);
|
|
312
|
+
const oldQuantity = item.quantity;
|
|
313
|
+
if (quantity <= 0) {
|
|
314
|
+
em.remove(item);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
item.quantity = quantity;
|
|
318
|
+
}
|
|
319
|
+
cart.lastActivityAt = new Date();
|
|
320
|
+
await em.flush();
|
|
321
|
+
if (this.approvalService && actor.customer) {
|
|
322
|
+
await this.approvalService.maybeReArm(cart, {
|
|
323
|
+
customerAccountId: actor.customer.customerAccountId,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
if (this.auditService) {
|
|
327
|
+
await this.auditService.record({
|
|
328
|
+
cartId: cart.id,
|
|
329
|
+
actorType: actor.customer ? 'customer' : 'system',
|
|
330
|
+
...(actor.customer ? { actorId: actor.customer.customerAccountId } : {}),
|
|
331
|
+
action: quantity <= 0 ? 'line_removed' : 'line_qty_changed',
|
|
332
|
+
metadata: {
|
|
333
|
+
productId: item.productId,
|
|
334
|
+
...(item.variantId ? { variantId: item.variantId } : {}),
|
|
335
|
+
oldQuantity,
|
|
336
|
+
newQuantity: quantity,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
await this.#invalidateRecomputeCache(cart.id);
|
|
341
|
+
const items = await em.find(CartItem, { cartId: cart.id });
|
|
342
|
+
return { cart, items };
|
|
343
|
+
}
|
|
344
|
+
async removeItem(actor, itemId) {
|
|
345
|
+
const em = this.emFactory();
|
|
346
|
+
const item = await em.findOne(CartItem, { id: itemId });
|
|
347
|
+
if (!item)
|
|
348
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Cart item not found.');
|
|
349
|
+
const cart = await em.findOne(Cart, { id: item.cartId });
|
|
350
|
+
if (!cart)
|
|
351
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Cart not found.');
|
|
352
|
+
this.#assertOwnership(cart, actor);
|
|
353
|
+
const removedProductId = item.productId;
|
|
354
|
+
const removedVariantId = item.variantId ?? null;
|
|
355
|
+
const removedQuantity = item.quantity;
|
|
356
|
+
em.remove(item);
|
|
357
|
+
cart.lastActivityAt = new Date();
|
|
358
|
+
await em.flush();
|
|
359
|
+
if (this.approvalService && actor.customer) {
|
|
360
|
+
await this.approvalService.maybeReArm(cart, {
|
|
361
|
+
customerAccountId: actor.customer.customerAccountId,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (this.auditService) {
|
|
365
|
+
await this.auditService.record({
|
|
366
|
+
cartId: cart.id,
|
|
367
|
+
actorType: actor.customer ? 'customer' : 'system',
|
|
368
|
+
...(actor.customer ? { actorId: actor.customer.customerAccountId } : {}),
|
|
369
|
+
action: 'line_removed',
|
|
370
|
+
metadata: {
|
|
371
|
+
productId: removedProductId,
|
|
372
|
+
...(removedVariantId ? { variantId: removedVariantId } : {}),
|
|
373
|
+
quantity: removedQuantity,
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
await this.#invalidateRecomputeCache(cart.id);
|
|
378
|
+
const items = await em.find(CartItem, { cartId: cart.id });
|
|
379
|
+
return { cart, items };
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Explicit "this buyer just opened the cart page" ping (feature 027 §R4 /
|
|
383
|
+
* FR-032). Bumps `last_activity_at` and, if the cart had transitioned to
|
|
384
|
+
* `abandoned`, brings it back to `active` and clears
|
|
385
|
+
* `abandonment_notified_at` so the next abandonment cycle can notify
|
|
386
|
+
* again. No-op on a missing cart.
|
|
387
|
+
*/
|
|
388
|
+
async touch(actor) {
|
|
389
|
+
// command-coverage-ignore: bumps the ephemeral cart's lastActivityAt for the
|
|
390
|
+
// abandonment sweep — transient bookkeeping, not an audited domain mutation.
|
|
391
|
+
const em = this.emFactory();
|
|
392
|
+
let cart = null;
|
|
393
|
+
if (actor.customer) {
|
|
394
|
+
cart = await em.findOne(Cart, {
|
|
395
|
+
customerAccountId: actor.customer.customerAccountId,
|
|
396
|
+
status: { $in: ['active', 'abandoned'] },
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
else if (actor.anonymousToken) {
|
|
400
|
+
cart = await em.findOne(Cart, {
|
|
401
|
+
anonymousCartToken: actor.anonymousToken,
|
|
402
|
+
status: { $in: ['active', 'abandoned'] },
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
if (!cart)
|
|
406
|
+
return null;
|
|
407
|
+
cart.lastActivityAt = new Date();
|
|
408
|
+
if (cart.status === 'abandoned') {
|
|
409
|
+
cart.status = 'active';
|
|
410
|
+
cart.abandonmentNotifiedAt = null;
|
|
411
|
+
}
|
|
412
|
+
await em.flush();
|
|
413
|
+
return cart;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Merge an anonymous cart's items into the customer's cart at login
|
|
417
|
+
* time (feature 037-cart-merge-on-login).
|
|
418
|
+
*
|
|
419
|
+
* Returns a `CartMergeOutcome` diagnostic the caller (the login route
|
|
420
|
+
* via the `onLogin` hook) uses to surface the result to the storefront
|
|
421
|
+
* and to decide whether to clear the stale `b2b_cart_anon` cookie.
|
|
422
|
+
* See specs/037-cart-merge-on-login/contracts/cart-merge.md § 3.
|
|
423
|
+
*
|
|
424
|
+
* State matrix:
|
|
425
|
+
* - `adopted` — the customer had no active cart; the anon
|
|
426
|
+
* cart's lines were placed onto a freshly-created destination.
|
|
427
|
+
* - `merged` — the customer already had an active cart and
|
|
428
|
+
* ≥ 1 anon line was moved or summed into it.
|
|
429
|
+
* - `noop_empty` — the anon cart was found but carried no items;
|
|
430
|
+
* the source row is still flipped to `completed` + detokenised
|
|
431
|
+
* (cleanup), no audit row is written.
|
|
432
|
+
* - `noop_no_anon` — no cart for the supplied token; no DB change.
|
|
433
|
+
*
|
|
434
|
+
* Coupons on the source cart are NOT carried to the destination
|
|
435
|
+
* (R-05 / FR-010 — the destination's existing `appliedPromotionCode`
|
|
436
|
+
* is preserved verbatim).
|
|
437
|
+
*/
|
|
438
|
+
async mergeAnonymousIntoCustomer(anonymousToken, ctx) {
|
|
439
|
+
const em = this.emFactory();
|
|
440
|
+
// Look up the source first so the noop_no_anon branch can skip the
|
|
441
|
+
// destination-cart creation. Per the spec, the diagnostic still
|
|
442
|
+
// includes a destinationCartId so the storefront has a stable id to
|
|
443
|
+
// re-read its cart from on next render.
|
|
444
|
+
const anon = await em.findOne(Cart, { anonymousCartToken: anonymousToken, status: 'active' });
|
|
445
|
+
if (!anon) {
|
|
446
|
+
const destForNoop = await this.#getOrCreateForCustomerOn(em, ctx);
|
|
447
|
+
return {
|
|
448
|
+
outcome: 'noop_no_anon',
|
|
449
|
+
movedLineCount: 0,
|
|
450
|
+
summedLineCount: 0,
|
|
451
|
+
destinationCartId: destForNoop.id,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
// Detect "the customer's destination cart was newly created right now":
|
|
455
|
+
// peek for an existing active cart row BEFORE calling the get-or-create
|
|
456
|
+
// helper. If absent and the source has items, we will return `adopted`.
|
|
457
|
+
const preExistingDest = await em.findOne(Cart, {
|
|
458
|
+
customerAccountId: ctx.customerAccountId,
|
|
459
|
+
status: 'active',
|
|
460
|
+
});
|
|
461
|
+
const customerCart = preExistingDest ?? (await this.#getOrCreateForCustomerOn(em, ctx));
|
|
462
|
+
let movedLineCount = 0;
|
|
463
|
+
let summedLineCount = 0;
|
|
464
|
+
const anonItems = await em.find(CartItem, { cartId: anon.id });
|
|
465
|
+
for (const item of anonItems) {
|
|
466
|
+
const existing = await em.findOne(CartItem, {
|
|
467
|
+
cartId: customerCart.id,
|
|
468
|
+
productId: item.productId,
|
|
469
|
+
variantId: item.variantId ?? null,
|
|
470
|
+
});
|
|
471
|
+
if (existing) {
|
|
472
|
+
existing.quantity += item.quantity;
|
|
473
|
+
summedLineCount += 1;
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
item.cartId = customerCart.id;
|
|
477
|
+
movedLineCount += 1;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// Merge complete — mark the source anonymous cart as `completed` even
|
|
481
|
+
// when the merge was a noop_empty (it never produced anything down-
|
|
482
|
+
// stream, but leaving it `active` would let the abandonment sweep
|
|
483
|
+
// pick it up later, and leaving the token live would let a second
|
|
484
|
+
// login try to re-merge an already-consumed source). Treating it as
|
|
485
|
+
// `abandoned` would falsely flag it for the abandonment-notification
|
|
486
|
+
// sweep. See feature 027 §R10.
|
|
487
|
+
anon.status = 'completed';
|
|
488
|
+
anon.anonymousCartToken = null;
|
|
489
|
+
const isNoopEmpty = movedLineCount + summedLineCount === 0;
|
|
490
|
+
const outcome = isNoopEmpty
|
|
491
|
+
? 'noop_empty'
|
|
492
|
+
: preExistingDest
|
|
493
|
+
? 'merged'
|
|
494
|
+
: 'adopted';
|
|
495
|
+
// Audit only on observable state-changes on the destination cart
|
|
496
|
+
// (R-08). noop_empty and noop_no_anon are not destination-state
|
|
497
|
+
// changes and are observable in the application log instead.
|
|
498
|
+
if ((outcome === 'adopted' || outcome === 'merged') && this.auditService) {
|
|
499
|
+
const sourceAnonTokenHash = createHash('sha256').update(anonymousToken).digest('hex');
|
|
500
|
+
await this.auditService.record({
|
|
501
|
+
cartId: customerCart.id,
|
|
502
|
+
actorType: 'system',
|
|
503
|
+
action: 'cart_merged_from_anon',
|
|
504
|
+
metadata: {
|
|
505
|
+
sourceAnonTokenHash,
|
|
506
|
+
outcome,
|
|
507
|
+
movedLineCount,
|
|
508
|
+
summedLineCount,
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
await em.flush();
|
|
513
|
+
return {
|
|
514
|
+
outcome,
|
|
515
|
+
movedLineCount,
|
|
516
|
+
summedLineCount,
|
|
517
|
+
destinationCartId: customerCart.id,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
async clearForCustomer(ctx) {
|
|
521
|
+
// command-coverage-ignore: empties the ephemeral cart — transient pre-order
|
|
522
|
+
// working state; the resulting order captures the audited final state.
|
|
523
|
+
const em = this.emFactory();
|
|
524
|
+
const cart = await em.findOne(Cart, {
|
|
525
|
+
customerAccountId: ctx.customerAccountId,
|
|
526
|
+
status: 'active',
|
|
527
|
+
});
|
|
528
|
+
if (!cart)
|
|
529
|
+
return;
|
|
530
|
+
await em.nativeDelete(CartItem, { cartId: cart.id });
|
|
531
|
+
cart.status = 'completed';
|
|
532
|
+
await em.flush();
|
|
533
|
+
}
|
|
534
|
+
#assertOwnership(cart, actor) {
|
|
535
|
+
if (actor.customer && cart.customerAccountId === actor.customer.customerAccountId)
|
|
536
|
+
return;
|
|
537
|
+
if (actor.anonymousToken && cart.anonymousCartToken === actor.anonymousToken)
|
|
538
|
+
return;
|
|
539
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'Cart not found.');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* The {@link ProductAudience} a cart operation speaks for (issue #227).
|
|
544
|
+
*
|
|
545
|
+
* An anonymous cart is the anonymous audience — the token in the cookie
|
|
546
|
+
* identifies a basket, not a buyer, and it is minted by asking for one. A
|
|
547
|
+
* signed-in shopper carries their Organization, `null` included: the guest-style
|
|
548
|
+
* accounts of feature 026 have none, and they are exactly the caller
|
|
549
|
+
* `logged_in_only` distinguishes from the public.
|
|
550
|
+
*/
|
|
551
|
+
function cartAudience(actor) {
|
|
552
|
+
if (!actor.customer)
|
|
553
|
+
return ANONYMOUS_PRODUCT_AUDIENCE;
|
|
554
|
+
return { organizationId: actor.customer.organizationId, authenticated: true };
|
|
555
|
+
}
|
|
556
|
+
//# sourceMappingURL=cart-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-service.js","sourceRoot":"","sources":["../../../src/backend/services/cart-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { CartStatus, CartApprovalStatus } from '../entities/cart.entity.js';
|
|
2
|
+
/**
|
|
3
|
+
* Pure state-machine helpers for the carts module (feature 027 US4).
|
|
4
|
+
*
|
|
5
|
+
* The cart has two orthogonal axes:
|
|
6
|
+
* - main `status`: active | abandoned | completed | rejected
|
|
7
|
+
* - `approval_status`: not_required | pending | approved
|
|
8
|
+
* | rejected_by_org_admin
|
|
9
|
+
*
|
|
10
|
+
* Production transitions are implemented by `CartService`,
|
|
11
|
+
* `CartApprovalService`, and the abandonment worker — all DB-coupled.
|
|
12
|
+
* This module exposes the same invariants as pure functions so the
|
|
13
|
+
* matrix can be unit-tested (feature 027 T084 / T085) and so callers
|
|
14
|
+
* (route handlers, audit-row builders) can reach decisions without
|
|
15
|
+
* duplicating the inline branching.
|
|
16
|
+
*/
|
|
17
|
+
export type ApprovalEvent = 'submit' | 'approve' | 'reject' | 'buyer_mutation' | 'policy_off' | 'policy_on';
|
|
18
|
+
export type ApprovalTransitionFailure = 'already_terminal' | 'already_initiated' | 'not_pending' | 'not_applicable';
|
|
19
|
+
export type ApprovalTransitionResult = {
|
|
20
|
+
ok: true;
|
|
21
|
+
next: CartApprovalStatus;
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
reason: ApprovalTransitionFailure;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Returns the post-event approval state for a cart whose current
|
|
28
|
+
* approval state is `from`. Pre-flight checks (policy on, cart not
|
|
29
|
+
* empty, buyer-not-org-admin, etc.) live in the service layer; this
|
|
30
|
+
* function captures only the FSM legality of (state, event) pairs.
|
|
31
|
+
*/
|
|
32
|
+
export declare function nextApprovalState(from: CartApprovalStatus, event: ApprovalEvent): ApprovalTransitionResult;
|
|
33
|
+
/**
|
|
34
|
+
* Primary CTA the storefront should render. Pure function over the
|
|
35
|
+
* cart's two-axis state. The route handler caches this on every cart
|
|
36
|
+
* read; see `routes.ts.serializeCart`.
|
|
37
|
+
*/
|
|
38
|
+
export type PrimaryCta = 'checkout' | 'submit_for_approval' | 'awaiting_approval' | 'blocked_by_organization';
|
|
39
|
+
export declare function derivePrimaryCta(input: {
|
|
40
|
+
status: CartStatus;
|
|
41
|
+
approvalStatus: CartApprovalStatus;
|
|
42
|
+
requiresApproval?: boolean;
|
|
43
|
+
}): PrimaryCta;
|
|
44
|
+
/**
|
|
45
|
+
* `true` when an `approved` cart should be silently re-armed to
|
|
46
|
+
* `pending` because the buyer just mutated it. Lives here so the
|
|
47
|
+
* various write paths (add / update / remove / coupon apply / coupon
|
|
48
|
+
* clear) all agree on the same rule.
|
|
49
|
+
*/
|
|
50
|
+
export declare function shouldReArmOnBuyerMutation(approvalStatus: CartApprovalStatus): boolean;
|
|
51
|
+
//# sourceMappingURL=cart-state-machine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-state-machine.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cart-state-machine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEjF;;;;;;;;;;;;;;GAcG;AAEH,MAAM,MAAM,aAAa,GACrB,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,gBAAgB,GAChB,YAAY,GACZ,WAAW,CAAC;AAEhB,MAAM,MAAM,yBAAyB,GACjC,kBAAkB,GAClB,mBAAmB,GACnB,aAAa,GACb,gBAAgB,CAAC;AAErB,MAAM,MAAM,wBAAwB,GAChC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,kBAAkB,CAAA;CAAE,GACtC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,yBAAyB,CAAA;CAAE,CAAC;AAErD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,kBAAkB,EACxB,KAAK,EAAE,aAAa,GACnB,wBAAwB,CA0C1B;AAED;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAClB,UAAU,GACV,qBAAqB,GACrB,mBAAmB,GACnB,yBAAyB,CAAC;AAE9B,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,MAAM,EAAE,UAAU,CAAC;IACnB,cAAc,EAAE,kBAAkB,CAAC;IACnC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,UAAU,CAeb;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,cAAc,EAAE,kBAAkB,GACjC,OAAO,CAET"}
|