@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,29 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
|
2
|
+
import { type CustomerAccountReadPort } from '@endora-commerce/contracts';
|
|
3
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
4
|
+
import type { CartService } from './services/cart-service.js';
|
|
5
|
+
import type { CartApprovalService } from './services/cart-approval-service.js';
|
|
6
|
+
import type { CartOrganizationVisibilityService } from './services/cart-organization-visibility-service.js';
|
|
7
|
+
export interface CartsOrganizationRoutesDeps {
|
|
8
|
+
cartService: CartService;
|
|
9
|
+
/**
|
|
10
|
+
* `customer_accounts`' read model (feature 075, Phase C). Two reads: the
|
|
11
|
+
* caller's own role — which is the org-admin gate on every route here — and
|
|
12
|
+
* the owner's e-mail on the cart detail. Both were `em.findOne` against
|
|
13
|
+
* `customer_accounts`' table, so the gate went on answering out of rows a
|
|
14
|
+
* deactivation leaves in place.
|
|
15
|
+
*/
|
|
16
|
+
customerAccounts: CustomerAccountReadPort;
|
|
17
|
+
cartApprovalService: CartApprovalService;
|
|
18
|
+
visibilityService: CartOrganizationVisibilityService;
|
|
19
|
+
emFactory: () => EntityManager;
|
|
20
|
+
resolveCartActor: (request: FastifyRequest) => {
|
|
21
|
+
customer?: {
|
|
22
|
+
customerAccountId: string;
|
|
23
|
+
organizationId: string | null;
|
|
24
|
+
};
|
|
25
|
+
anonymousToken?: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare function registerCartsOrganizationRoutes(app: FastifyInstance, deps: CartsOrganizationRoutesDeps): Promise<void>;
|
|
29
|
+
//# sourceMappingURL=routes.organization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.organization.d.ts","sourceRoot":"","sources":["../../src/backend/routes.organization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAQL,KAAK,uBAAuB,EAC7B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC/E,OAAO,KAAK,EAAE,iCAAiC,EAAE,MAAM,oDAAoD,CAAC;AAsC5G,MAAM,WAAW,2BAA2B;IAC1C,WAAW,EAAE,WAAW,CAAC;IACzB;;;;;;OAMG;IACH,gBAAgB,EAAE,uBAAuB,CAAC;IAC1C,mBAAmB,EAAE,mBAAmB,CAAC;IACzC,iBAAiB,EAAE,iCAAiC,CAAC;IACrD,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,gBAAgB,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK;QAC7C,QAAQ,CAAC,EAAE;YAAE,iBAAiB,EAAE,MAAM,CAAC;YAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;SAAE,CAAC;QACxE,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED,wBAAsB,+BAA+B,CACnD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,2BAA2B,GAChC,OAAO,CAAC,IAAI,CAAC,CAoJf"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { rejectOrgCartSchema, setCartApprovalPolicySchema, cartStatusSchema, cartApprovalStatusSchema, ERROR_CODES, } from '@endora-commerce/contracts';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
4
|
+
/**
|
|
5
|
+
* Storefront routes scoped to an Organization Admin (feature 027 US4).
|
|
6
|
+
*
|
|
7
|
+
* - GET /api/v1/organization/carts org-admin
|
|
8
|
+
* - GET /api/v1/organization/carts/:id org-admin
|
|
9
|
+
* - PATCH /api/v1/organization/policies/cart-approval org-admin
|
|
10
|
+
* - POST /api/v1/cart/submit-for-approval ordinary member
|
|
11
|
+
* - POST /api/v1/organization/carts/:id/approve org-admin
|
|
12
|
+
* - POST /api/v1/organization/carts/:id/reject org-admin
|
|
13
|
+
*
|
|
14
|
+
* Role check is uniform: the caller's CustomerAccount.role must be
|
|
15
|
+
* `organization_admin` for every endpoint marked "org-admin" above.
|
|
16
|
+
* Anti-enumeration: a foreign organization's carts return 404, not 403.
|
|
17
|
+
*/
|
|
18
|
+
const orgCartsListQuerySchema = z.object({
|
|
19
|
+
status: z.union([
|
|
20
|
+
z.array(cartStatusSchema),
|
|
21
|
+
z.string().transform((s) => s.split(',').filter(Boolean)),
|
|
22
|
+
]).optional(),
|
|
23
|
+
approvalStatus: z.union([
|
|
24
|
+
z.array(cartApprovalStatusSchema),
|
|
25
|
+
z.string().transform((s) => s.split(',').filter(Boolean)),
|
|
26
|
+
]).optional(),
|
|
27
|
+
page: z.coerce.number().int().positive().optional(),
|
|
28
|
+
pageSize: z.coerce.number().int().positive().max(100).optional(),
|
|
29
|
+
sort: z.enum([
|
|
30
|
+
'last_activity_desc',
|
|
31
|
+
'last_activity_asc',
|
|
32
|
+
'total_desc',
|
|
33
|
+
'total_asc',
|
|
34
|
+
'created_desc',
|
|
35
|
+
]).optional(),
|
|
36
|
+
});
|
|
37
|
+
export async function registerCartsOrganizationRoutes(app, deps) {
|
|
38
|
+
const { cartService, cartApprovalService, visibilityService, customerAccounts, resolveCartActor, } = deps;
|
|
39
|
+
/** Resolves the caller as an Organization Administrator (404 if not). */
|
|
40
|
+
const requireOrgAdmin = async (request) => {
|
|
41
|
+
const actor = resolveCartActor(request);
|
|
42
|
+
if (!actor.customer) {
|
|
43
|
+
throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'Sign-in required.');
|
|
44
|
+
}
|
|
45
|
+
if (!actor.customer.organizationId) {
|
|
46
|
+
throw new HttpError(403, ERROR_CODES.FORBIDDEN, 'No organization in session.');
|
|
47
|
+
}
|
|
48
|
+
const me = await customerAccounts.findById(actor.customer.customerAccountId);
|
|
49
|
+
if (!me || me.role !== 'organization_admin') {
|
|
50
|
+
throw new HttpError(403, ERROR_CODES.FORBIDDEN, 'Organization Admin role required.');
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
customerAccountId: actor.customer.customerAccountId,
|
|
54
|
+
organizationId: actor.customer.organizationId,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
app.get('/api/v1/organization/carts', async (request) => {
|
|
58
|
+
const orgAdmin = await requireOrgAdmin(request);
|
|
59
|
+
const parsed = orgCartsListQuerySchema.parse(request.query ?? {});
|
|
60
|
+
const query = {};
|
|
61
|
+
if (parsed.status !== undefined)
|
|
62
|
+
query.status = parsed.status;
|
|
63
|
+
if (parsed.approvalStatus !== undefined)
|
|
64
|
+
query.approvalStatus = parsed.approvalStatus;
|
|
65
|
+
if (parsed.page !== undefined)
|
|
66
|
+
query.page = parsed.page;
|
|
67
|
+
if (parsed.pageSize !== undefined)
|
|
68
|
+
query.pageSize = parsed.pageSize;
|
|
69
|
+
if (parsed.sort !== undefined)
|
|
70
|
+
query.sort = parsed.sort;
|
|
71
|
+
return visibilityService.list(orgAdmin.organizationId, query);
|
|
72
|
+
});
|
|
73
|
+
app.get('/api/v1/organization/carts/:id', async (request) => {
|
|
74
|
+
const orgAdmin = await requireOrgAdmin(request);
|
|
75
|
+
const cart = await visibilityService.getInOrganization(request.params.id, orgAdmin.organizationId);
|
|
76
|
+
const items = await cartService.getItems(cart.id);
|
|
77
|
+
const total = items.reduce((acc, it) => acc + Number(it.unitPrice) * it.quantity, 0);
|
|
78
|
+
const currency = items[0]?.currency ?? 'PLN';
|
|
79
|
+
const owner = cart.customerAccountId
|
|
80
|
+
? await customerAccounts.findById(cart.customerAccountId)
|
|
81
|
+
: null;
|
|
82
|
+
return {
|
|
83
|
+
data: {
|
|
84
|
+
id: cart.id,
|
|
85
|
+
ownerCustomerAccountId: cart.customerAccountId,
|
|
86
|
+
ownerDisplayName: owner?.email ?? cart.customerAccountId,
|
|
87
|
+
status: cart.status,
|
|
88
|
+
approvalStatus: cart.approvalStatus,
|
|
89
|
+
itemCount: items.length,
|
|
90
|
+
total: { amount: total, currency },
|
|
91
|
+
lastActivityAt: cart.lastActivityAt.toISOString(),
|
|
92
|
+
submittedForApprovalAt: cart.submittedForApprovalAt?.toISOString() ?? null,
|
|
93
|
+
approvedAt: cart.approvedAt?.toISOString() ?? null,
|
|
94
|
+
rejectedAt: cart.rejectedAt?.toISOString() ?? null,
|
|
95
|
+
rejectedReason: cart.rejectedReason ?? null,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
app.patch('/api/v1/organization/policies/cart-approval', { schema: { body: setCartApprovalPolicySchema } }, async (request) => {
|
|
100
|
+
const orgAdmin = await requireOrgAdmin(request);
|
|
101
|
+
const body = setCartApprovalPolicySchema.parse(request.body);
|
|
102
|
+
const org = await cartApprovalService.setPolicyForOrganization(orgAdmin.organizationId, orgAdmin, body.requiresCartApproval);
|
|
103
|
+
return {
|
|
104
|
+
data: {
|
|
105
|
+
organizationId: org.id,
|
|
106
|
+
requiresCartApproval: org.requiresCartApproval,
|
|
107
|
+
updatedAt: org.updatedAt.toISOString(),
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
app.post('/api/v1/cart/submit-for-approval', async (request) => {
|
|
112
|
+
const actor = resolveCartActor(request);
|
|
113
|
+
if (!actor.customer) {
|
|
114
|
+
throw new HttpError(401, ERROR_CODES.UNAUTHORIZED, 'Sign-in required.');
|
|
115
|
+
}
|
|
116
|
+
// D-178 — an invariant, not a business state: every customer account has an
|
|
117
|
+
// Organization (`customer_accounts.organization_id` is `NOT NULL`), so
|
|
118
|
+
// reaching this is a broken platform and not a buyer who needs to attach
|
|
119
|
+
// one. Kept rather than deleted — the actor's field is optional at this
|
|
120
|
+
// seam and the call below takes a `string`.
|
|
121
|
+
if (!actor.customer.organizationId) {
|
|
122
|
+
throw new HttpError(500, ERROR_CODES.INTERNAL, 'Invariant violated: a customer account has no Organization (Principle XI).');
|
|
123
|
+
}
|
|
124
|
+
const cart = await cartService.getOrCreateForCustomer(actor.customer);
|
|
125
|
+
await cartApprovalService.submitForApproval(cart, {
|
|
126
|
+
customerAccountId: actor.customer.customerAccountId,
|
|
127
|
+
organizationId: actor.customer.organizationId,
|
|
128
|
+
});
|
|
129
|
+
return { data: { ok: true, approvalStatus: cart.approvalStatus } };
|
|
130
|
+
});
|
|
131
|
+
app.post('/api/v1/organization/carts/:id/approve', async (request) => {
|
|
132
|
+
const orgAdmin = await requireOrgAdmin(request);
|
|
133
|
+
const cart = await cartApprovalService.approve(request.params.id, orgAdmin);
|
|
134
|
+
return { data: { ok: true, cartId: cart.id, approvalStatus: cart.approvalStatus } };
|
|
135
|
+
});
|
|
136
|
+
app.post('/api/v1/organization/carts/:id/reject', { schema: { body: rejectOrgCartSchema } }, async (request) => {
|
|
137
|
+
const orgAdmin = await requireOrgAdmin(request);
|
|
138
|
+
const body = rejectOrgCartSchema.parse(request.body);
|
|
139
|
+
const cart = await cartApprovalService.reject(request.params.id, orgAdmin, body.reason);
|
|
140
|
+
return {
|
|
141
|
+
data: {
|
|
142
|
+
ok: true,
|
|
143
|
+
cartId: cart.id,
|
|
144
|
+
status: cart.status,
|
|
145
|
+
approvalStatus: cart.approvalStatus,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=routes.organization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.organization.js","sourceRoot":"","sources":["../../src/backend/routes.organization.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SettingsReadPort } from '@endora-commerce/platform/kernel';
|
|
2
|
+
/**
|
|
3
|
+
* The two `carts.abandonment.*` reads, in one place (issue #54).
|
|
4
|
+
*
|
|
5
|
+
* They stood twice: once in `backend.ts`, for the composed sweep, and once in
|
|
6
|
+
* the hand-built ops CLI, for the operator running it by hand. Two copies of
|
|
7
|
+
* "which channel does this read name, and what does it fall back to" is
|
|
8
|
+
* precisely the drift the composition checklist's item 6 is about, and this
|
|
9
|
+
* pair has drifted before — both copies once passed the literal `'default'` as
|
|
10
|
+
* a channel id against a `uuid` column and both fell back to `0`, which the
|
|
11
|
+
* sweep reads as "sweep nothing" (feature 072, D-41/D-43).
|
|
12
|
+
*
|
|
13
|
+
* There is one reader now and no second copy to drift: since feature 080's
|
|
14
|
+
* T042b the operator command resolves the composition's own
|
|
15
|
+
* `cartAbandonmentWorker`, which this file already supplies.
|
|
16
|
+
*
|
|
17
|
+
* The reads are **platform-wide** (`null` channel) on purpose: an abandonment
|
|
18
|
+
* threshold and an ops mailbox are properties of the platform, not of a
|
|
19
|
+
* storefront. What may be absorbed is enumerated, because a bare `catch` around
|
|
20
|
+
* a port call turns fail-closed into fail-open — a shape mismatch, a driver
|
|
21
|
+
* error or a `ModuleDisabledError` propagates.
|
|
22
|
+
*/
|
|
23
|
+
/** Resolvers in the shape {@link CartAbandonmentWorkerDeps} asks for. */
|
|
24
|
+
export interface AbandonmentSettingsReaders {
|
|
25
|
+
resolveInactivityMinutes: () => Promise<number>;
|
|
26
|
+
resolveNotificationRecipient: () => Promise<string>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `settings` is a thunk, not a value: in the composed module it resolves the
|
|
30
|
+
* settings port on every call, and a port captured once keeps answering after
|
|
31
|
+
* its owner is switched off.
|
|
32
|
+
*/
|
|
33
|
+
export declare function abandonmentSettingsReaders(settings: () => SettingsReadPort): AbandonmentSettingsReaders;
|
|
34
|
+
//# sourceMappingURL=cart-abandonment-settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-abandonment-settings.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cart-abandonment-settings.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAOzE;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,yEAAyE;AACzE,MAAM,WAAW,0BAA0B;IACzC,wBAAwB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,4BAA4B,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CACrD;AAkDD;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,MAAM,gBAAgB,GAC/B,0BAA0B,CAiB5B"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { SettingNotRegistered, SettingOutOfScopeForChannel } from '@endora-commerce/platform/kernel';
|
|
3
|
+
import { CARTS_SETTING_CODES, DEFAULT_ABANDONMENT_INACTIVITY_MINUTES, DEFAULT_ABANDONMENT_NOTIFICATION_RECIPIENT, } from '../../manifest.js';
|
|
4
|
+
/**
|
|
5
|
+
* Conditions the process has already reported. Never reset, so the guard is per
|
|
6
|
+
* **process**: an out-of-scope setting is a deployment fact that holds for every
|
|
7
|
+
* subsequent read, and the sweep runs on a timer. One line per condition is what
|
|
8
|
+
* makes it findable; one line per read is what makes it invisible. A CLI run is
|
|
9
|
+
* one pass and then exits, so "once per process" is "once" there — and the warn
|
|
10
|
+
* is still worth emitting, because an operator running the sweep by hand is
|
|
11
|
+
* precisely the person who needs to be told the threshold did not come from the
|
|
12
|
+
* setting they configured.
|
|
13
|
+
*/
|
|
14
|
+
const warnedConditions = new Set();
|
|
15
|
+
function warnOnce(condition, message) {
|
|
16
|
+
if (warnedConditions.has(condition))
|
|
17
|
+
return;
|
|
18
|
+
warnedConditions.add(condition);
|
|
19
|
+
console.warn(message);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* One platform-wide read of one of this module's own settings, degrading to the
|
|
23
|
+
* manifest default under exactly the two conditions D-43 allows: an
|
|
24
|
+
* unregistered code (quiet — the normal state before the manifest reconciler's
|
|
25
|
+
* first run) and a setting the operator scoped to specific channels (warned
|
|
26
|
+
* once, because that scoping cannot be intentional for a platform-wide value).
|
|
27
|
+
*/
|
|
28
|
+
async function readPlatformSetting(settings, code, schema, fallback) {
|
|
29
|
+
try {
|
|
30
|
+
return await settings.get(code, null, schema);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof SettingNotRegistered)
|
|
34
|
+
return fallback;
|
|
35
|
+
if (error instanceof SettingOutOfScopeForChannel) {
|
|
36
|
+
warnOnce(`out-of-scope:${code}`, `[carts] setting "${code}" is scoped to specific sales channels, so it has ` +
|
|
37
|
+
`no platform-wide value — falling back to the manifest default ` +
|
|
38
|
+
`(logged once per process).`);
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `settings` is a thunk, not a value: in the composed module it resolves the
|
|
46
|
+
* settings port on every call, and a port captured once keeps answering after
|
|
47
|
+
* its owner is switched off.
|
|
48
|
+
*/
|
|
49
|
+
export function abandonmentSettingsReaders(settings) {
|
|
50
|
+
return {
|
|
51
|
+
resolveInactivityMinutes: async () => readPlatformSetting(settings(), CARTS_SETTING_CODES.ABANDONMENT_INACTIVITY_MINUTES, z.number().int().nonnegative(), DEFAULT_ABANDONMENT_INACTIVITY_MINUTES),
|
|
52
|
+
resolveNotificationRecipient: async () => readPlatformSetting(settings(), CARTS_SETTING_CODES.ABANDONMENT_NOTIFICATION_RECIPIENT, z.string(), DEFAULT_ABANDONMENT_NOTIFICATION_RECIPIENT),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=cart-abandonment-settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-abandonment-settings.js","sourceRoot":"","sources":["../../../src/backend/services/cart-abandonment-settings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,MAAM,kCAAkC,CAAC;AAErG,OAAO,EACL,mBAAmB,EACnB,sCAAsC,EACtC,0CAA0C,GAC3C,MAAM,mBAAmB,CAAC;AA8B3B;;;;;;;;;GASG;AACH,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAC;AAE3C,SAAS,QAAQ,CAAC,SAAiB,EAAE,OAAe;IAClD,IAAI,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC;QAAE,OAAO;IAC5C,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACxB,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,mBAAmB,CAChC,QAA0B,EAC1B,IAAY,EACZ,MAAoB,EACpB,QAAW;IAEX,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAChD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,oBAAoB;YAAE,OAAO,QAAQ,CAAC;QAC3D,IAAI,KAAK,YAAY,2BAA2B,EAAE,CAAC;YACjD,QAAQ,CACN,gBAAgB,IAAI,EAAE,EACtB,oBAAoB,IAAI,oDAAoD;gBAC1E,gEAAgE;gBAChE,4BAA4B,CAC/B,CAAC;YACF,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,0BAA0B,CACxC,QAAgC;IAEhC,OAAO;QACL,wBAAwB,EAAE,KAAK,IAAI,EAAE,CACnC,mBAAmB,CACjB,QAAQ,EAAE,EACV,mBAAmB,CAAC,8BAA8B,EAClD,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,EAC9B,sCAAsC,CACvC;QACH,4BAA4B,EAAE,KAAK,IAAI,EAAE,CACvC,mBAAmB,CACjB,QAAQ,EAAE,EACV,mBAAmB,CAAC,kCAAkC,EACtD,CAAC,CAAC,MAAM,EAAE,EACV,0CAA0C,CAC3C;KACJ,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { CartAuditService } from './cart-audit-service.js';
|
|
3
|
+
/**
|
|
4
|
+
* CartAbandonmentWorker — feature 027 US5.
|
|
5
|
+
*
|
|
6
|
+
* Periodic sweep that transitions `active` carts inactive past the
|
|
7
|
+
* configured threshold to `abandoned`. Mirrors the RfqExpiryWorker
|
|
8
|
+
* pattern from feature 008: public `sweep()` is a plain async function
|
|
9
|
+
* so tests can drive it directly without Redis/BullMQ.
|
|
10
|
+
*
|
|
11
|
+
* Idempotency: the eligibility filter excludes carts that already have
|
|
12
|
+
* `abandonment_notified_at` set. The sweep cycle thus fires the
|
|
13
|
+
* notification at most once per abandonment episode; reactivation
|
|
14
|
+
* (CartService.touch / any owner-driven write) clears the column so
|
|
15
|
+
* the next episode can re-notify.
|
|
16
|
+
*
|
|
17
|
+
* Empty-cart suppression: per the spec's edge case, empty carts still
|
|
18
|
+
* transition to `abandoned` by status, but the notification e-mail is
|
|
19
|
+
* suppressed (no buyer-side signal worth notifying anyone about).
|
|
20
|
+
*
|
|
21
|
+
* ## The sweep works in batches (issue #152)
|
|
22
|
+
*
|
|
23
|
+
* It used to load every eligible cart at once and then, per cart, run one
|
|
24
|
+
* `em.find(CartItem)` and one `CartAuditService.record()` — which flushed. Over
|
|
25
|
+
* an identity map that kept every cart and every audit row it had already
|
|
26
|
+
* touched, so each flush re-examined the whole run: 500 carts cost 8.4 s
|
|
27
|
+
* (16.8 ms each) and 2000 cost 132.0 s (66.0 ms each). Four times the carts,
|
|
28
|
+
* sixteen times the time. At the 50 000 the spec states its target over, the
|
|
29
|
+
* benchmark timed out rather than reporting a number, which is why it was
|
|
30
|
+
* excluded from the perf schedules.
|
|
31
|
+
*
|
|
32
|
+
* Three things changed, and only together do they make the cost linear:
|
|
33
|
+
*
|
|
34
|
+
* 1. **Eligible carts are read a batch at a time.** The flip removes a cart
|
|
35
|
+
* from the eligibility filter, so the same query paginates itself; nothing
|
|
36
|
+
* holds 50 000 entities.
|
|
37
|
+
* 2. **Line counts are one aggregate per batch**, not one query per cart.
|
|
38
|
+
* 3. **Audit rows land with the flip**, in one flush per batch, through
|
|
39
|
+
* {@link CartAuditService.recordWithin}.
|
|
40
|
+
*
|
|
41
|
+
* and `em.clear()` at the end of each batch keeps the identity map the size of
|
|
42
|
+
* one batch rather than the size of the run.
|
|
43
|
+
*
|
|
44
|
+
* ## What a failure mid-sweep now costs
|
|
45
|
+
*
|
|
46
|
+
* Each batch commits its status flip and its audit rows in the same flush, so
|
|
47
|
+
* a batch is atomic: never an `abandoned` cart whose audit row is missing.
|
|
48
|
+
* Before, the whole eligible set was flipped and committed **first** and the
|
|
49
|
+
* audit rows followed one at a time, so a crash left every cart the sweep had
|
|
50
|
+
* not reached yet flipped-but-unaudited — permanently, since
|
|
51
|
+
* `abandonment_notified_at` had already been set and the next tick skips those
|
|
52
|
+
* carts. The window that used to be "the rest of the run" is now "nothing".
|
|
53
|
+
*
|
|
54
|
+
* Batches before the failure are complete and durable; batches after it are
|
|
55
|
+
* untouched and eligible for the next tick. The notification e-mail is the one
|
|
56
|
+
* thing that stays best-effort — it is dispatched after the batch is durable,
|
|
57
|
+
* exactly as it was after the per-cart write before.
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* Eligible carts read, flipped, audited and flushed per round trip.
|
|
61
|
+
*
|
|
62
|
+
* Chosen for the flush rather than the query: 500 carts is 500 updates plus
|
|
63
|
+
* 1000 audit inserts in one transaction, which PostgreSQL takes in a few
|
|
64
|
+
* multi-row statements, while the memory held between two `em.clear()` calls
|
|
65
|
+
* stays bounded by this number rather than by the corpus. Overridable per
|
|
66
|
+
* instance so a test can cross a batch boundary with a handful of carts.
|
|
67
|
+
*/
|
|
68
|
+
export declare const DEFAULT_SWEEP_BATCH_SIZE = 500;
|
|
69
|
+
export interface CartAbandonmentWorkerDeps {
|
|
70
|
+
emFactory: () => EntityManager;
|
|
71
|
+
cartAuditService: CartAuditService;
|
|
72
|
+
/** Resolves the current `carts.abandonment.inactivity_minutes` from settings.
|
|
73
|
+
* `0` disables the sweep entirely. */
|
|
74
|
+
resolveInactivityMinutes: () => Promise<number>;
|
|
75
|
+
/** Resolves the current `carts.abandonment.notification_recipient`
|
|
76
|
+
* from settings. Empty string = no notification e-mail. */
|
|
77
|
+
resolveNotificationRecipient: () => Promise<string>;
|
|
78
|
+
/** Outbound e-mail dispatch. Implementation lives in composition so
|
|
79
|
+
* the worker stays decoupled from the email module. */
|
|
80
|
+
dispatchNotification?: (input: {
|
|
81
|
+
recipientEmail: string;
|
|
82
|
+
cartId: string;
|
|
83
|
+
ownerDisplayName: string | null;
|
|
84
|
+
lineCount: number;
|
|
85
|
+
organizationId: string | null;
|
|
86
|
+
}) => Promise<void>;
|
|
87
|
+
/** Defaults to {@link DEFAULT_SWEEP_BATCH_SIZE}. */
|
|
88
|
+
batchSize?: number;
|
|
89
|
+
}
|
|
90
|
+
export interface CartAbandonmentSweepResult {
|
|
91
|
+
abandonedCount: number;
|
|
92
|
+
notifiedCount: number;
|
|
93
|
+
}
|
|
94
|
+
export declare class CartAbandonmentWorker {
|
|
95
|
+
private readonly deps;
|
|
96
|
+
constructor(deps: CartAbandonmentWorkerDeps);
|
|
97
|
+
sweep(now?: Date): Promise<CartAbandonmentSweepResult>;
|
|
98
|
+
/**
|
|
99
|
+
* Lines per cart for a whole batch, as one grouped count.
|
|
100
|
+
*
|
|
101
|
+
* Executed through `em.execute` rather than awaited on the knex builder, so
|
|
102
|
+
* it runs on the EntityManager's transaction context — a bare knex call would
|
|
103
|
+
* take its own connection and, inside a wrapping transaction, would not see
|
|
104
|
+
* the rows the caller has written but not committed.
|
|
105
|
+
*/
|
|
106
|
+
private countLines;
|
|
107
|
+
/** Returns how many notifications were dispatched for this batch. */
|
|
108
|
+
private dispatchFor;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=cart-abandonment-worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-abandonment-worker.d.ts","sourceRoot":"","sources":["../../../src/backend/services/cart-abandonment-worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,gBAAgB,EAAE,gBAAgB,CAAC;IACnC;0CACsC;IACtC,wBAAwB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD;+DAC2D;IAC3D,4BAA4B,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACpD;2DACuD;IACvD,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE;QAC7B,cAAc,EAAE,MAAM,CAAC;QACvB,MAAM,EAAE,MAAM,CAAC;QACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;KAC/B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,qBAAqB;IACpB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,yBAAyB;IAEtD,KAAK,CAAC,GAAG,GAAE,IAAiB,GAAG,OAAO,CAAC,0BAA0B,CAAC;IA4ExE;;;;;;;OAOG;YACW,UAAU;IAmBxB,qEAAqE;YACvD,WAAW;CA+B1B"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { Cart } from '../entities/cart.entity.js';
|
|
2
|
+
import { CartItem } from '../entities/cart-item.entity.js';
|
|
3
|
+
import { withSystemScope } from '@endora-commerce/platform/tenancy';
|
|
4
|
+
/**
|
|
5
|
+
* CartAbandonmentWorker — feature 027 US5.
|
|
6
|
+
*
|
|
7
|
+
* Periodic sweep that transitions `active` carts inactive past the
|
|
8
|
+
* configured threshold to `abandoned`. Mirrors the RfqExpiryWorker
|
|
9
|
+
* pattern from feature 008: public `sweep()` is a plain async function
|
|
10
|
+
* so tests can drive it directly without Redis/BullMQ.
|
|
11
|
+
*
|
|
12
|
+
* Idempotency: the eligibility filter excludes carts that already have
|
|
13
|
+
* `abandonment_notified_at` set. The sweep cycle thus fires the
|
|
14
|
+
* notification at most once per abandonment episode; reactivation
|
|
15
|
+
* (CartService.touch / any owner-driven write) clears the column so
|
|
16
|
+
* the next episode can re-notify.
|
|
17
|
+
*
|
|
18
|
+
* Empty-cart suppression: per the spec's edge case, empty carts still
|
|
19
|
+
* transition to `abandoned` by status, but the notification e-mail is
|
|
20
|
+
* suppressed (no buyer-side signal worth notifying anyone about).
|
|
21
|
+
*
|
|
22
|
+
* ## The sweep works in batches (issue #152)
|
|
23
|
+
*
|
|
24
|
+
* It used to load every eligible cart at once and then, per cart, run one
|
|
25
|
+
* `em.find(CartItem)` and one `CartAuditService.record()` — which flushed. Over
|
|
26
|
+
* an identity map that kept every cart and every audit row it had already
|
|
27
|
+
* touched, so each flush re-examined the whole run: 500 carts cost 8.4 s
|
|
28
|
+
* (16.8 ms each) and 2000 cost 132.0 s (66.0 ms each). Four times the carts,
|
|
29
|
+
* sixteen times the time. At the 50 000 the spec states its target over, the
|
|
30
|
+
* benchmark timed out rather than reporting a number, which is why it was
|
|
31
|
+
* excluded from the perf schedules.
|
|
32
|
+
*
|
|
33
|
+
* Three things changed, and only together do they make the cost linear:
|
|
34
|
+
*
|
|
35
|
+
* 1. **Eligible carts are read a batch at a time.** The flip removes a cart
|
|
36
|
+
* from the eligibility filter, so the same query paginates itself; nothing
|
|
37
|
+
* holds 50 000 entities.
|
|
38
|
+
* 2. **Line counts are one aggregate per batch**, not one query per cart.
|
|
39
|
+
* 3. **Audit rows land with the flip**, in one flush per batch, through
|
|
40
|
+
* {@link CartAuditService.recordWithin}.
|
|
41
|
+
*
|
|
42
|
+
* and `em.clear()` at the end of each batch keeps the identity map the size of
|
|
43
|
+
* one batch rather than the size of the run.
|
|
44
|
+
*
|
|
45
|
+
* ## What a failure mid-sweep now costs
|
|
46
|
+
*
|
|
47
|
+
* Each batch commits its status flip and its audit rows in the same flush, so
|
|
48
|
+
* a batch is atomic: never an `abandoned` cart whose audit row is missing.
|
|
49
|
+
* Before, the whole eligible set was flipped and committed **first** and the
|
|
50
|
+
* audit rows followed one at a time, so a crash left every cart the sweep had
|
|
51
|
+
* not reached yet flipped-but-unaudited — permanently, since
|
|
52
|
+
* `abandonment_notified_at` had already been set and the next tick skips those
|
|
53
|
+
* carts. The window that used to be "the rest of the run" is now "nothing".
|
|
54
|
+
*
|
|
55
|
+
* Batches before the failure are complete and durable; batches after it are
|
|
56
|
+
* untouched and eligible for the next tick. The notification e-mail is the one
|
|
57
|
+
* thing that stays best-effort — it is dispatched after the batch is durable,
|
|
58
|
+
* exactly as it was after the per-cart write before.
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* Eligible carts read, flipped, audited and flushed per round trip.
|
|
62
|
+
*
|
|
63
|
+
* Chosen for the flush rather than the query: 500 carts is 500 updates plus
|
|
64
|
+
* 1000 audit inserts in one transaction, which PostgreSQL takes in a few
|
|
65
|
+
* multi-row statements, while the memory held between two `em.clear()` calls
|
|
66
|
+
* stays bounded by this number rather than by the corpus. Overridable per
|
|
67
|
+
* instance so a test can cross a batch boundary with a handful of carts.
|
|
68
|
+
*/
|
|
69
|
+
export const DEFAULT_SWEEP_BATCH_SIZE = 500;
|
|
70
|
+
export class CartAbandonmentWorker {
|
|
71
|
+
deps;
|
|
72
|
+
constructor(deps) {
|
|
73
|
+
this.deps = deps;
|
|
74
|
+
}
|
|
75
|
+
async sweep(now = new Date()) {
|
|
76
|
+
// Feature 050 — system-wide sweep across all customers; run under a system
|
|
77
|
+
// scope so the Cart reads/writes carry a tenant context (fail-closed guard).
|
|
78
|
+
return withSystemScope('cart-abandonment sweep', async () => {
|
|
79
|
+
const inactivityMinutes = await this.deps.resolveInactivityMinutes();
|
|
80
|
+
if (!Number.isFinite(inactivityMinutes) || inactivityMinutes <= 0) {
|
|
81
|
+
return { abandonedCount: 0, notifiedCount: 0 };
|
|
82
|
+
}
|
|
83
|
+
const em = this.deps.emFactory();
|
|
84
|
+
const cutoff = new Date(now.getTime() - inactivityMinutes * 60_000);
|
|
85
|
+
const batchSize = Math.max(1, this.deps.batchSize ?? DEFAULT_SWEEP_BATCH_SIZE);
|
|
86
|
+
let abandonedCount = 0;
|
|
87
|
+
let notifiedCount = 0;
|
|
88
|
+
/** Read once, on the first batch that has work — as it was per sweep. */
|
|
89
|
+
let recipient = null;
|
|
90
|
+
for (;;) {
|
|
91
|
+
const batch = await em.find(Cart, {
|
|
92
|
+
status: 'active',
|
|
93
|
+
lastActivityAt: { $lt: cutoff },
|
|
94
|
+
abandonmentNotifiedAt: null,
|
|
95
|
+
}, { limit: batchSize });
|
|
96
|
+
if (batch.length === 0)
|
|
97
|
+
break;
|
|
98
|
+
if (recipient === null)
|
|
99
|
+
recipient = await this.deps.resolveNotificationRecipient();
|
|
100
|
+
const shouldNotify = recipient.trim().length > 0 && Boolean(this.deps.dispatchNotification);
|
|
101
|
+
const lineCounts = await this.countLines(em, batch.map((c) => c.id));
|
|
102
|
+
for (const cart of batch) {
|
|
103
|
+
cart.status = 'abandoned';
|
|
104
|
+
cart.abandonmentNotifiedAt = now;
|
|
105
|
+
}
|
|
106
|
+
this.deps.cartAuditService.recordWithin(em, batch.map((cart) => ({
|
|
107
|
+
cartId: cart.id,
|
|
108
|
+
actorType: 'sweep',
|
|
109
|
+
action: 'abandonment_swept',
|
|
110
|
+
fromState: 'active',
|
|
111
|
+
toState: 'abandoned',
|
|
112
|
+
metadata: { lineCount: lineCounts.get(cart.id) ?? 0, inactivityMinutes },
|
|
113
|
+
})));
|
|
114
|
+
// One flush, one transaction: the flip and the rows auditing it.
|
|
115
|
+
await em.flush();
|
|
116
|
+
abandonedCount += batch.length;
|
|
117
|
+
// Notification fan-out happens AFTER the batch is durable so a failure
|
|
118
|
+
// in the e-mail dispatch cannot leave status in a transient state.
|
|
119
|
+
if (shouldNotify && this.deps.dispatchNotification) {
|
|
120
|
+
notifiedCount += await this.dispatchFor(batch, recipient, lineCounts);
|
|
121
|
+
}
|
|
122
|
+
// The identity map is the other half of the old superlinear cost: every
|
|
123
|
+
// later flush walked the carts and audit rows of every earlier batch.
|
|
124
|
+
// Nothing above this line survives the batch, so dropping it is free.
|
|
125
|
+
em.clear();
|
|
126
|
+
if (batch.length < batchSize)
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
return { abandonedCount, notifiedCount };
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Lines per cart for a whole batch, as one grouped count.
|
|
134
|
+
*
|
|
135
|
+
* Executed through `em.execute` rather than awaited on the knex builder, so
|
|
136
|
+
* it runs on the EntityManager's transaction context — a bare knex call would
|
|
137
|
+
* take its own connection and, inside a wrapping transaction, would not see
|
|
138
|
+
* the rows the caller has written but not committed.
|
|
139
|
+
*/
|
|
140
|
+
async countLines(em, cartIds) {
|
|
141
|
+
if (cartIds.length === 0)
|
|
142
|
+
return new Map();
|
|
143
|
+
const knex = em.getKnex();
|
|
144
|
+
// The table name comes from the entity's own metadata rather than a
|
|
145
|
+
// literal, so a rename stays a one-file change.
|
|
146
|
+
const table = em.getMetadata().get(CartItem).tableName;
|
|
147
|
+
const rows = await em.execute(knex(table)
|
|
148
|
+
.select('cart_id')
|
|
149
|
+
.count({ line_count: '*' })
|
|
150
|
+
.whereIn('cart_id', [...cartIds])
|
|
151
|
+
.groupBy('cart_id'));
|
|
152
|
+
return new Map(rows.map((r) => [r.cart_id, Number(r.line_count)]));
|
|
153
|
+
}
|
|
154
|
+
/** Returns how many notifications were dispatched for this batch. */
|
|
155
|
+
async dispatchFor(batch, recipient, lineCounts) {
|
|
156
|
+
const dispatch = this.deps.dispatchNotification;
|
|
157
|
+
if (!dispatch)
|
|
158
|
+
return 0;
|
|
159
|
+
let sent = 0;
|
|
160
|
+
for (const cart of batch) {
|
|
161
|
+
const lineCount = lineCounts.get(cart.id) ?? 0;
|
|
162
|
+
// Empty-cart suppression — no buyer signal worth e-mailing.
|
|
163
|
+
if (lineCount === 0)
|
|
164
|
+
continue;
|
|
165
|
+
try {
|
|
166
|
+
await dispatch({
|
|
167
|
+
recipientEmail: recipient,
|
|
168
|
+
cartId: cart.id,
|
|
169
|
+
ownerDisplayName: cart.customerAccountId ?? null,
|
|
170
|
+
lineCount,
|
|
171
|
+
organizationId: cart.organizationId ?? null,
|
|
172
|
+
});
|
|
173
|
+
sent += 1;
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
// Notification failure must not roll back the status flip.
|
|
177
|
+
// The audit trail records the abandonment; an oncall who
|
|
178
|
+
// notices the e-mail never arrived can replay manually.
|
|
179
|
+
console.error('[cart-abandonment-worker] notification failed', err);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return sent;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=cart-abandonment-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cart-abandonment-worker.js","sourceRoot":"","sources":["../../../src/backend/services/cart-abandonment-worker.ts"],"names":[],"mappings":"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"}
|