@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,242 @@
|
|
|
1
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
import { Cart } from '../entities/cart.entity.js';
|
|
4
|
+
import { CartItem } from '../entities/cart-item.entity.js';
|
|
5
|
+
export class CartApprovalService {
|
|
6
|
+
emFactory;
|
|
7
|
+
cartAuditService;
|
|
8
|
+
organizations;
|
|
9
|
+
organizationCartApproval;
|
|
10
|
+
customerAccounts;
|
|
11
|
+
emailDispatch;
|
|
12
|
+
constructor(emFactory, cartAuditService,
|
|
13
|
+
/** `organizations`' read model — the approval policy and its owner row. */
|
|
14
|
+
organizations,
|
|
15
|
+
/**
|
|
16
|
+
* `organizations`' write surface for the policy column (issue #175). The
|
|
17
|
+
* two `setPolicy*` methods below used to assign
|
|
18
|
+
* `organizations.requires_cart_approval` on that module's entity; the write
|
|
19
|
+
* belongs to its owner, which audits it through the Command Bus. What stays
|
|
20
|
+
* here is the cascade over this module's own carts.
|
|
21
|
+
*/
|
|
22
|
+
organizationCartApproval,
|
|
23
|
+
/** `customer_accounts`' read model — the submitting buyer's role. */
|
|
24
|
+
customerAccounts, emailDispatch) {
|
|
25
|
+
this.emFactory = emFactory;
|
|
26
|
+
this.cartAuditService = cartAuditService;
|
|
27
|
+
this.organizations = organizations;
|
|
28
|
+
this.organizationCartApproval = organizationCartApproval;
|
|
29
|
+
this.customerAccounts = customerAccounts;
|
|
30
|
+
this.emailDispatch = emailDispatch;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Fire-and-forget e-mail dispatch. Wraps each call so any throw is
|
|
34
|
+
* swallowed — approval transitions must complete even if mail
|
|
35
|
+
* delivery hiccups. Production composition is responsible for
|
|
36
|
+
* surfacing the failure through the platform's log + alerting paths.
|
|
37
|
+
*/
|
|
38
|
+
async dispatch(fn) {
|
|
39
|
+
if (!this.emailDispatch)
|
|
40
|
+
return;
|
|
41
|
+
try {
|
|
42
|
+
await fn();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Swallow — see contract on CartEmailDispatch.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Buyer submits the cart for Org-Admin approval. Refuses if:
|
|
50
|
+
* - the cart is empty
|
|
51
|
+
* - the policy is off (approval not required)
|
|
52
|
+
* - the buyer is themself an Org Admin (self-approval exemption)
|
|
53
|
+
* - approval_status is already non-`not_required`
|
|
54
|
+
*/
|
|
55
|
+
async submitForApproval(cart, actor) {
|
|
56
|
+
const em = this.emFactory();
|
|
57
|
+
if (!cart.organizationId || cart.organizationId !== actor.organizationId) {
|
|
58
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'cart_not_found');
|
|
59
|
+
}
|
|
60
|
+
const items = await em.find(CartItem, { cartId: cart.id });
|
|
61
|
+
if (items.length === 0) {
|
|
62
|
+
throw new HttpError(422, ERROR_CODES.CART_EMPTY, 'cart_empty');
|
|
63
|
+
}
|
|
64
|
+
if (cart.approvalStatus !== 'not_required') {
|
|
65
|
+
throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'approval_already_initiated');
|
|
66
|
+
}
|
|
67
|
+
const org = await this.organizations.findById(actor.organizationId);
|
|
68
|
+
if (!org || !org.requiresCartApproval) {
|
|
69
|
+
throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'approval_not_required');
|
|
70
|
+
}
|
|
71
|
+
const buyer = await this.customerAccounts.findById(actor.customerAccountId);
|
|
72
|
+
if (buyer?.role === 'organization_admin') {
|
|
73
|
+
throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'org_admin_self_submit');
|
|
74
|
+
}
|
|
75
|
+
cart.approvalStatus = 'pending';
|
|
76
|
+
cart.submittedForApprovalAt = new Date();
|
|
77
|
+
cart.lastActivityAt = new Date();
|
|
78
|
+
await em.flush();
|
|
79
|
+
await this.cartAuditService.record({
|
|
80
|
+
cartId: cart.id,
|
|
81
|
+
actorType: 'customer',
|
|
82
|
+
actorId: actor.customerAccountId,
|
|
83
|
+
action: 'approval_submitted',
|
|
84
|
+
fromState: 'not_required',
|
|
85
|
+
toState: 'pending',
|
|
86
|
+
});
|
|
87
|
+
await this.dispatch(() => this.emailDispatch.onSubmittedForApproval({ cart, submitter: actor }));
|
|
88
|
+
return cart;
|
|
89
|
+
}
|
|
90
|
+
/** Org Admin approves a pending cart. */
|
|
91
|
+
async approve(cartId, actor) {
|
|
92
|
+
const em = this.emFactory();
|
|
93
|
+
const cart = await em.findOne(Cart, { id: cartId });
|
|
94
|
+
if (!cart || cart.organizationId !== actor.organizationId) {
|
|
95
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'cart_not_found');
|
|
96
|
+
}
|
|
97
|
+
if (cart.approvalStatus !== 'pending') {
|
|
98
|
+
throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'approval_not_pending');
|
|
99
|
+
}
|
|
100
|
+
cart.approvalStatus = 'approved';
|
|
101
|
+
cart.approvedAt = new Date();
|
|
102
|
+
cart.approvedByCustomerAccountId = actor.customerAccountId;
|
|
103
|
+
await em.flush();
|
|
104
|
+
await this.cartAuditService.record({
|
|
105
|
+
cartId: cart.id,
|
|
106
|
+
actorType: 'org_admin',
|
|
107
|
+
actorId: actor.customerAccountId,
|
|
108
|
+
action: 'approval_approved',
|
|
109
|
+
fromState: 'pending',
|
|
110
|
+
toState: 'approved',
|
|
111
|
+
});
|
|
112
|
+
await this.dispatch(() => this.emailDispatch.onApproved({ cart, approver: actor }));
|
|
113
|
+
return cart;
|
|
114
|
+
}
|
|
115
|
+
/** Org Admin rejects a pending cart with a reason. */
|
|
116
|
+
async reject(cartId, actor, reason) {
|
|
117
|
+
const em = this.emFactory();
|
|
118
|
+
const cart = await em.findOne(Cart, { id: cartId });
|
|
119
|
+
if (!cart || cart.organizationId !== actor.organizationId) {
|
|
120
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'cart_not_found');
|
|
121
|
+
}
|
|
122
|
+
if (cart.approvalStatus !== 'pending') {
|
|
123
|
+
throw new HttpError(422, ERROR_CODES.VALIDATION_FAILED, 'approval_not_pending');
|
|
124
|
+
}
|
|
125
|
+
cart.approvalStatus = 'rejected_by_org_admin';
|
|
126
|
+
cart.status = 'rejected';
|
|
127
|
+
cart.rejectedAt = new Date();
|
|
128
|
+
cart.rejectedByActor = `org_admin:${actor.customerAccountId}`;
|
|
129
|
+
cart.rejectedReason = reason;
|
|
130
|
+
await em.flush();
|
|
131
|
+
await this.cartAuditService.record({
|
|
132
|
+
cartId: cart.id,
|
|
133
|
+
actorType: 'org_admin',
|
|
134
|
+
actorId: actor.customerAccountId,
|
|
135
|
+
action: 'approval_rejected',
|
|
136
|
+
fromState: 'pending',
|
|
137
|
+
toState: 'rejected_by_org_admin',
|
|
138
|
+
reason,
|
|
139
|
+
});
|
|
140
|
+
await this.dispatch(() => this.emailDispatch.onRejected({ cart, rejector: actor, reason }));
|
|
141
|
+
return cart;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The current policy, for a platform admin (feature 091, P7b;
|
|
145
|
+
* `contracts/admin-component-contribution.md` §10.5).
|
|
146
|
+
*
|
|
147
|
+
* The read half of {@link setPolicyByAdmin}, added because the admin
|
|
148
|
+
* contribution that renders the toggle has to know what to render it in.
|
|
149
|
+
* Until P7b that value crossed as a prop — `initialRequiresCartApproval`, out
|
|
150
|
+
* of `organizations`' detail payload — and a zone's props may not carry it:
|
|
151
|
+
* the other three contributors to `organization.detail.after` want neither
|
|
152
|
+
* the prop nor the request behind it.
|
|
153
|
+
*
|
|
154
|
+
* It reads through `organizations`' own port rather than this module's
|
|
155
|
+
* tables, which is the direction the write has taken since issue #175: the
|
|
156
|
+
* column belongs to that module.
|
|
157
|
+
*/
|
|
158
|
+
async getPolicyByAdmin(organizationId) {
|
|
159
|
+
const organization = await this.organizations.findById(organizationId);
|
|
160
|
+
if (!organization) {
|
|
161
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'organization_not_found');
|
|
162
|
+
}
|
|
163
|
+
return organization;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Platform-admin variant of {@link setPolicyForOrganization}. Same
|
|
167
|
+
* semantics (policy-off cascades a reset of every pending/approved
|
|
168
|
+
* cart in the Org) but the actor is a platform admin rather than an
|
|
169
|
+
* Org Admin — recorded as `actorType: 'platform_admin'` in the
|
|
170
|
+
* audit row.
|
|
171
|
+
*/
|
|
172
|
+
async setPolicyByAdmin(organizationId, adminUserId, requires) {
|
|
173
|
+
const { organization, changed } = await this.organizationCartApproval.setCartApprovalPolicy(organizationId, requires);
|
|
174
|
+
if (changed && !requires) {
|
|
175
|
+
await this.resetPendingApprovals(organizationId, 'platform_admin', adminUserId);
|
|
176
|
+
}
|
|
177
|
+
return organization;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Org Admin toggles the per-Organization `requires_cart_approval` policy.
|
|
181
|
+
* On `false`: every `pending` or `approved` cart in the Org returns to
|
|
182
|
+
* `not_required` and is audited as `approval_policy_reset`.
|
|
183
|
+
*/
|
|
184
|
+
async setPolicyForOrganization(organizationId, actor, requires) {
|
|
185
|
+
if (organizationId !== actor.organizationId) {
|
|
186
|
+
throw new HttpError(404, ERROR_CODES.NOT_FOUND, 'organization_not_found');
|
|
187
|
+
}
|
|
188
|
+
const { organization, changed } = await this.organizationCartApproval.setCartApprovalPolicy(organizationId, requires);
|
|
189
|
+
if (changed && !requires) {
|
|
190
|
+
await this.resetPendingApprovals(organizationId, 'org_admin', actor.customerAccountId);
|
|
191
|
+
}
|
|
192
|
+
return organization;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The half of a policy switch-off that belongs to this module: every
|
|
196
|
+
* `pending` or `approved` cart in the Organization returns to
|
|
197
|
+
* `not_required`, audited as `approval_policy_reset` in this module's own
|
|
198
|
+
* per-cart trail with the actor that switched the policy off.
|
|
199
|
+
*/
|
|
200
|
+
async resetPendingApprovals(organizationId, actorType, actorId) {
|
|
201
|
+
const em = this.emFactory();
|
|
202
|
+
const affected = await em.find(Cart, {
|
|
203
|
+
organizationId,
|
|
204
|
+
approvalStatus: { $in: ['pending', 'approved'] },
|
|
205
|
+
});
|
|
206
|
+
for (const cart of affected) {
|
|
207
|
+
const prev = cart.approvalStatus;
|
|
208
|
+
cart.approvalStatus = 'not_required';
|
|
209
|
+
await em.flush();
|
|
210
|
+
await this.cartAuditService.record({
|
|
211
|
+
cartId: cart.id,
|
|
212
|
+
actorType,
|
|
213
|
+
actorId,
|
|
214
|
+
action: 'approval_policy_reset',
|
|
215
|
+
fromState: prev,
|
|
216
|
+
toState: 'not_required',
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Re-arm hook: any buyer-driven mutation on an `approved` cart drops
|
|
222
|
+
* it back to `pending`. Called by CartService.{addItem,updateItem,
|
|
223
|
+
* removeItem} and by CartCouponService.{apply,clear}.
|
|
224
|
+
*/
|
|
225
|
+
async maybeReArm(cart, actor) {
|
|
226
|
+
if (cart.approvalStatus !== 'approved')
|
|
227
|
+
return;
|
|
228
|
+
const em = this.emFactory();
|
|
229
|
+
cart.approvalStatus = 'pending';
|
|
230
|
+
cart.submittedForApprovalAt = new Date();
|
|
231
|
+
await em.flush();
|
|
232
|
+
await this.cartAuditService.record({
|
|
233
|
+
cartId: cart.id,
|
|
234
|
+
actorType: 'customer',
|
|
235
|
+
...(actor.customerAccountId ? { actorId: actor.customerAccountId } : {}),
|
|
236
|
+
action: 'approval_resubmission_required',
|
|
237
|
+
fromState: 'approved',
|
|
238
|
+
toState: 'pending',
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=cart-approval-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-approval-service.js","sourceRoot":"","sources":["../../../src/backend/services/cart-approval-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { CartAuditEntry } from '../entities/cart-audit-entry.entity.js';
|
|
3
|
+
import type { CartAuditAction, CartAuditActorType } from '../entities/cart-audit-entry.entity.js';
|
|
4
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
5
|
+
/**
|
|
6
|
+
* Single writer for cart audit landings (feature 027 §R10). Every call
|
|
7
|
+
* lands TWO rows in the same transaction:
|
|
8
|
+
*
|
|
9
|
+
* 1. `cart_audit_entries` — the typed cart-detail feed (powers the
|
|
10
|
+
* cart-detail UI for both Org-Admin and platform-admin views).
|
|
11
|
+
* 2. `audit_log_entries` — the platform-wide audit timeline
|
|
12
|
+
* (queryable across modules from the dashboard recent-activity
|
|
13
|
+
* surface of feature 024).
|
|
14
|
+
*
|
|
15
|
+
* The two writes share a single MikroORM unit-of-work so a partial
|
|
16
|
+
* commit is structurally impossible — either both rows land or
|
|
17
|
+
* neither does. No call site outside the carts module may write to
|
|
18
|
+
* `cart_audit_entries` directly.
|
|
19
|
+
*
|
|
20
|
+
* That claim used to hold only where `emFactory` returned the same manager
|
|
21
|
+
* twice (issue #152). `record()` took one fork for the cart row and
|
|
22
|
+
* `AuditPort.record()` took another for the platform row and flushed it
|
|
23
|
+
* itself, so in production — where the factory forks per call — the two rows
|
|
24
|
+
* were two transactions, and a failure between them landed the platform row
|
|
25
|
+
* without its cart-detail counterpart. Both rows are built on ONE manager now
|
|
26
|
+
* and land in one flush, which is what the paragraph above always said.
|
|
27
|
+
*
|
|
28
|
+
* `actorAdminUserId` on the `audit_log_entries` row is set ONLY when
|
|
29
|
+
* `actor_type === 'platform_admin'`. For customer / org-admin / system /
|
|
30
|
+
* sweep actors the column is null (the cart audit feed still records
|
|
31
|
+
* the actor in `cart_audit_entries.actor_id` / `.actor_type`).
|
|
32
|
+
*/
|
|
33
|
+
export interface CartAuditRecordInput {
|
|
34
|
+
cartId: string;
|
|
35
|
+
actorType: CartAuditActorType;
|
|
36
|
+
actorId?: string | null;
|
|
37
|
+
action: CartAuditAction;
|
|
38
|
+
fromState?: string | null;
|
|
39
|
+
toState?: string | null;
|
|
40
|
+
reason?: string | null;
|
|
41
|
+
metadata?: Record<string, unknown>;
|
|
42
|
+
/** Correlation id from the inbound request (X-Request-Id). */
|
|
43
|
+
requestId?: string | null;
|
|
44
|
+
}
|
|
45
|
+
export declare class CartAuditService {
|
|
46
|
+
private readonly emFactory;
|
|
47
|
+
private readonly auditLog;
|
|
48
|
+
constructor(emFactory: () => EntityManager, auditLog: AuditPort);
|
|
49
|
+
/** One landing, on this service's own manager, flushed. The default form. */
|
|
50
|
+
record(input: CartAuditRecordInput): Promise<CartAuditEntry>;
|
|
51
|
+
/**
|
|
52
|
+
* Batch form, landing on the CALLER's EntityManager and flushing nothing
|
|
53
|
+
* (issue #152).
|
|
54
|
+
*
|
|
55
|
+
* The caller's next `flush()` is the unit of work, so a batch of cart writes
|
|
56
|
+
* and the audit rows for that batch commit together or not at all — the
|
|
57
|
+
* guarantee `record()` gives one write, given to a run of them. The
|
|
58
|
+
* abandonment sweep is the caller it exists for: it used to flush once per
|
|
59
|
+
* cart, which is a transaction per cart plus an identity map that never
|
|
60
|
+
* shrank, and at 2000 carts that cost 66 ms each.
|
|
61
|
+
*
|
|
62
|
+
* It flushes nothing on purpose. A method that both joined the caller's unit
|
|
63
|
+
* of work and committed it would decide, on the caller's behalf, that the
|
|
64
|
+
* audit rows are the end of the batch — and for the sweep they are not: the
|
|
65
|
+
* status flip is dirty on the same manager and belongs in the same commit.
|
|
66
|
+
*
|
|
67
|
+
* The name is {@link AuditPort.recordWithin}'s, deliberately: `Within`
|
|
68
|
+
* is the repository's word for "on the caller's manager, no flush", and it is
|
|
69
|
+
* the word `check-command-coverage` reads as an audit landing. A name of its
|
|
70
|
+
* own would have made this method invisible to that check while it was doing
|
|
71
|
+
* exactly what the check exists to find.
|
|
72
|
+
*/
|
|
73
|
+
recordWithin(em: EntityManager, inputs: readonly CartAuditRecordInput[]): CartAuditEntry[];
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=cart-audit-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-audit-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cart-audit-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAC;AACxE,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EACnB,MAAM,wCAAwC,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,kBAAkB,CAAC;IAC9B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,eAAe,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBADR,SAAS,EAAE,MAAM,aAAa,EAC9B,QAAQ,EAAE,SAAS;IAGtC,6EAA6E;IACvE,MAAM,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,cAAc,CAAC;IAQlE;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,YAAY,CACV,EAAE,EAAE,aAAa,EACjB,MAAM,EAAE,SAAS,oBAAoB,EAAE,GACtC,cAAc,EAAE;CAiCpB"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { CartAuditEntry } from '../entities/cart-audit-entry.entity.js';
|
|
2
|
+
export class CartAuditService {
|
|
3
|
+
emFactory;
|
|
4
|
+
auditLog;
|
|
5
|
+
constructor(emFactory, auditLog) {
|
|
6
|
+
this.emFactory = emFactory;
|
|
7
|
+
this.auditLog = auditLog;
|
|
8
|
+
}
|
|
9
|
+
/** One landing, on this service's own manager, flushed. The default form. */
|
|
10
|
+
async record(input) {
|
|
11
|
+
const em = this.emFactory();
|
|
12
|
+
const entries = this.recordWithin(em, [input]);
|
|
13
|
+
await em.flush();
|
|
14
|
+
// One input in, one entry out — the map below preserves the arity.
|
|
15
|
+
return entries[0];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Batch form, landing on the CALLER's EntityManager and flushing nothing
|
|
19
|
+
* (issue #152).
|
|
20
|
+
*
|
|
21
|
+
* The caller's next `flush()` is the unit of work, so a batch of cart writes
|
|
22
|
+
* and the audit rows for that batch commit together or not at all — the
|
|
23
|
+
* guarantee `record()` gives one write, given to a run of them. The
|
|
24
|
+
* abandonment sweep is the caller it exists for: it used to flush once per
|
|
25
|
+
* cart, which is a transaction per cart plus an identity map that never
|
|
26
|
+
* shrank, and at 2000 carts that cost 66 ms each.
|
|
27
|
+
*
|
|
28
|
+
* It flushes nothing on purpose. A method that both joined the caller's unit
|
|
29
|
+
* of work and committed it would decide, on the caller's behalf, that the
|
|
30
|
+
* audit rows are the end of the batch — and for the sweep they are not: the
|
|
31
|
+
* status flip is dirty on the same manager and belongs in the same commit.
|
|
32
|
+
*
|
|
33
|
+
* The name is {@link AuditPort.recordWithin}'s, deliberately: `Within`
|
|
34
|
+
* is the repository's word for "on the caller's manager, no flush", and it is
|
|
35
|
+
* the word `check-command-coverage` reads as an audit landing. A name of its
|
|
36
|
+
* own would have made this method invisible to that check while it was doing
|
|
37
|
+
* exactly what the check exists to find.
|
|
38
|
+
*/
|
|
39
|
+
recordWithin(em, inputs) {
|
|
40
|
+
return inputs.map((input) => {
|
|
41
|
+
const entry = em.create(CartAuditEntry, {
|
|
42
|
+
cartId: input.cartId,
|
|
43
|
+
actorType: input.actorType,
|
|
44
|
+
...(input.actorId !== undefined ? { actorId: input.actorId } : {}),
|
|
45
|
+
action: input.action,
|
|
46
|
+
...(input.fromState !== undefined ? { fromState: input.fromState } : {}),
|
|
47
|
+
...(input.toState !== undefined ? { toState: input.toState } : {}),
|
|
48
|
+
...(input.reason !== undefined ? { reason: input.reason } : {}),
|
|
49
|
+
metadata: input.metadata ?? {},
|
|
50
|
+
});
|
|
51
|
+
em.persist(entry);
|
|
52
|
+
this.auditLog.recordWithin(em, {
|
|
53
|
+
action: `cart.${input.action}`,
|
|
54
|
+
objectType: 'cart',
|
|
55
|
+
objectId: input.cartId,
|
|
56
|
+
...(input.actorType === 'platform_admin' && input.actorId
|
|
57
|
+
? { actorAdminUserId: input.actorId }
|
|
58
|
+
: {}),
|
|
59
|
+
...(input.actorType === 'customer' || input.actorType === 'org_admin'
|
|
60
|
+
? { impersonatedCustomerAccountId: input.actorId ?? null }
|
|
61
|
+
: {}),
|
|
62
|
+
stateBefore: input.fromState ? { state: input.fromState } : null,
|
|
63
|
+
stateAfter: input.toState ? { state: input.toState } : null,
|
|
64
|
+
...(input.requestId !== undefined ? { requestId: input.requestId } : {}),
|
|
65
|
+
});
|
|
66
|
+
return entry;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=cart-audit-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-audit-service.js","sourceRoot":"","sources":["../../../src/backend/services/cart-audit-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { Cart } from '../entities/cart.entity.js';
|
|
3
|
+
import type { CatalogProductReadPort, QuoteRequestReadPort, RfqCustomerContext, RfqCustomerPort } from '@endora-commerce/contracts';
|
|
4
|
+
import type { SalesChannelMembershipPort } from '@endora-commerce/platform/kernel';
|
|
5
|
+
import type { CartService, CustomerContext as CartCustomerContext } from './cart-service.js';
|
|
6
|
+
/**
|
|
7
|
+
* Three conversions around the Cart (feature 027 US3):
|
|
8
|
+
*
|
|
9
|
+
* - convertToQuoteRequest(cart, ctx, note?)
|
|
10
|
+
* Reads the cart's lines, hands them to `RfqCustomerPort.createForCustomer`,
|
|
11
|
+
* flips the source cart's status to `completed` with
|
|
12
|
+
* `converted_to_quote_request_id` set. The next storefront visit
|
|
13
|
+
* lazy-creates a fresh `active` cart for the buyer.
|
|
14
|
+
*
|
|
15
|
+
* - createCartFromQuoteRequest(qrId, ctx)
|
|
16
|
+
* Reads a Quote Request's lines, creates (or appends to) the buyer's
|
|
17
|
+
* active cart with re-resolved prices (the QR's proposed prices are
|
|
18
|
+
* NOT carried over — per FR-017). Skips lines whose product is no
|
|
19
|
+
* longer purchasable; returns the dropped-line list to the caller.
|
|
20
|
+
*
|
|
21
|
+
* - appendShoppingListToCart(listId, ctx)
|
|
22
|
+
* Delegated to `ShoppingListService.convertToCart`, which already
|
|
23
|
+
* implements the append + skip-archived-products contract from
|
|
24
|
+
* feature 010 / FR-018. This service just exposes a uniform return
|
|
25
|
+
* shape across all three conversions.
|
|
26
|
+
*
|
|
27
|
+
* The service does not know about `cart_audit_entries` directly — every
|
|
28
|
+
* write path bumps `cart.lastActivityAt` so the abandonment sweep treats
|
|
29
|
+
* the cart as fresh. Audit-trail wiring is left to a follow-up.
|
|
30
|
+
*/
|
|
31
|
+
export interface CartDroppedLine {
|
|
32
|
+
productId: string;
|
|
33
|
+
productName: string;
|
|
34
|
+
reason: 'not_purchasable' | 'out_of_stock' | 'no_price_in_customer_list' | 'removed_by_conversion';
|
|
35
|
+
}
|
|
36
|
+
export interface ConvertToQrResult {
|
|
37
|
+
quoteRequestId: string;
|
|
38
|
+
cartId: string;
|
|
39
|
+
}
|
|
40
|
+
export interface CreateCartFromQrResult {
|
|
41
|
+
cartId: string;
|
|
42
|
+
appendedLineCount: number;
|
|
43
|
+
droppedLines: CartDroppedLine[];
|
|
44
|
+
}
|
|
45
|
+
export interface AppendShoppingListResult {
|
|
46
|
+
cartId: string;
|
|
47
|
+
appendedLineCount: number;
|
|
48
|
+
droppedLines: CartDroppedLine[];
|
|
49
|
+
}
|
|
50
|
+
export declare class CartConversionService {
|
|
51
|
+
private readonly emFactory;
|
|
52
|
+
private readonly cartService;
|
|
53
|
+
/** `quote_requests`' customer-facing write surface. */
|
|
54
|
+
private readonly rfqService;
|
|
55
|
+
/** `quote_requests`' row read model — the quote and its lines. */
|
|
56
|
+
private readonly quoteRequests;
|
|
57
|
+
/** `catalog`'s product read model — is the quoted product still there? */
|
|
58
|
+
private readonly catalogProducts;
|
|
59
|
+
/**
|
|
60
|
+
* The sanctioned bridge accessor (Constitution XII) — and on this seam it
|
|
61
|
+
* is what keeps the *reason* honest (issue #259). `CartService.addItem`
|
|
62
|
+
* refuses an out-of-channel line by itself, but this method catches every
|
|
63
|
+
* `HttpError` from it and reports one dropped-line reason,
|
|
64
|
+
* `no_price_in_customer_list`. A line the channel does not sell has nothing
|
|
65
|
+
* to do with the buyer's price list, so it is narrowed here instead and
|
|
66
|
+
* falls into the `not_purchasable` branch — where a line whose product has
|
|
67
|
+
* gone already lands.
|
|
68
|
+
*/
|
|
69
|
+
private readonly channelMembership;
|
|
70
|
+
constructor(emFactory: () => EntityManager, cartService: CartService,
|
|
71
|
+
/** `quote_requests`' customer-facing write surface. */
|
|
72
|
+
rfqService: RfqCustomerPort,
|
|
73
|
+
/** `quote_requests`' row read model — the quote and its lines. */
|
|
74
|
+
quoteRequests: QuoteRequestReadPort,
|
|
75
|
+
/** `catalog`'s product read model — is the quoted product still there? */
|
|
76
|
+
catalogProducts: CatalogProductReadPort,
|
|
77
|
+
/**
|
|
78
|
+
* The sanctioned bridge accessor (Constitution XII) — and on this seam it
|
|
79
|
+
* is what keeps the *reason* honest (issue #259). `CartService.addItem`
|
|
80
|
+
* refuses an out-of-channel line by itself, but this method catches every
|
|
81
|
+
* `HttpError` from it and reports one dropped-line reason,
|
|
82
|
+
* `no_price_in_customer_list`. A line the channel does not sell has nothing
|
|
83
|
+
* to do with the buyer's price list, so it is narrowed here instead and
|
|
84
|
+
* falls into the `not_purchasable` branch — where a line whose product has
|
|
85
|
+
* gone already lands.
|
|
86
|
+
*/
|
|
87
|
+
channelMembership: SalesChannelMembershipPort);
|
|
88
|
+
/**
|
|
89
|
+
* Cart → Quote Request. Refuses on an empty cart. Source cart flips to
|
|
90
|
+
* `completed` with `converted_to_quote_request_id` set.
|
|
91
|
+
*/
|
|
92
|
+
convertToQuoteRequest(cart: Cart, ctx: RfqCustomerContext, note?: string): Promise<ConvertToQrResult>;
|
|
93
|
+
/**
|
|
94
|
+
* Quote Request → Cart. Creates a fresh `active` cart (or returns the
|
|
95
|
+
* existing one) for the buyer, appends the QR's lines with re-resolved
|
|
96
|
+
* prices (via `CartService.addItem`, which already routes through the
|
|
97
|
+
* pricing resolver when wired), and surfaces a list of dropped lines.
|
|
98
|
+
*
|
|
99
|
+
* The QR's `desiredUnitPrice` is intentionally dropped — the cart's
|
|
100
|
+
* unit prices come from the customer's current price list.
|
|
101
|
+
*/
|
|
102
|
+
createCartFromQuoteRequest(qrId: string, ctx: CartCustomerContext): Promise<CreateCartFromQrResult>;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=cart-conversion-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-conversion-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cart-conversion-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAElD,OAAO,KAAK,EACV,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EAChB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AAEnF,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,IAAI,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE7F;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,iBAAiB,GAAG,cAAc,GAAG,2BAA2B,GAAG,uBAAuB,CAAC;CACpG;AAED,MAAM,WAAW,iBAAiB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,eAAe,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,eAAe,EAAE,CAAC;CACjC;AAED,qBAAa,qBAAqB;IAE9B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,uDAAuD;IACvD,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,kEAAkE;IAClE,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ,CAAC,iBAAiB;gBAlBjB,SAAS,EAAE,MAAM,aAAa,EAC9B,WAAW,EAAE,WAAW;IACzC,uDAAuD;IACtC,UAAU,EAAE,eAAe;IAC5C,kEAAkE;IACjD,aAAa,EAAE,oBAAoB;IACpD,0EAA0E;IACzD,eAAe,EAAE,sBAAsB;IACxD;;;;;;;;;OASG;IACc,iBAAiB,EAAE,0BAA0B;IAGhE;;;OAGG;IACG,qBAAqB,CACzB,IAAI,EAAE,IAAI,EACV,GAAG,EAAE,kBAAkB,EACvB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,iBAAiB,CAAC;IAmD7B;;;;;;;;OAQG;IACG,0BAA0B,CAC9B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,mBAAmB,GACvB,OAAO,CAAC,sBAAsB,CAAC;CAqFnC"}
|