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