@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,175 @@
|
|
|
1
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { Cart } from '../entities/cart.entity.js';
|
|
4
|
+
import { CartItem } from '../entities/cart-item.entity.js';
|
|
5
|
+
import { rethrowIfModuleDisabled } from '@endora-commerce/platform/kernel';
|
|
6
|
+
import { productIdsInRequestChannel } from '@endora-commerce/platform/kernel';
|
|
7
|
+
export class CartConversionService {
|
|
8
|
+
emFactory;
|
|
9
|
+
cartService;
|
|
10
|
+
rfqService;
|
|
11
|
+
quoteRequests;
|
|
12
|
+
catalogProducts;
|
|
13
|
+
channelMembership;
|
|
14
|
+
constructor(emFactory, cartService,
|
|
15
|
+
/** `quote_requests`' customer-facing write surface. */
|
|
16
|
+
rfqService,
|
|
17
|
+
/** `quote_requests`' row read model — the quote and its lines. */
|
|
18
|
+
quoteRequests,
|
|
19
|
+
/** `catalog`'s product read model — is the quoted product still there? */
|
|
20
|
+
catalogProducts,
|
|
21
|
+
/**
|
|
22
|
+
* The sanctioned bridge accessor (Constitution XII) — and on this seam it
|
|
23
|
+
* is what keeps the *reason* honest (issue #259). `CartService.addItem`
|
|
24
|
+
* refuses an out-of-channel line by itself, but this method catches every
|
|
25
|
+
* `HttpError` from it and reports one dropped-line reason,
|
|
26
|
+
* `no_price_in_customer_list`. A line the channel does not sell has nothing
|
|
27
|
+
* to do with the buyer's price list, so it is narrowed here instead and
|
|
28
|
+
* falls into the `not_purchasable` branch — where a line whose product has
|
|
29
|
+
* gone already lands.
|
|
30
|
+
*/
|
|
31
|
+
channelMembership) {
|
|
32
|
+
this.emFactory = emFactory;
|
|
33
|
+
this.cartService = cartService;
|
|
34
|
+
this.rfqService = rfqService;
|
|
35
|
+
this.quoteRequests = quoteRequests;
|
|
36
|
+
this.catalogProducts = catalogProducts;
|
|
37
|
+
this.channelMembership = channelMembership;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Cart → Quote Request. Refuses on an empty cart. Source cart flips to
|
|
41
|
+
* `completed` with `converted_to_quote_request_id` set.
|
|
42
|
+
*/
|
|
43
|
+
async convertToQuoteRequest(cart, ctx, note) {
|
|
44
|
+
// command-coverage-ignore: the durable write is the RFQ (RfqService
|
|
45
|
+
// .createForCustomer, audited as quote_request.create); this only marks the
|
|
46
|
+
// ephemeral cart completed and clears its items — transient working state.
|
|
47
|
+
const em = this.emFactory();
|
|
48
|
+
const items = await em.find(CartItem, { cartId: cart.id });
|
|
49
|
+
if (items.length === 0) {
|
|
50
|
+
throw new HttpError(422, ERROR_CODES.CART_EMPTY, 'cart_empty');
|
|
51
|
+
}
|
|
52
|
+
const rfq = await this.rfqService.createForCustomer(ctx, {
|
|
53
|
+
...(note !== undefined ? { headerNote: note } : {}),
|
|
54
|
+
items: items.map((it) => ({
|
|
55
|
+
productId: it.productId,
|
|
56
|
+
...(it.variantId ? { variantId: it.variantId } : {}),
|
|
57
|
+
quantity: it.quantity,
|
|
58
|
+
// Feature 043 — carry the packaging-unit snapshot so the RFQ line
|
|
59
|
+
// shows "<name> (Paleta)" like the cart did.
|
|
60
|
+
...(it.packagingUnitName ? { packagingUnitName: it.packagingUnitName } : {}),
|
|
61
|
+
...(it.packagingUnitBaseQuantity != null
|
|
62
|
+
? { packagingUnitBaseQuantity: it.packagingUnitBaseQuantity }
|
|
63
|
+
: {}),
|
|
64
|
+
// Carry the buyer's currently-snapshotted unit price as the
|
|
65
|
+
// reference price on the QR line — sales sees what the cart
|
|
66
|
+
// would have cost without negotiation.
|
|
67
|
+
desiredUnitPrice: Number(it.unitPrice),
|
|
68
|
+
})),
|
|
69
|
+
});
|
|
70
|
+
// Flip the source cart to `completed` + record the conversion
|
|
71
|
+
// lineage. Reload on this service's own EM fork before mutating —
|
|
72
|
+
// the `cart` arg was loaded by the route handler's fork and a
|
|
73
|
+
// cross-fork flush would silently no-op (same bug we hit in
|
|
74
|
+
// CartCouponService.apply). Mirror the change back onto the
|
|
75
|
+
// caller's in-memory cart so the audit / response paths see the
|
|
76
|
+
// post-conversion state.
|
|
77
|
+
const managedCart = await em.findOne(Cart, { id: cart.id });
|
|
78
|
+
if (managedCart) {
|
|
79
|
+
managedCart.status = 'completed';
|
|
80
|
+
managedCart.convertedToQuoteRequestId = rfq.id;
|
|
81
|
+
managedCart.lastActivityAt = new Date();
|
|
82
|
+
}
|
|
83
|
+
await em.nativeDelete(CartItem, { cartId: cart.id });
|
|
84
|
+
await em.flush();
|
|
85
|
+
cart.status = 'completed';
|
|
86
|
+
cart.convertedToQuoteRequestId = rfq.id;
|
|
87
|
+
cart.lastActivityAt = managedCart?.lastActivityAt ?? new Date();
|
|
88
|
+
return { quoteRequestId: rfq.id, cartId: cart.id };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Quote Request → Cart. Creates a fresh `active` cart (or returns the
|
|
92
|
+
* existing one) for the buyer, appends the QR's lines with re-resolved
|
|
93
|
+
* prices (via `CartService.addItem`, which already routes through the
|
|
94
|
+
* pricing resolver when wired), and surfaces a list of dropped lines.
|
|
95
|
+
*
|
|
96
|
+
* The QR's `desiredUnitPrice` is intentionally dropped — the cart's
|
|
97
|
+
* unit prices come from the customer's current price list.
|
|
98
|
+
*/
|
|
99
|
+
async createCartFromQuoteRequest(qrId, ctx) {
|
|
100
|
+
const qr = await this.quoteRequests.findById(qrId);
|
|
101
|
+
if (!qr) {
|
|
102
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'quote_request_not_found');
|
|
103
|
+
}
|
|
104
|
+
// Anti-enumeration: only the requesting customer (or the Organization's
|
|
105
|
+
// org-admin members) may convert it. Foundation gates ownership via the
|
|
106
|
+
// RFQ-detail endpoint; we mirror the customer ownership check here.
|
|
107
|
+
if (qr.customerAccountId !== ctx.customerAccountId) {
|
|
108
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'quote_request_not_found');
|
|
109
|
+
}
|
|
110
|
+
const qrItems = await this.quoteRequests.listItems(qr.id);
|
|
111
|
+
if (qrItems.length === 0) {
|
|
112
|
+
throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'quote_request_empty');
|
|
113
|
+
}
|
|
114
|
+
const productIds = Array.from(new Set(qrItems.map((it) => it.productId)));
|
|
115
|
+
const products = await this.catalogProducts.findByIds(productIds);
|
|
116
|
+
const publishedHere = await productIdsInRequestChannel(this.channelMembership, productIds);
|
|
117
|
+
const productById = new Map(products
|
|
118
|
+
.filter((p) => publishedHere === null || publishedHere.has(p.id))
|
|
119
|
+
.map((p) => [p.id, p]));
|
|
120
|
+
const droppedLines = [];
|
|
121
|
+
let appendedLineCount = 0;
|
|
122
|
+
let targetCart = null;
|
|
123
|
+
for (const qrItem of qrItems) {
|
|
124
|
+
const product = productById.get(qrItem.productId);
|
|
125
|
+
if (!product) {
|
|
126
|
+
droppedLines.push({
|
|
127
|
+
productId: qrItem.productId,
|
|
128
|
+
productName: qrItem.productName ?? qrItem.productId,
|
|
129
|
+
reason: 'not_purchasable',
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const result = await this.cartService.addItem({ customer: ctx }, {
|
|
135
|
+
productId: qrItem.productId,
|
|
136
|
+
...(qrItem.variantId ? { variantId: qrItem.variantId } : {}),
|
|
137
|
+
quantity: qrItem.quantity,
|
|
138
|
+
});
|
|
139
|
+
targetCart = result.cart;
|
|
140
|
+
appendedLineCount += 1;
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
// The tolerance is per line and it is right: one unpriceable product
|
|
144
|
+
// must not abort a twenty-line conversion, and the buyer is told which
|
|
145
|
+
// lines were dropped and why.
|
|
146
|
+
//
|
|
147
|
+
// `rethrowIfModuleDisabled` is its first line because `addItem` reaches
|
|
148
|
+
// `catalog`'s and `price_lists`' gated ports, and `ModuleDisabledError`
|
|
149
|
+
// **is** an `HttpError` — so the `instanceof` test below would let a
|
|
150
|
+
// switched-off pricing engine through as "no price in your list" for
|
|
151
|
+
// every line, which is issue #124's defect wearing a per-line report.
|
|
152
|
+
rethrowIfModuleDisabled(err);
|
|
153
|
+
// CartService throws an HttpError when the product is quote-only
|
|
154
|
+
// (displayMode='none') or no resolver match — both translate to
|
|
155
|
+
// `no_price_in_customer_list` for the storefront banner.
|
|
156
|
+
if (err instanceof HttpError) {
|
|
157
|
+
droppedLines.push({
|
|
158
|
+
productId: qrItem.productId,
|
|
159
|
+
productName: qrItem.productName ?? qrItem.productId,
|
|
160
|
+
reason: 'no_price_in_customer_list',
|
|
161
|
+
});
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!targetCart) {
|
|
168
|
+
// Every line dropped — still create / fetch the customer's active
|
|
169
|
+
// cart so the caller has a destination cartId.
|
|
170
|
+
targetCart = await this.cartService.getOrCreateForCustomer(ctx);
|
|
171
|
+
}
|
|
172
|
+
return { cartId: targetCart.id, appendedLineCount, droppedLines };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=cart-conversion-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-conversion-service.js","sourceRoot":"","sources":["../../../src/backend/services/cart-conversion-service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAO3D,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAE3E,OAAO,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAoD9E,MAAM,OAAO,qBAAqB;IAEb;IACA;IAEA;IAEA;IAEA;IAWA;IAnBnB,YACmB,SAA8B,EAC9B,WAAwB;IACzC,uDAAuD;IACtC,UAA2B;IAC5C,kEAAkE;IACjD,aAAmC;IACpD,0EAA0E;IACzD,eAAuC;IACxD;;;;;;;;;OASG;IACc,iBAA6C;QAlB7C,cAAS,GAAT,SAAS,CAAqB;QAC9B,gBAAW,GAAX,WAAW,CAAa;QAExB,eAAU,GAAV,UAAU,CAAiB;QAE3B,kBAAa,GAAb,aAAa,CAAsB;QAEnC,oBAAe,GAAf,eAAe,CAAwB;QAWvC,sBAAiB,GAAjB,iBAAiB,CAA4B;IAC7D,CAAC;IAEJ;;;OAGG;IACH,KAAK,CAAC,qBAAqB,CACzB,IAAU,EACV,GAAuB,EACvB,IAAa;QAEb,oEAAoE;QACpE,4EAA4E;QAC5E,2EAA2E;QAC3E,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;QACjE,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,iBAAiB,CAAC,GAAG,EAAE;YACvD,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnD,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;gBACxB,SAAS,EAAE,EAAE,CAAC,SAAS;gBACvB,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACpD,QAAQ,EAAE,EAAE,CAAC,QAAQ;gBACrB,kEAAkE;gBAClE,6CAA6C;gBAC7C,GAAG,CAAC,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,EAAE,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC5E,GAAG,CAAC,EAAE,CAAC,yBAAyB,IAAI,IAAI;oBACtC,CAAC,CAAC,EAAE,yBAAyB,EAAE,EAAE,CAAC,yBAAyB,EAAE;oBAC7D,CAAC,CAAC,EAAE,CAAC;gBACP,4DAA4D;gBAC5D,4DAA4D;gBAC5D,uCAAuC;gBACvC,gBAAgB,EAAE,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC;aACvC,CAAC,CAAC;SACJ,CAAC,CAAC;QAEH,8DAA8D;QAC9D,kEAAkE;QAClE,8DAA8D;QAC9D,4DAA4D;QAC5D,4DAA4D;QAC5D,gEAAgE;QAChE,yBAAyB;QACzB,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5D,IAAI,WAAW,EAAE,CAAC;YAChB,WAAW,CAAC,MAAM,GAAG,WAAW,CAAC;YACjC,WAAW,CAAC,yBAAyB,GAAG,GAAG,CAAC,EAAE,CAAC;YAC/C,WAAW,CAAC,cAAc,GAAG,IAAI,IAAI,EAAE,CAAC;QAC1C,CAAC;QACD,MAAM,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QACrD,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC;QAC1B,IAAI,CAAC,yBAAyB,GAAG,GAAG,CAAC,EAAE,CAAC;QACxC,IAAI,CAAC,cAAc,GAAG,WAAW,EAAE,cAAc,IAAI,IAAI,IAAI,EAAE,CAAC;QAEhE,OAAO,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;IACrD,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,0BAA0B,CAC9B,IAAY,EACZ,GAAwB;QAExB,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,CAAC,EAAE,EAAE,CAAC;YACR,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,SAAS,EAAE,yBAAyB,CAAC,CAAC;QAC7E,CAAC;QACD,wEAAwE;QACxE,wEAAwE;QACxE,oEAAoE;QACpE,IAAI,EAAE,CAAC,iBAAiB,KAAK,GAAG,CAAC,iBAAiB,EAAE,CAAC;YACnD,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,SAAS,EAAE,yBAAyB,CAAC,CAAC;QAC7E,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAC1D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC,iBAAiB,EAAE,qBAAqB,CAAC,CAAC;QACjF,CAAC;QAED,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAC1E,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QAClE,MAAM,aAAa,GAAG,MAAM,0BAA0B,CAAC,IAAI,CAAC,iBAAiB,EAAE,UAAU,CAAC,CAAC;QAC3F,MAAM,WAAW,GAAG,IAAI,GAAG,CACzB,QAAQ;aACL,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,KAAK,IAAI,IAAI,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;aAChE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CACzB,CAAC;QAEF,MAAM,YAAY,GAAsB,EAAE,CAAC;QAC3C,IAAI,iBAAiB,GAAG,CAAC,CAAC;QAC1B,IAAI,UAAU,GAAgB,IAAI,CAAC;QAEnC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAClD,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,YAAY,CAAC,IAAI,CAAC;oBAChB,SAAS,EAAE,MAAM,CAAC,SAAS;oBAC3B,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,SAAS;oBACnD,MAAM,EAAE,iBAAiB;iBAC1B,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,CAC3C,EAAE,QAAQ,EAAE,GAAG,EAAE,EACjB;oBACE,SAAS,EAAE,MAAM,CAAC,SAAS;oBAC3B,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC5D,QAAQ,EAAE,MAAM,CAAC,QAAQ;iBAC1B,CACF,CAAC;gBACF,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC;gBACzB,iBAAiB,IAAI,CAAC,CAAC;YACzB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,qEAAqE;gBACrE,uEAAuE;gBACvE,8BAA8B;gBAC9B,EAAE;gBACF,wEAAwE;gBACxE,wEAAwE;gBACxE,qEAAqE;gBACrE,qEAAqE;gBACrE,sEAAsE;gBACtE,uBAAuB,CAAC,GAAG,CAAC,CAAC;gBAC7B,iEAAiE;gBACjE,gEAAgE;gBAChE,yDAAyD;gBACzD,IAAI,GAAG,YAAY,SAAS,EAAE,CAAC;oBAC7B,YAAY,CAAC,IAAI,CAAC;wBAChB,SAAS,EAAE,MAAM,CAAC,SAAS;wBAC3B,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,MAAM,CAAC,SAAS;wBACnD,MAAM,EAAE,2BAA2B;qBACpC,CAAC,CAAC;oBACH,SAAS;gBACX,CAAC;gBACD,MAAM,GAAG,CAAC;YACZ,CAAC;QACH,CAAC;QAED,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,kEAAkE;YAClE,+CAA+C;YAC/C,UAAU,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,sBAAsB,CAAC,GAAG,CAAC,CAAC;QAClE,CAAC;QAED,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,EAAE,iBAAiB,EAAE,YAAY,EAAE,CAAC;IACpE,CAAC;CACF"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { CouponDropReason, PromotionApplication } from '@endora-commerce/contracts';
|
|
3
|
+
import { Cart } from '../entities/cart.entity.js';
|
|
4
|
+
import { CartItem } from '../entities/cart-item.entity.js';
|
|
5
|
+
import type { CustomerAccountReadPort, OrganizationDetailsPort, PromotionApplyPort, PromotionCodePort } from '@endora-commerce/contracts';
|
|
6
|
+
import type { CartApprovalService } from './cart-approval-service.js';
|
|
7
|
+
/**
|
|
8
|
+
* Cart-level coupon application (feature 027 US2).
|
|
9
|
+
*
|
|
10
|
+
* Plumbs the buyer's coupon code through `PromotionService.applyToCart`:
|
|
11
|
+
* - `apply(cart, code)` writes the code on the cart and asks the
|
|
12
|
+
* promotion engine whether the named promotion would stick. If yes,
|
|
13
|
+
* the discount is reflected via the recomputed snapshot the caller
|
|
14
|
+
* returns to the buyer. If no, the cart's previous code (if any) is
|
|
15
|
+
* preserved and a typed `CouponApplyResult.dropped` is returned
|
|
16
|
+
* identifying the precise reason from the snapshot's eligibility
|
|
17
|
+
* filter.
|
|
18
|
+
* - `clear(cart)` removes the active code. Idempotent.
|
|
19
|
+
* - `reevaluateOnRead(cart, snapshot)` is called from the GET-cart
|
|
20
|
+
* read path. If a previously-applied code is no longer eligible
|
|
21
|
+
* (e.g. the cart fell below `min_cart_subtotal` after a line
|
|
22
|
+
* removal), the code is silently dropped and the caller can surface
|
|
23
|
+
* `couponDroppedThisRead` in the response.
|
|
24
|
+
*
|
|
25
|
+
* The service never mutates promotion rows — only `cart.applied_promotion_code`.
|
|
26
|
+
*/
|
|
27
|
+
export interface CouponApplyOk {
|
|
28
|
+
outcome: 'applied';
|
|
29
|
+
cart: Cart;
|
|
30
|
+
appliedCode: string;
|
|
31
|
+
}
|
|
32
|
+
export interface CouponApplyDropped {
|
|
33
|
+
outcome: 'dropped';
|
|
34
|
+
reason: CouponDropReason;
|
|
35
|
+
shortfall?: {
|
|
36
|
+
amount: number;
|
|
37
|
+
currency: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export type CouponApplyResult = CouponApplyOk | CouponApplyDropped;
|
|
41
|
+
export interface CouponReevaluateResult {
|
|
42
|
+
/** When non-null, the read path should populate `couponDroppedThisRead`. */
|
|
43
|
+
dropped: {
|
|
44
|
+
code: string;
|
|
45
|
+
reason: CouponDropReason;
|
|
46
|
+
} | null;
|
|
47
|
+
}
|
|
48
|
+
export declare class CartCouponService {
|
|
49
|
+
private readonly emFactory;
|
|
50
|
+
/** `promotions`' apply surface — the discount the engine computes. */
|
|
51
|
+
private readonly promotionService;
|
|
52
|
+
/**
|
|
53
|
+
* `promotions`' code-resolution port (feature 075, Phase C). The
|
|
54
|
+
* three-step lookup it replaces — legacy `promotions.code`, then
|
|
55
|
+
* `promotion_coupons`, then the promotion behind the coupon, each filtered
|
|
56
|
+
* on `isActive` — is that module's business, and this file reproduced it.
|
|
57
|
+
*/
|
|
58
|
+
private readonly promotionCodes;
|
|
59
|
+
/**
|
|
60
|
+
* Issue #177 — the two reads behind the buyer's effective customer group,
|
|
61
|
+
* which every snapshot this service builds used to pass as a literal
|
|
62
|
+
* `null`. A coupon or automatic promotion an operator restricted to a
|
|
63
|
+
* group therefore matched nobody.
|
|
64
|
+
*/
|
|
65
|
+
private readonly customerAccounts;
|
|
66
|
+
private readonly organizations;
|
|
67
|
+
private readonly approvalService?;
|
|
68
|
+
/**
|
|
69
|
+
* `approvalService` is optional — when wired, applying or clearing a
|
|
70
|
+
* coupon on an `approved` cart re-arms approval to `pending`. Legacy
|
|
71
|
+
* compositions without the approval surface omit it (re-arm is a
|
|
72
|
+
* no-op when absent).
|
|
73
|
+
*/
|
|
74
|
+
constructor(emFactory: () => EntityManager,
|
|
75
|
+
/** `promotions`' apply surface — the discount the engine computes. */
|
|
76
|
+
promotionService: PromotionApplyPort,
|
|
77
|
+
/**
|
|
78
|
+
* `promotions`' code-resolution port (feature 075, Phase C). The
|
|
79
|
+
* three-step lookup it replaces — legacy `promotions.code`, then
|
|
80
|
+
* `promotion_coupons`, then the promotion behind the coupon, each filtered
|
|
81
|
+
* on `isActive` — is that module's business, and this file reproduced it.
|
|
82
|
+
*/
|
|
83
|
+
promotionCodes: PromotionCodePort,
|
|
84
|
+
/**
|
|
85
|
+
* Issue #177 — the two reads behind the buyer's effective customer group,
|
|
86
|
+
* which every snapshot this service builds used to pass as a literal
|
|
87
|
+
* `null`. A coupon or automatic promotion an operator restricted to a
|
|
88
|
+
* group therefore matched nobody.
|
|
89
|
+
*/
|
|
90
|
+
customerAccounts: CustomerAccountReadPort, organizations: OrganizationDetailsPort, approvalService?: CartApprovalService | undefined);
|
|
91
|
+
/** The buyer's effective group: their own, else their Organization's. */
|
|
92
|
+
private customerGroupIdFor;
|
|
93
|
+
/**
|
|
94
|
+
* Apply (or replace) the active coupon on the cart. Validates against
|
|
95
|
+
* the promotion engine using the current cart contents.
|
|
96
|
+
*
|
|
97
|
+
* `code === null` clears any active coupon (use the `clear` alias for
|
|
98
|
+
* readability — both are equivalent).
|
|
99
|
+
*
|
|
100
|
+
* Note: the cart passed in may have been loaded by a different
|
|
101
|
+
* EntityManager fork. We re-load it on our own fork so the
|
|
102
|
+
* `appliedPromotionCode` mutation flushes to the DB and the
|
|
103
|
+
* caller's in-memory cart is also updated.
|
|
104
|
+
*/
|
|
105
|
+
apply(cart: Cart, code: string | null): Promise<CouponApplyResult>;
|
|
106
|
+
/**
|
|
107
|
+
* Feature 045 — compute the promotion application for a cart on read, so
|
|
108
|
+
* the serialized cart can show the real discount amount + per-promotion
|
|
109
|
+
* breakdown (automatic action-based promotions plus any applied coupon).
|
|
110
|
+
* Pure read — never mutates the cart.
|
|
111
|
+
*/
|
|
112
|
+
computeApplication(cart: Cart, items: CartItem[]): Promise<PromotionApplication>;
|
|
113
|
+
clear(cart: Cart): Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* Called from the GET-cart read path. If a previously-applied code is
|
|
116
|
+
* no longer eligible (e.g. the cart fell below `min_cart_subtotal`),
|
|
117
|
+
* silently drops it on the cart record and returns a typed result the
|
|
118
|
+
* caller can surface as `couponDroppedThisRead` in the response.
|
|
119
|
+
*/
|
|
120
|
+
reevaluateOnRead(cart: Cart, items: CartItem[]): Promise<CouponReevaluateResult>;
|
|
121
|
+
/**
|
|
122
|
+
* Persists a coupon drop. The caller's `cart` entity may belong to a
|
|
123
|
+
* different EntityManager fork; we reload on our own fork, mutate, and
|
|
124
|
+
* also mirror the change onto the caller's in-memory cart so the
|
|
125
|
+
* read-side serializer sees the dropped coupon.
|
|
126
|
+
*/
|
|
127
|
+
private persistDrop;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=cart-coupon-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-coupon-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cart-coupon-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAgB,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACvG,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAC3D,OAAO,KAAK,EACV,uBAAuB,EACvB,uBAAuB,EACvB,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAGtE;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,SAAS,CAAC;IACnB,IAAI,EAAE,IAAI,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,SAAS,CAAC;IACnB,MAAM,EAAE,gBAAgB,CAAC;IACzB,SAAS,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CAClD;AAED,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,kBAAkB,CAAC;AAEnE,MAAM,WAAW,sBAAsB;IACrC,4EAA4E;IAC5E,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,gBAAgB,CAAA;KAAE,GAAG,IAAI,CAAC;CAC5D;AAED,qBAAa,iBAAiB;IAQ1B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,sEAAsE;IACtE,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B;;;;;OAKG;IACH,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC;IAzBnC;;;;;OAKG;gBAEgB,SAAS,EAAE,MAAM,aAAa;IAC/C,sEAAsE;IACrD,gBAAgB,EAAE,kBAAkB;IACrD;;;;;OAKG;IACc,cAAc,EAAE,iBAAiB;IAClD;;;;;OAKG;IACc,gBAAgB,EAAE,uBAAuB,EACzC,aAAa,EAAE,uBAAuB,EACtC,eAAe,CAAC,EAAE,mBAAmB,YAAA;IAGxD,yEAAyE;IACzE,OAAO,CAAC,kBAAkB;IAU1B;;;;;;;;;;;OAWG;IACG,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAoGxE;;;;;OAKG;IACG,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAoBhF,KAAK,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBtC;;;;;OAKG;IACG,gBAAgB,CACpB,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,QAAQ,EAAE,GAChB,OAAO,CAAC,sBAAsB,CAAC;IAoClC;;;;;OAKG;YACW,WAAW;CAQ1B"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { Cart } from '../entities/cart.entity.js';
|
|
2
|
+
import { CartItem } from '../entities/cart-item.entity.js';
|
|
3
|
+
import { resolveCartCustomerGroupId } from './customer-group-resolver.js';
|
|
4
|
+
export class CartCouponService {
|
|
5
|
+
emFactory;
|
|
6
|
+
promotionService;
|
|
7
|
+
promotionCodes;
|
|
8
|
+
customerAccounts;
|
|
9
|
+
organizations;
|
|
10
|
+
approvalService;
|
|
11
|
+
/**
|
|
12
|
+
* `approvalService` is optional — when wired, applying or clearing a
|
|
13
|
+
* coupon on an `approved` cart re-arms approval to `pending`. Legacy
|
|
14
|
+
* compositions without the approval surface omit it (re-arm is a
|
|
15
|
+
* no-op when absent).
|
|
16
|
+
*/
|
|
17
|
+
constructor(emFactory,
|
|
18
|
+
/** `promotions`' apply surface — the discount the engine computes. */
|
|
19
|
+
promotionService,
|
|
20
|
+
/**
|
|
21
|
+
* `promotions`' code-resolution port (feature 075, Phase C). The
|
|
22
|
+
* three-step lookup it replaces — legacy `promotions.code`, then
|
|
23
|
+
* `promotion_coupons`, then the promotion behind the coupon, each filtered
|
|
24
|
+
* on `isActive` — is that module's business, and this file reproduced it.
|
|
25
|
+
*/
|
|
26
|
+
promotionCodes,
|
|
27
|
+
/**
|
|
28
|
+
* Issue #177 — the two reads behind the buyer's effective customer group,
|
|
29
|
+
* which every snapshot this service builds used to pass as a literal
|
|
30
|
+
* `null`. A coupon or automatic promotion an operator restricted to a
|
|
31
|
+
* group therefore matched nobody.
|
|
32
|
+
*/
|
|
33
|
+
customerAccounts, organizations, approvalService) {
|
|
34
|
+
this.emFactory = emFactory;
|
|
35
|
+
this.promotionService = promotionService;
|
|
36
|
+
this.promotionCodes = promotionCodes;
|
|
37
|
+
this.customerAccounts = customerAccounts;
|
|
38
|
+
this.organizations = organizations;
|
|
39
|
+
this.approvalService = approvalService;
|
|
40
|
+
}
|
|
41
|
+
/** The buyer's effective group: their own, else their Organization's. */
|
|
42
|
+
customerGroupIdFor(cart) {
|
|
43
|
+
return resolveCartCustomerGroupId({ customerAccounts: this.customerAccounts, organizations: this.organizations }, {
|
|
44
|
+
customerAccountId: cart.customerAccountId ?? null,
|
|
45
|
+
organizationId: cart.organizationId ?? null,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Apply (or replace) the active coupon on the cart. Validates against
|
|
50
|
+
* the promotion engine using the current cart contents.
|
|
51
|
+
*
|
|
52
|
+
* `code === null` clears any active coupon (use the `clear` alias for
|
|
53
|
+
* readability — both are equivalent).
|
|
54
|
+
*
|
|
55
|
+
* Note: the cart passed in may have been loaded by a different
|
|
56
|
+
* EntityManager fork. We re-load it on our own fork so the
|
|
57
|
+
* `appliedPromotionCode` mutation flushes to the DB and the
|
|
58
|
+
* caller's in-memory cart is also updated.
|
|
59
|
+
*/
|
|
60
|
+
async apply(cart, code) {
|
|
61
|
+
// command-coverage-ignore: transient cart coupon state — the durable applied
|
|
62
|
+
// promotion is captured on the order at checkout (audited there); the cart is
|
|
63
|
+
// ephemeral pre-order working state.
|
|
64
|
+
if (code === null) {
|
|
65
|
+
await this.clear(cart);
|
|
66
|
+
return { outcome: 'applied', cart, appliedCode: '' };
|
|
67
|
+
}
|
|
68
|
+
const em = this.emFactory();
|
|
69
|
+
const managedCart = await em.findOne(Cart, { id: cart.id });
|
|
70
|
+
if (!managedCart) {
|
|
71
|
+
return { outcome: 'dropped', reason: 'invalid_code' };
|
|
72
|
+
}
|
|
73
|
+
// Resolve the promotion row by code so we can build the reason-specific
|
|
74
|
+
// drop response (the snapshot's eligibility filter inside applyToCart
|
|
75
|
+
// is opaque to callers, so we re-implement the precondition checks
|
|
76
|
+
// here for the reason mapping).
|
|
77
|
+
const promotion = await this.promotionCodes.resolveByCode(code);
|
|
78
|
+
if (!promotion) {
|
|
79
|
+
return { outcome: 'dropped', reason: 'invalid_code' };
|
|
80
|
+
}
|
|
81
|
+
const now = new Date();
|
|
82
|
+
if (promotion.validFrom && now < promotion.validFrom) {
|
|
83
|
+
return { outcome: 'dropped', reason: 'expired' };
|
|
84
|
+
}
|
|
85
|
+
if (promotion.validUntil && now > promotion.validUntil) {
|
|
86
|
+
return { outcome: 'dropped', reason: 'expired' };
|
|
87
|
+
}
|
|
88
|
+
if (promotion.organizationId && promotion.organizationId !== managedCart.organizationId) {
|
|
89
|
+
return { outcome: 'dropped', reason: 'wrong_organization' };
|
|
90
|
+
}
|
|
91
|
+
const items = await em.find(CartItem, { cartId: managedCart.id });
|
|
92
|
+
const subtotal = items.reduce((acc, it) => acc + Number(it.unitPrice) * it.quantity, 0);
|
|
93
|
+
const currency = items[0]?.currency ?? 'PLN';
|
|
94
|
+
if (promotion.minCartSubtotal != null) {
|
|
95
|
+
const min = Number(promotion.minCartSubtotal);
|
|
96
|
+
if (min > subtotal) {
|
|
97
|
+
return {
|
|
98
|
+
outcome: 'dropped',
|
|
99
|
+
reason: 'below_min_spend',
|
|
100
|
+
shortfall: { amount: round2(min - subtotal), currency },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Double-check via the promotion engine — this catches any criteria
|
|
105
|
+
// we don't replicate here (e.g. attribute/category rules from
|
|
106
|
+
// feature 012).
|
|
107
|
+
const snapshot = {
|
|
108
|
+
organizationId: cart.organizationId ?? null,
|
|
109
|
+
customerGroupId: await this.customerGroupIdFor(managedCart),
|
|
110
|
+
currency,
|
|
111
|
+
lines: items.map((it) => ({
|
|
112
|
+
productId: it.productId,
|
|
113
|
+
variantId: it.variantId ?? null,
|
|
114
|
+
// Feature 012 / US8 category criteria — hydrated by the engine
|
|
115
|
+
// when needed. Empty seed keeps the contract satisfied.
|
|
116
|
+
categoryIds: [],
|
|
117
|
+
quantity: it.quantity,
|
|
118
|
+
unitPrice: { amount: Number(it.unitPrice), currency: it.currency },
|
|
119
|
+
})),
|
|
120
|
+
deliveryTotal: 0,
|
|
121
|
+
promotionCode: code,
|
|
122
|
+
};
|
|
123
|
+
const application = await this.promotionService.applyToCart(snapshot);
|
|
124
|
+
const stuck = application.appliedPromotions.some((ap) => ap.promotionId === promotion.promotionId);
|
|
125
|
+
if (!stuck) {
|
|
126
|
+
// Eligible by the local pre-checks but rejected by the engine's
|
|
127
|
+
// criteria — bucket as `wrong_customer_group` since that's the
|
|
128
|
+
// most common criterion we don't pre-check here. (The promotion
|
|
129
|
+
// engine doesn't expose a typed reason, so this is the best
|
|
130
|
+
// generic mapping.)
|
|
131
|
+
return { outcome: 'dropped', reason: 'wrong_customer_group' };
|
|
132
|
+
}
|
|
133
|
+
managedCart.appliedPromotionCode = code;
|
|
134
|
+
managedCart.lastActivityAt = new Date();
|
|
135
|
+
await em.flush();
|
|
136
|
+
// Mirror the persisted mutation onto the caller's in-memory cart so
|
|
137
|
+
// serializeCart in the route handler reflects it without re-reading.
|
|
138
|
+
cart.appliedPromotionCode = code;
|
|
139
|
+
cart.lastActivityAt = managedCart.lastActivityAt;
|
|
140
|
+
if (this.approvalService && managedCart.customerAccountId) {
|
|
141
|
+
await this.approvalService.maybeReArm(managedCart, {
|
|
142
|
+
customerAccountId: managedCart.customerAccountId,
|
|
143
|
+
});
|
|
144
|
+
cart.approvalStatus = managedCart.approvalStatus;
|
|
145
|
+
}
|
|
146
|
+
return { outcome: 'applied', cart, appliedCode: code };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Feature 045 — compute the promotion application for a cart on read, so
|
|
150
|
+
* the serialized cart can show the real discount amount + per-promotion
|
|
151
|
+
* breakdown (automatic action-based promotions plus any applied coupon).
|
|
152
|
+
* Pure read — never mutates the cart.
|
|
153
|
+
*/
|
|
154
|
+
async computeApplication(cart, items) {
|
|
155
|
+
const currency = items[0]?.currency ?? 'PLN';
|
|
156
|
+
const snapshot = {
|
|
157
|
+
organizationId: cart.organizationId ?? null,
|
|
158
|
+
customerGroupId: await this.customerGroupIdFor(cart),
|
|
159
|
+
currency,
|
|
160
|
+
lines: items.map((it) => ({
|
|
161
|
+
productId: it.productId,
|
|
162
|
+
variantId: it.variantId ?? null,
|
|
163
|
+
categoryIds: [],
|
|
164
|
+
quantity: it.quantity,
|
|
165
|
+
unitPrice: { amount: Number(it.unitPrice), currency: it.currency },
|
|
166
|
+
})),
|
|
167
|
+
deliveryTotal: 0,
|
|
168
|
+
promotionCode: cart.appliedPromotionCode ?? null,
|
|
169
|
+
salesChannelId: cart.salesChannelId ?? null,
|
|
170
|
+
};
|
|
171
|
+
return this.promotionService.applyToCart(snapshot);
|
|
172
|
+
}
|
|
173
|
+
async clear(cart) {
|
|
174
|
+
// command-coverage-ignore: transient cart coupon state (see apply()).
|
|
175
|
+
if (cart.appliedPromotionCode === null)
|
|
176
|
+
return;
|
|
177
|
+
const em = this.emFactory();
|
|
178
|
+
const managedCart = await em.findOne(Cart, { id: cart.id });
|
|
179
|
+
if (!managedCart)
|
|
180
|
+
return;
|
|
181
|
+
managedCart.appliedPromotionCode = null;
|
|
182
|
+
managedCart.lastActivityAt = new Date();
|
|
183
|
+
await em.flush();
|
|
184
|
+
cart.appliedPromotionCode = null;
|
|
185
|
+
cart.lastActivityAt = managedCart.lastActivityAt;
|
|
186
|
+
if (this.approvalService && managedCart.customerAccountId) {
|
|
187
|
+
await this.approvalService.maybeReArm(managedCart, {
|
|
188
|
+
customerAccountId: managedCart.customerAccountId,
|
|
189
|
+
});
|
|
190
|
+
cart.approvalStatus = managedCart.approvalStatus;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Called from the GET-cart read path. If a previously-applied code is
|
|
195
|
+
* no longer eligible (e.g. the cart fell below `min_cart_subtotal`),
|
|
196
|
+
* silently drops it on the cart record and returns a typed result the
|
|
197
|
+
* caller can surface as `couponDroppedThisRead` in the response.
|
|
198
|
+
*/
|
|
199
|
+
async reevaluateOnRead(cart, items) {
|
|
200
|
+
if (!cart.appliedPromotionCode)
|
|
201
|
+
return { dropped: null };
|
|
202
|
+
const code = cart.appliedPromotionCode;
|
|
203
|
+
const em = this.emFactory();
|
|
204
|
+
const promotion = await this.promotionCodes.resolveByCode(code);
|
|
205
|
+
if (!promotion) {
|
|
206
|
+
await this.persistDrop(em, cart);
|
|
207
|
+
return { dropped: { code, reason: 'invalid_code' } };
|
|
208
|
+
}
|
|
209
|
+
const now = new Date();
|
|
210
|
+
if ((promotion.validFrom && now < promotion.validFrom) ||
|
|
211
|
+
(promotion.validUntil && now > promotion.validUntil)) {
|
|
212
|
+
await this.persistDrop(em, cart);
|
|
213
|
+
return { dropped: { code, reason: 'expired' } };
|
|
214
|
+
}
|
|
215
|
+
const subtotal = items.reduce((acc, it) => acc + Number(it.unitPrice) * it.quantity, 0);
|
|
216
|
+
if (promotion.minCartSubtotal != null) {
|
|
217
|
+
const min = Number(promotion.minCartSubtotal);
|
|
218
|
+
if (min > subtotal) {
|
|
219
|
+
await this.persistDrop(em, cart);
|
|
220
|
+
return { dropped: { code, reason: 'below_min_spend' } };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return { dropped: null };
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Persists a coupon drop. The caller's `cart` entity may belong to a
|
|
227
|
+
* different EntityManager fork; we reload on our own fork, mutate, and
|
|
228
|
+
* also mirror the change onto the caller's in-memory cart so the
|
|
229
|
+
* read-side serializer sees the dropped coupon.
|
|
230
|
+
*/
|
|
231
|
+
async persistDrop(em, cart) {
|
|
232
|
+
// command-coverage-ignore: transient cart coupon state (see apply()).
|
|
233
|
+
const managed = await em.findOne(Cart, { id: cart.id });
|
|
234
|
+
if (!managed)
|
|
235
|
+
return;
|
|
236
|
+
managed.appliedPromotionCode = null;
|
|
237
|
+
await em.flush();
|
|
238
|
+
cart.appliedPromotionCode = null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function round2(n) {
|
|
242
|
+
return Math.round(n * 100) / 100;
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=cart-coupon-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-coupon-service.js","sourceRoot":"","sources":["../../../src/backend/services/cart-coupon-service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAQ3D,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AA0C1E,MAAM,OAAO,iBAAiB;IAQT;IAEA;IAOA;IAOA;IACA;IACA;IAzBnB;;;;;OAKG;IACH,YACmB,SAA8B;IAC/C,sEAAsE;IACrD,gBAAoC;IACrD;;;;;OAKG;IACc,cAAiC;IAClD;;;;;OAKG;IACc,gBAAyC,EACzC,aAAsC,EACtC,eAAqC;QAlBrC,cAAS,GAAT,SAAS,CAAqB;QAE9B,qBAAgB,GAAhB,gBAAgB,CAAoB;QAOpC,mBAAc,GAAd,cAAc,CAAmB;QAOjC,qBAAgB,GAAhB,gBAAgB,CAAyB;QACzC,kBAAa,GAAb,aAAa,CAAyB;QACtC,oBAAe,GAAf,eAAe,CAAsB;IACrD,CAAC;IAEJ,yEAAyE;IACjE,kBAAkB,CAAC,IAAU;QACnC,OAAO,0BAA0B,CAC/B,EAAE,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,EAAE,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,EAC9E;YACE,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,IAAI,IAAI;YACjD,cAAc,EAAE,IAAI,CAAC,cAAc,IAAI,IAAI;SAC5C,CACF,CAAC;IACJ,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,KAAK,CAAC,IAAU,EAAE,IAAmB;QACzC,6EAA6E;QAC7E,8EAA8E;QAC9E,qCAAqC;QACrC,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,MAAM,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACvB,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;QACvD,CAAC;QAED,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;QACxD,CAAC;QAED,wEAAwE;QACxE,sEAAsE;QACtE,mEAAmE;QACnE,gCAAgC;QAChC,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QAChE,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;QACxD,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,SAAS,CAAC,SAAS,IAAI,GAAG,GAAG,SAAS,CAAC,SAAS,EAAE,CAAC;YACrD,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QACnD,CAAC;QACD,IAAI,SAAS,CAAC,UAAU,IAAI,GAAG,GAAG,SAAS,CAAC,UAAU,EAAE,CAAC;YACvD,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QACnD,CAAC;QACD,IAAI,SAAS,CAAC,cAAc,IAAI,SAAS,CAAC,cAAc,KAAK,WAAW,CAAC,cAAc,EAAE,CAAC;YACxF,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;QAC9D,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,EAAE,CAAC,CAAC;QAClE,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAC3B,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,GAAG,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,QAAQ,EACrD,CAAC,CACF,CAAC;QACF,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,IAAI,KAAK,CAAC;QAE7C,IAAI,SAAS,CAAC,eAAe,IAAI,IAAI,EAAE,CAAC;YACtC,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;YAC9C,IAAI,GAAG,GAAG,QAAQ,EAAE,CAAC;gBACnB,OAAO;oBACL,OAAO,EAAE,SAAS;oBAClB,MAAM,EAAE,iBAAiB;oBACzB,SAAS,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,GAAG,QAAQ,CAAC,EAAE,QAAQ,EAAE;iBACxD,CAAC;YACJ,CAAC;QACH,CAAC;QAED,oEAAoE;QACpE,8DAA8D;QAC9D,gBAAgB;QAChB,MAAM,QAAQ,GAAiB;YAC7B,cAAc,EAAE,IAAI,CAAC,cAAc,IAAI,IAAI;YAC3C,eAAe,EAAE,MAAM,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC;YAC3D,QAAQ;YACR,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;gBACxB,SAAS,EAAE,EAAE,CAAC,SAAS;gBACvB,SAAS,EAAE,EAAE,CAAC,SAAS,IAAI,IAAI;gBAC/B,+DAA+D;gBAC/D,wDAAwD;gBACxD,WAAW,EAAE,EAAE;gBACf,QAAQ,EAAE,EAAE,CAAC,QAAQ;gBACrB,SAAS,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE;aACnE,CAAC,CAAC;YACH,aAAa,EAAE,CAAC;YAChB,aAAa,EAAE,IAAI;SACpB,CAAC;QAEF,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QACtE,MAAM,KAAK,GAAG,WAAW,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,WAAW,KAAK,SAAS,CAAC,WAAW,CAAC,CAAC;QACnG,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,gEAAgE;YAChE,+DAA+D;YAC/D,gEAAgE;YAChE,4DAA4D;YAC5D,oBAAoB;YACpB,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAC;QAChE,CAAC;QAED,WAAW,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACxC,WAAW,CAAC,cAAc,GAAG,IAAI,IAAI,EAAE,CAAC;QACxC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QACjB,oEAAoE;QACpE,qEAAqE;QACrE,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACjC,IAAI,CAAC,cAAc,GAAG,WAAW,CAAC,cAAc,CAAC;QACjD,IAAI,IAAI,CAAC,eAAe,IAAI,WAAW,CAAC,iBAAiB,EAAE,CAAC;YAC1D,MAAM,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,WAAW,EAAE;gBACjD,iBAAiB,EAAE,WAAW,CAAC,iBAAiB;aACjD,CAAC,CAAC;YACH,IAAI,CAAC,cAAc,GAAG,WAAW,CAAC,cAAc,CAAC;QACnD,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IACzD,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,kBAAkB,CAAC,IAAU,EAAE,KAAiB;QACpD,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,IAAI,KAAK,CAAC;QAC7C,MAAM,QAAQ,GAAiB;YAC7B,cAAc,EAAE,IAAI,CAAC,cAAc,IAAI,IAAI;YAC3C,eAAe,EAAE,MAAM,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC;YACpD,QAAQ;YACR,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;gBACxB,SAAS,EAAE,EAAE,CAAC,SAAS;gBACvB,SAAS,EAAE,EAAE,CAAC,SAAS,IAAI,IAAI;gBAC/B,WAAW,EAAE,EAAE;gBACf,QAAQ,EAAE,EAAE,CAAC,QAAQ;gBACrB,SAAS,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE;aACnE,CAAC,CAAC;YACH,aAAa,EAAE,CAAC;YAChB,aAAa,EAAE,IAAI,CAAC,oBAAoB,IAAI,IAAI;YAChD,cAAc,EAAE,IAAI,CAAC,cAAc,IAAI,IAAI;SAC5C,CAAC;QACF,OAAO,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,IAAU;QACpB,sEAAsE;QACtE,IAAI,IAAI,CAAC,oBAAoB,KAAK,IAAI;YAAE,OAAO;QAC/C,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,WAAW;YAAE,OAAO;QACzB,WAAW,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACxC,WAAW,CAAC,cAAc,GAAG,IAAI,IAAI,EAAE,CAAC;QACxC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QACjB,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACjC,IAAI,CAAC,cAAc,GAAG,WAAW,CAAC,cAAc,CAAC;QACjD,IAAI,IAAI,CAAC,eAAe,IAAI,WAAW,CAAC,iBAAiB,EAAE,CAAC;YAC1D,MAAM,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,WAAW,EAAE;gBACjD,iBAAiB,EAAE,WAAW,CAAC,iBAAiB;aACjD,CAAC,CAAC;YACH,IAAI,CAAC,cAAc,GAAG,WAAW,CAAC,cAAc,CAAC;QACnD,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,gBAAgB,CACpB,IAAU,EACV,KAAiB;QAEjB,IAAI,CAAC,IAAI,CAAC,oBAAoB;YAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACzD,MAAM,IAAI,GAAG,IAAI,CAAC,oBAAoB,CAAC;QAEvC,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QAChE,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YACjC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,EAAE,CAAC;QACvD,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IACE,CAAC,SAAS,CAAC,SAAS,IAAI,GAAG,GAAG,SAAS,CAAC,SAAS,CAAC;YAClD,CAAC,SAAS,CAAC,UAAU,IAAI,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,EACpD,CAAC;YACD,MAAM,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YACjC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,CAAC;QAClD,CAAC;QAED,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAC3B,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,GAAG,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,QAAQ,EACrD,CAAC,CACF,CAAC;QAEF,IAAI,SAAS,CAAC,eAAe,IAAI,IAAI,EAAE,CAAC;YACtC,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;YAC9C,IAAI,GAAG,GAAG,QAAQ,EAAE,CAAC;gBACnB,MAAM,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;gBACjC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,iBAAiB,EAAE,EAAE,CAAC;YAC1D,CAAC;QACH,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,WAAW,CAAC,EAAiB,EAAE,IAAU;QACrD,sEAAsE;QACtE,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QACxD,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACpC,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QACjB,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;IACnC,CAAC;CACF;AAED,SAAS,MAAM,CAAC,CAAS;IACvB,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { CartStatus, CartApprovalStatus, CustomerAccountReadPort, OrgCartsListResponse } from '@endora-commerce/contracts';
|
|
3
|
+
import { Cart } from '../entities/cart.entity.js';
|
|
4
|
+
/**
|
|
5
|
+
* Organization-Administrator visibility surface (feature 027 US4).
|
|
6
|
+
*
|
|
7
|
+
* Lists every cart belonging to a member of the caller's Organization.
|
|
8
|
+
* The role gate is enforced at the route level; this service trusts
|
|
9
|
+
* its caller's `actor.organizationId` and refuses anti-enumeration
|
|
10
|
+
* lookups against other organizations.
|
|
11
|
+
*/
|
|
12
|
+
export interface OrgCartsListQuery {
|
|
13
|
+
status?: CartStatus[];
|
|
14
|
+
approvalStatus?: CartApprovalStatus[];
|
|
15
|
+
page?: number;
|
|
16
|
+
pageSize?: number;
|
|
17
|
+
sort?: 'last_activity_desc' | 'last_activity_asc' | 'total_desc' | 'total_asc' | 'created_desc';
|
|
18
|
+
}
|
|
19
|
+
export declare class CartOrganizationVisibilityService {
|
|
20
|
+
private readonly emFactory;
|
|
21
|
+
/**
|
|
22
|
+
* `customer_accounts`' read model (feature 075, Phase C). The org-admin
|
|
23
|
+
* cart list renders each cart's owner by e-mail, and it read the account
|
|
24
|
+
* row itself — out of a table deactivation leaves in place.
|
|
25
|
+
*/
|
|
26
|
+
private readonly customerAccounts;
|
|
27
|
+
constructor(emFactory: () => EntityManager,
|
|
28
|
+
/**
|
|
29
|
+
* `customer_accounts`' read model (feature 075, Phase C). The org-admin
|
|
30
|
+
* cart list renders each cart's owner by e-mail, and it read the account
|
|
31
|
+
* row itself — out of a table deactivation leaves in place.
|
|
32
|
+
*/
|
|
33
|
+
customerAccounts: CustomerAccountReadPort);
|
|
34
|
+
list(organizationId: string, query?: OrgCartsListQuery): Promise<OrgCartsListResponse>;
|
|
35
|
+
/**
|
|
36
|
+
* Fetch a single cart in the caller's Organization. Refuses with 404
|
|
37
|
+
* when the cart belongs to a different Organization (anti-enumeration
|
|
38
|
+
* symmetry with the ordinary-member ownership check).
|
|
39
|
+
*/
|
|
40
|
+
getInOrganization(cartId: string, organizationId: string): Promise<Cart>;
|
|
41
|
+
private sortClause;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=cart-organization-visibility-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-organization-visibility-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cart-organization-visibility-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EACV,UAAU,EACV,kBAAkB,EAClB,uBAAuB,EACvB,oBAAoB,EACrB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAGlD;;;;;;;GAOG;AAEH,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC;IACtB,cAAc,CAAC,EAAE,kBAAkB,EAAE,CAAC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,oBAAoB,GAAG,mBAAmB,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,CAAC;CACjG;AAED,qBAAa,iCAAiC;IAE1C,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,gBAAgB;gBANhB,SAAS,EAAE,MAAM,aAAa;IAC/C;;;;OAIG;IACc,gBAAgB,EAAE,uBAAuB;IAGtD,IAAI,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,GAAE,iBAAsB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IA0DhG;;;;OAIG;IACG,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS9E,OAAO,CAAC,UAAU;CAgBnB"}
|