@porulle/core 0.11.0 → 0.14.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/dist/auth/actor.d.ts +10 -0
- package/dist/auth/actor.d.ts.map +1 -0
- package/dist/auth/actor.js +91 -0
- package/dist/auth/auth-schema-guard.d.ts +2444 -0
- package/dist/auth/auth-schema-guard.d.ts.map +1 -0
- package/dist/auth/auth-schema-guard.js +113 -0
- package/dist/auth/auth-schema.d.ts +228 -0
- package/dist/auth/auth-schema.d.ts.map +1 -1
- package/dist/auth/auth-schema.js +24 -2
- package/dist/auth/middleware.d.ts +1 -0
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +16 -92
- package/dist/auth/org.d.ts +8 -2
- package/dist/auth/org.d.ts.map +1 -1
- package/dist/auth/org.js +12 -4
- package/dist/auth/organization-guard.d.ts +5 -0
- package/dist/auth/organization-guard.d.ts.map +1 -0
- package/dist/auth/organization-guard.js +124 -0
- package/dist/auth/permissions.d.ts +10 -0
- package/dist/auth/permissions.d.ts.map +1 -1
- package/dist/auth/permissions.js +31 -7
- package/dist/auth/role-authority.d.ts +44 -0
- package/dist/auth/role-authority.d.ts.map +1 -0
- package/dist/auth/role-authority.js +85 -0
- package/dist/auth/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +19 -1
- package/dist/auth/strict-org-resolution.d.ts.map +1 -1
- package/dist/auth/strict-org-resolution.js +9 -1
- package/dist/auth/types.d.ts +1 -1
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +2 -0
- package/dist/config/types.d.ts +32 -4
- package/dist/config/types.d.ts.map +1 -1
- package/dist/hooks/checkout.d.ts.map +1 -1
- package/dist/hooks/checkout.js +10 -9
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/interfaces/rest/audit-middleware.d.ts.map +1 -1
- package/dist/interfaces/rest/audit-middleware.js +2 -0
- package/dist/interfaces/rest/customer-portal.d.ts.map +1 -1
- package/dist/interfaces/rest/customer-portal.js +9 -8
- package/dist/interfaces/rest/route-coverage.d.ts +14 -0
- package/dist/interfaces/rest/route-coverage.d.ts.map +1 -0
- package/dist/interfaces/rest/route-coverage.js +81 -0
- package/dist/interfaces/rest/router.d.ts +7 -2
- package/dist/interfaces/rest/router.d.ts.map +1 -1
- package/dist/interfaces/rest/router.js +8 -4
- package/dist/interfaces/rest/routes/admin/compensation-failures.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/admin/compensation-failures.js +7 -5
- package/dist/interfaces/rest/routes/admin/custom-field-proposals.js +2 -2
- package/dist/interfaces/rest/routes/admin/permissions.d.ts +1 -1
- package/dist/interfaces/rest/routes/admin/permissions.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/admin/permissions.js +2 -0
- package/dist/interfaces/rest/routes/admin/staff.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/admin/staff.js +126 -66
- package/dist/interfaces/rest/routes/admin-jobs.js +3 -3
- package/dist/interfaces/rest/routes/audit.js +3 -3
- package/dist/interfaces/rest/routes/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/carts.js +13 -10
- package/dist/interfaces/rest/routes/catalog.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/catalog.js +10 -2
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +30 -35
- package/dist/interfaces/rest/routes/documents.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/documents.js +10 -5
- package/dist/interfaces/rest/routes/inventory.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/inventory.js +2 -0
- package/dist/interfaces/rest/routes/media.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/media.js +7 -3
- package/dist/interfaces/rest/routes/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/orders.js +26 -8
- package/dist/interfaces/rest/routes/pricing.js +2 -2
- package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/promotions.js +13 -10
- package/dist/interfaces/rest/routes/search.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/search.js +2 -1
- package/dist/interfaces/rest/schemas/carts.d.ts +7 -0
- package/dist/interfaces/rest/schemas/carts.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/customer-portal.d.ts +35 -0
- package/dist/interfaces/rest/schemas/customer-portal.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/orders.d.ts +204 -0
- package/dist/interfaces/rest/schemas/orders.d.ts.map +1 -1
- package/dist/interfaces/rest/schemas/responses.d.ts +71 -0
- package/dist/interfaces/rest/schemas/responses.d.ts.map +1 -1
- package/dist/interfaces/rest/utils.d.ts +15 -0
- package/dist/interfaces/rest/utils.d.ts.map +1 -1
- package/dist/interfaces/rest/utils.js +45 -2
- package/dist/kernel/compensation/executor.js +2 -2
- package/dist/kernel/database/plugin-types.d.ts +11 -0
- package/dist/kernel/database/plugin-types.d.ts.map +1 -1
- package/dist/kernel/database/scoped-db.d.ts +12 -0
- package/dist/kernel/database/scoped-db.d.ts.map +1 -1
- package/dist/kernel/database/scoped-db.js +12 -0
- package/dist/kernel/database/tx-context.d.ts +8 -0
- package/dist/kernel/database/tx-context.d.ts.map +1 -1
- package/dist/kernel/database/tx-context.js +13 -0
- package/dist/kernel/hooks/create-context.d.ts +2 -0
- package/dist/kernel/hooks/create-context.d.ts.map +1 -1
- package/dist/kernel/hooks/create-context.js +1 -0
- package/dist/kernel/hooks/types.d.ts +2 -0
- package/dist/kernel/hooks/types.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.js +9 -5
- package/dist/modules/analytics/service.js +2 -2
- package/dist/modules/audit/service.js +3 -3
- package/dist/modules/cart/repository/index.d.ts +13 -7
- package/dist/modules/cart/repository/index.d.ts.map +1 -1
- package/dist/modules/cart/repository/index.js +16 -10
- package/dist/modules/cart/schema.d.ts +17 -0
- package/dist/modules/cart/schema.d.ts.map +1 -1
- package/dist/modules/cart/schema.js +4 -0
- package/dist/modules/cart/service.d.ts +22 -9
- package/dist/modules/cart/service.d.ts.map +1 -1
- package/dist/modules/cart/service.js +57 -33
- package/dist/modules/catalog/brand-service.js +7 -7
- package/dist/modules/catalog/category-service.js +7 -7
- package/dist/modules/catalog/entity-service.d.ts +14 -7
- package/dist/modules/catalog/entity-service.d.ts.map +1 -1
- package/dist/modules/catalog/entity-service.js +109 -31
- package/dist/modules/catalog/read-policy.d.ts +9 -0
- package/dist/modules/catalog/read-policy.d.ts.map +1 -0
- package/dist/modules/catalog/read-policy.js +8 -0
- package/dist/modules/catalog/repository/index.d.ts +3 -2
- package/dist/modules/catalog/repository/index.d.ts.map +1 -1
- package/dist/modules/catalog/repository/index.js +14 -4
- package/dist/modules/catalog/service.d.ts +15 -13
- package/dist/modules/catalog/service.d.ts.map +1 -1
- package/dist/modules/catalog/service.js +25 -18
- package/dist/modules/customers/module.d.ts.map +1 -1
- package/dist/modules/customers/module.js +1 -0
- package/dist/modules/customers/service.d.ts +2 -0
- package/dist/modules/customers/service.d.ts.map +1 -1
- package/dist/modules/customers/service.js +21 -20
- package/dist/modules/documents/module.d.ts.map +1 -1
- package/dist/modules/documents/module.js +1 -0
- package/dist/modules/documents/service.d.ts +18 -4
- package/dist/modules/documents/service.d.ts.map +1 -1
- package/dist/modules/documents/service.js +54 -16
- package/dist/modules/fulfillment/module.d.ts.map +1 -1
- package/dist/modules/fulfillment/module.js +1 -0
- package/dist/modules/fulfillment/service.d.ts +2 -0
- package/dist/modules/fulfillment/service.d.ts.map +1 -1
- package/dist/modules/fulfillment/service.js +15 -6
- package/dist/modules/inventory/service.d.ts.map +1 -1
- package/dist/modules/inventory/service.js +15 -14
- package/dist/modules/media/service.js +6 -6
- package/dist/modules/orders/guest-access.d.ts +27 -0
- package/dist/modules/orders/guest-access.d.ts.map +1 -0
- package/dist/modules/orders/guest-access.js +46 -0
- package/dist/modules/orders/module.d.ts.map +1 -1
- package/dist/modules/orders/module.js +1 -0
- package/dist/modules/orders/quote.d.ts.map +1 -1
- package/dist/modules/orders/quote.js +1 -0
- package/dist/modules/orders/repository/index.d.ts +1 -1
- package/dist/modules/orders/repository/index.d.ts.map +1 -1
- package/dist/modules/orders/repository/index.js +2 -2
- package/dist/modules/orders/schema.d.ts +17 -0
- package/dist/modules/orders/schema.d.ts.map +1 -1
- package/dist/modules/orders/schema.js +5 -3
- package/dist/modules/orders/service.d.ts +16 -7
- package/dist/modules/orders/service.d.ts.map +1 -1
- package/dist/modules/orders/service.js +184 -55
- package/dist/modules/pricing/module.d.ts.map +1 -1
- package/dist/modules/pricing/module.js +1 -0
- package/dist/modules/pricing/service.d.ts +2 -0
- package/dist/modules/pricing/service.d.ts.map +1 -1
- package/dist/modules/pricing/service.js +12 -11
- package/dist/modules/promotions/module.d.ts.map +1 -1
- package/dist/modules/promotions/module.js +1 -0
- package/dist/modules/promotions/service.d.ts +3 -0
- package/dist/modules/promotions/service.d.ts.map +1 -1
- package/dist/modules/promotions/service.js +19 -11
- package/dist/modules/search/adapter.d.ts +11 -0
- package/dist/modules/search/adapter.d.ts.map +1 -1
- package/dist/modules/search/module.d.ts.map +1 -1
- package/dist/modules/search/module.js +4 -1
- package/dist/modules/search/service.d.ts +1 -0
- package/dist/modules/search/service.d.ts.map +1 -1
- package/dist/modules/search/service.js +57 -6
- package/dist/modules/settings/module.d.ts.map +1 -1
- package/dist/modules/settings/module.js +1 -0
- package/dist/modules/settings/service.d.ts +3 -0
- package/dist/modules/settings/service.d.ts.map +1 -1
- package/dist/modules/settings/service.js +6 -4
- package/dist/modules/shipping/service.js +9 -9
- package/dist/modules/tax/module.d.ts.map +1 -1
- package/dist/modules/tax/module.js +1 -0
- package/dist/modules/tax/service.d.ts +3 -0
- package/dist/modules/tax/service.d.ts.map +1 -1
- package/dist/modules/tax/service.js +11 -9
- package/dist/modules/webhooks/hook.js +2 -2
- package/dist/modules/webhooks/module.d.ts.map +1 -1
- package/dist/modules/webhooks/module.js +1 -0
- package/dist/modules/webhooks/service.d.ts +2 -0
- package/dist/modules/webhooks/service.d.ts.map +1 -1
- package/dist/modules/webhooks/service.js +4 -4
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +28 -2
- package/dist/test-utils/create-pglite-adapter.d.ts.map +1 -1
- package/dist/test-utils/create-pglite-adapter.js +35 -13
- package/dist/test-utils/create-test-config.d.ts.map +1 -1
- package/dist/test-utils/create-test-config.js +4 -0
- package/dist/test-utils/rest-api-test-utils.d.ts.map +1 -1
- package/dist/test-utils/rest-api-test-utils.js +2 -0
- package/dist/test-utils/test-actors.js +2 -2
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +11 -0
- package/package.json +6 -5
- package/src/auth/actor.ts +110 -0
- package/src/auth/auth-schema-guard.ts +143 -0
- package/src/auth/auth-schema.ts +50 -19
- package/src/auth/middleware.ts +25 -101
- package/src/auth/org.ts +15 -3
- package/src/auth/organization-guard.ts +158 -0
- package/src/auth/permissions.ts +31 -8
- package/src/auth/role-authority.ts +107 -0
- package/src/auth/setup.ts +19 -1
- package/src/auth/strict-org-resolution.ts +7 -1
- package/src/auth/types.ts +1 -1
- package/src/config/defaults.ts +2 -0
- package/src/config/types.ts +31 -4
- package/src/hooks/checkout.ts +19 -12
- package/src/index.ts +21 -6
- package/src/interfaces/rest/audit-middleware.ts +2 -0
- package/src/interfaces/rest/customer-portal.ts +31 -24
- package/src/interfaces/rest/route-coverage.ts +103 -0
- package/src/interfaces/rest/router.ts +16 -10
- package/src/interfaces/rest/routes/admin/compensation-failures.ts +8 -5
- package/src/interfaces/rest/routes/admin/custom-field-proposals.ts +2 -2
- package/src/interfaces/rest/routes/admin/permissions.ts +3 -1
- package/src/interfaces/rest/routes/admin/staff.ts +161 -72
- package/src/interfaces/rest/routes/admin-jobs.ts +3 -3
- package/src/interfaces/rest/routes/audit.ts +3 -3
- package/src/interfaces/rest/routes/carts.ts +17 -7
- package/src/interfaces/rest/routes/catalog.ts +11 -0
- package/src/interfaces/rest/routes/checkout.ts +41 -38
- package/src/interfaces/rest/routes/documents.ts +15 -1
- package/src/interfaces/rest/routes/inventory.ts +2 -0
- package/src/interfaces/rest/routes/media.ts +8 -3
- package/src/interfaces/rest/routes/orders.ts +27 -8
- package/src/interfaces/rest/routes/pricing.ts +2 -2
- package/src/interfaces/rest/routes/promotions.ts +12 -9
- package/src/interfaces/rest/routes/search.ts +3 -1
- package/src/interfaces/rest/utils.ts +61 -2
- package/src/kernel/compensation/executor.ts +2 -2
- package/src/kernel/database/plugin-types.ts +11 -0
- package/src/kernel/database/scoped-db.ts +12 -0
- package/src/kernel/database/tx-context.ts +25 -0
- package/src/kernel/hooks/create-context.ts +3 -0
- package/src/kernel/hooks/types.ts +2 -0
- package/src/kernel/plugin/manifest.ts +9 -5
- package/src/modules/analytics/service.ts +2 -2
- package/src/modules/audit/service.ts +3 -3
- package/src/modules/cart/repository/index.ts +22 -8
- package/src/modules/cart/schema.ts +4 -0
- package/src/modules/cart/service.ts +62 -27
- package/src/modules/catalog/brand-service.ts +7 -7
- package/src/modules/catalog/category-service.ts +7 -7
- package/src/modules/catalog/entity-service.ts +117 -35
- package/src/modules/catalog/read-policy.ts +16 -0
- package/src/modules/catalog/repository/index.ts +19 -3
- package/src/modules/catalog/service.ts +46 -31
- package/src/modules/customers/module.ts +2 -0
- package/src/modules/customers/service.ts +23 -22
- package/src/modules/documents/module.ts +2 -0
- package/src/modules/documents/service.ts +74 -11
- package/src/modules/fulfillment/module.ts +2 -0
- package/src/modules/fulfillment/service.ts +17 -6
- package/src/modules/inventory/service.ts +15 -24
- package/src/modules/media/service.ts +6 -6
- package/src/modules/orders/guest-access.ts +55 -0
- package/src/modules/orders/module.ts +1 -0
- package/src/modules/orders/quote.ts +1 -0
- package/src/modules/orders/repository/index.ts +2 -0
- package/src/modules/orders/schema.ts +5 -3
- package/src/modules/orders/service.ts +302 -52
- package/src/modules/pricing/module.ts +2 -0
- package/src/modules/pricing/service.ts +14 -16
- package/src/modules/promotions/module.ts +2 -0
- package/src/modules/promotions/service.ts +25 -16
- package/src/modules/search/adapter.ts +9 -0
- package/src/modules/search/module.ts +5 -1
- package/src/modules/search/service.ts +55 -6
- package/src/modules/settings/module.ts +2 -0
- package/src/modules/settings/service.ts +8 -4
- package/src/modules/shipping/service.ts +9 -9
- package/src/modules/tax/module.ts +1 -0
- package/src/modules/tax/service.ts +13 -9
- package/src/modules/webhooks/hook.ts +2 -2
- package/src/modules/webhooks/module.ts +2 -0
- package/src/modules/webhooks/service.ts +6 -4
- package/src/runtime/server.ts +41 -2
- package/src/test-utils/create-pglite-adapter.ts +34 -12
- package/src/test-utils/create-test-config.ts +10 -6
- package/src/test-utils/rest-api-test-utils.ts +2 -0
- package/src/test-utils/test-actors.ts +2 -2
- package/src/utils/id.ts +20 -0
|
@@ -2,7 +2,8 @@ import type { Hono } from "hono";
|
|
|
2
2
|
import type { OpenAPIHono, RouteConfig } from "@hono/zod-openapi";
|
|
3
3
|
import type { PluginDb } from "../database/plugin-types.js";
|
|
4
4
|
import type { DatabaseAdapter } from "../database/adapter.js";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveOrgIdForCommerce } from "../../auth/org.js";
|
|
6
|
+
import { isRoutePermissionGuard, markRoutePermissionGuard } from "../../interfaces/rest/utils.js";
|
|
6
7
|
import { createScopedDb } from "../database/scoped-db.js";
|
|
7
8
|
import {
|
|
8
9
|
getPluginDatabaseScopeOrganizationId,
|
|
@@ -97,7 +98,7 @@ function buildPluginContextDatabase(
|
|
|
97
98
|
): PluginContext["database"] {
|
|
98
99
|
const orgGetter = () =>
|
|
99
100
|
getPluginDatabaseScopeOrganizationId() ??
|
|
100
|
-
|
|
101
|
+
resolveOrgIdForCommerce(null, commerceConfig);
|
|
101
102
|
|
|
102
103
|
const scopedDb = createScopedDb(rawDb, orgGetter);
|
|
103
104
|
const warnUnscoped = createRateLimitedUnscopedAccessWarning(logger);
|
|
@@ -288,7 +289,7 @@ export function defineCommercePlugin(
|
|
|
288
289
|
const wrappedHandler = async (...args: unknown[]) => {
|
|
289
290
|
const c = args[0] as { get?: (key: string) => unknown } | undefined;
|
|
290
291
|
const actor = c?.get?.("actor");
|
|
291
|
-
const orgId =
|
|
292
|
+
const orgId = resolveOrgIdForCommerce(actor, k.config);
|
|
292
293
|
try {
|
|
293
294
|
return await runWithPluginDatabaseScope(orgId, async () => {
|
|
294
295
|
const out = originalHandler(...args);
|
|
@@ -305,19 +306,22 @@ export function defineCommercePlugin(
|
|
|
305
306
|
throw err; // re-throw so global handler still catches
|
|
306
307
|
}
|
|
307
308
|
};
|
|
309
|
+
const registeredHandler = isRoutePermissionGuard(originalHandler)
|
|
310
|
+
? markRoutePermissionGuard(wrappedHandler)
|
|
311
|
+
: wrappedHandler;
|
|
308
312
|
|
|
309
313
|
if ("openapi" in route) {
|
|
310
314
|
// OpenAPI route: validated by Zod, appears in /api/doc
|
|
311
315
|
// Hono type interop — OpenAPIHono.openapi() expects strict RouteConfig+Handler
|
|
312
316
|
// generics that can't be statically resolved for dynamic plugin routes.
|
|
313
317
|
// @ts-expect-error -- dynamic plugin routes cannot satisfy Hono's strict handler generics
|
|
314
|
-
(app as OpenAPIHono).openapi(route.openapi,
|
|
318
|
+
(app as OpenAPIHono).openapi(route.openapi, registeredHandler);
|
|
315
319
|
} else {
|
|
316
320
|
// Legacy route: raw handler, invisible to OpenAPI spec.
|
|
317
321
|
// Explicit dispatch avoids casting Hono to a Record.
|
|
318
322
|
// Handler cast is a single `as any` (Hono's overloaded method
|
|
319
323
|
// signatures can't unify with our generic handler shape).
|
|
320
|
-
const h =
|
|
324
|
+
const h = registeredHandler as any;
|
|
321
325
|
switch (route.method.toLowerCase()) {
|
|
322
326
|
case "get": app.get(route.path, h); break;
|
|
323
327
|
case "post": app.post(route.path, h); break;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { CommerceConfig } from "../../config/types.js";
|
|
10
10
|
import { CommerceValidationError } from "../../kernel/errors.js";
|
|
11
11
|
import { Err, Ok, type Result } from "../../kernel/result.js";
|
|
12
|
-
import {
|
|
12
|
+
import { resolveOrgIdForCommerce } from "../../auth/org.js";
|
|
13
13
|
import type { Actor } from "../../auth/types.js";
|
|
14
14
|
import type { ReportParams, RetailReportsEngine } from "./reports.js";
|
|
15
15
|
import { RETAIL_REPORTS } from "./reports.js";
|
|
@@ -135,7 +135,7 @@ export class AnalyticsService {
|
|
|
135
135
|
if (!this.deps.reports) {
|
|
136
136
|
return Err(new CommerceValidationError("Retail reports are not available."));
|
|
137
137
|
}
|
|
138
|
-
const orgId =
|
|
138
|
+
const orgId = resolveOrgIdForCommerce(actor ?? null, this.deps.config);
|
|
139
139
|
return this.deps.reports.run(name, params, orgId);
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -3,7 +3,7 @@ import type { InferSelectModel } from "drizzle-orm";
|
|
|
3
3
|
import type { DrizzleDatabase } from "../../kernel/database/drizzle-db.js";
|
|
4
4
|
import type { TxContext } from "../../kernel/database/tx-context.js";
|
|
5
5
|
import type { HookContext } from "../../kernel/hooks/types.js";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveOrgIdForCommerce } from "../../auth/org.js";
|
|
7
7
|
import { auditLog } from "./schema.js";
|
|
8
8
|
|
|
9
9
|
export type AuditEntry = InferSelectModel<typeof auditLog>;
|
|
@@ -47,7 +47,7 @@ export function createNullAuditService(): AuditService {
|
|
|
47
47
|
async record(args) {
|
|
48
48
|
entries.push({
|
|
49
49
|
id: crypto.randomUUID(),
|
|
50
|
-
organizationId:
|
|
50
|
+
organizationId: resolveOrgIdForCommerce(args.ctx.actor, args.ctx.commerceConfig),
|
|
51
51
|
entityType: args.entityType,
|
|
52
52
|
entityId: args.entityId,
|
|
53
53
|
event: args.event,
|
|
@@ -94,7 +94,7 @@ export function createAuditService(db: DrizzleDatabase): AuditService {
|
|
|
94
94
|
: db;
|
|
95
95
|
|
|
96
96
|
await dbOrTx.insert(auditLog).values({
|
|
97
|
-
organizationId:
|
|
97
|
+
organizationId: resolveOrgIdForCommerce(ctx.actor, ctx.commerceConfig),
|
|
98
98
|
entityType,
|
|
99
99
|
entityId,
|
|
100
100
|
event,
|
|
@@ -130,34 +130,48 @@ export class CartRepository {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
|
-
* Atomically transitions a cart from "active" to "checking_out"
|
|
134
|
-
*
|
|
135
|
-
* the cart was not in "active" status
|
|
136
|
-
* claimed it). This prevents TOCTOU race
|
|
133
|
+
* Atomically transitions a cart from "active" to "checking_out" and records
|
|
134
|
+
* `claimToken` as the holder of that claim. Returns the updated cart if the
|
|
135
|
+
* transition succeeded, or undefined if the cart was not in "active" status
|
|
136
|
+
* (e.g., a concurrent checkout already claimed it). This prevents TOCTOU race
|
|
137
|
+
* conditions on double-checkout.
|
|
137
138
|
*/
|
|
138
139
|
async transitionToCheckingOut(
|
|
139
140
|
id: string,
|
|
141
|
+
claimToken: string,
|
|
140
142
|
ctx?: TxContext,
|
|
141
143
|
): Promise<Cart | undefined> {
|
|
142
144
|
const db = this.getDb(ctx);
|
|
143
145
|
const rows = await db
|
|
144
146
|
.update(carts)
|
|
145
|
-
.set({ status: "checking_out", updatedAt: new Date() })
|
|
147
|
+
.set({ status: "checking_out", checkoutClaimToken: claimToken, updatedAt: new Date() })
|
|
146
148
|
.where(and(eq(carts.id, id), eq(carts.status, "active")))
|
|
147
149
|
.returning();
|
|
148
150
|
return rows[0];
|
|
149
151
|
}
|
|
150
152
|
|
|
151
|
-
/**
|
|
153
|
+
/**
|
|
154
|
+
* Release an in-progress checkout claim held by `claimToken`. Terminal cart
|
|
155
|
+
* states are untouched, and so is a claim held by a different checkout
|
|
156
|
+
* attempt — a losing attempt's failure path must not reopen the cart the
|
|
157
|
+
* winner is still checking out.
|
|
158
|
+
*/
|
|
152
159
|
async releaseCheckoutClaim(
|
|
153
160
|
id: string,
|
|
161
|
+
claimToken: string,
|
|
154
162
|
ctx?: TxContext,
|
|
155
163
|
): Promise<Cart | undefined> {
|
|
156
164
|
const db = this.getDb(ctx);
|
|
157
165
|
const rows = await db
|
|
158
166
|
.update(carts)
|
|
159
|
-
.set({ status: "active", updatedAt: new Date() })
|
|
160
|
-
.where(
|
|
167
|
+
.set({ status: "active", checkoutClaimToken: null, updatedAt: new Date() })
|
|
168
|
+
.where(
|
|
169
|
+
and(
|
|
170
|
+
eq(carts.id, id),
|
|
171
|
+
eq(carts.status, "checking_out"),
|
|
172
|
+
eq(carts.checkoutClaimToken, claimToken),
|
|
173
|
+
),
|
|
174
|
+
)
|
|
161
175
|
.returning();
|
|
162
176
|
return rows[0];
|
|
163
177
|
}
|
|
@@ -15,6 +15,10 @@ export const carts = pgTable("carts", {
|
|
|
15
15
|
// Shopper contact for guest carts — enables abandoned-checkout recovery
|
|
16
16
|
email: text("email"),
|
|
17
17
|
secret: text("secret"),
|
|
18
|
+
// Owner of an in-progress `checking_out` claim. Minted by the checkout
|
|
19
|
+
// attempt that wins the CAS; only that holder may release the claim, so a
|
|
20
|
+
// losing attempt's failure path cannot unwind the winner's claim.
|
|
21
|
+
checkoutClaimToken: text("checkout_claim_token"),
|
|
18
22
|
metadata: jsonb("metadata").$type<Record<string, unknown>>().default({}),
|
|
19
23
|
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
|
20
24
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveOrgIdForCommerce } from "../../auth/org.js";
|
|
2
2
|
import { assertOwnership, assertPermission } from "../../auth/permissions.js";
|
|
3
3
|
import type { Actor } from "../../auth/types.js";
|
|
4
4
|
import type { CommerceConfig } from "../../config/types.js";
|
|
@@ -60,6 +60,7 @@ function makeContext(
|
|
|
60
60
|
actor: Actor | null,
|
|
61
61
|
services: Record<string, unknown>,
|
|
62
62
|
database: DatabaseAdapter,
|
|
63
|
+
config: CommerceConfig,
|
|
63
64
|
tx: unknown = null,
|
|
64
65
|
): HookContext {
|
|
65
66
|
return createHookContext({
|
|
@@ -69,6 +70,7 @@ function makeContext(
|
|
|
69
70
|
services,
|
|
70
71
|
context: { moduleName: "cart" },
|
|
71
72
|
database: { db: database.db as PluginDb },
|
|
73
|
+
commerceConfig: config,
|
|
72
74
|
});
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -98,7 +100,7 @@ export class CartService {
|
|
|
98
100
|
|
|
99
101
|
const ttlMinutes = this.deps.config.cart?.ttlMinutes ?? 60 * 24 * 7;
|
|
100
102
|
const now = new Date();
|
|
101
|
-
const orgId =
|
|
103
|
+
const orgId = resolveOrgIdForCommerce(actor ?? null, this.deps.config);
|
|
102
104
|
|
|
103
105
|
// SECURITY: cart.customerId is service-managed.
|
|
104
106
|
//
|
|
@@ -145,6 +147,7 @@ export class CartService {
|
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
const guestSecret = resolvedCustomerId === null ? crypto.randomUUID() : undefined;
|
|
148
151
|
const cart = await this.repo.create(
|
|
149
152
|
{
|
|
150
153
|
organizationId: orgId,
|
|
@@ -153,6 +156,7 @@ export class CartService {
|
|
|
153
156
|
metadata: input.metadata ?? {},
|
|
154
157
|
expiresAt: new Date(now.getTime() + ttlMinutes * 60 * 1000),
|
|
155
158
|
...(resolvedCustomerId !== null ? { customerId: resolvedCustomerId } : {}),
|
|
159
|
+
...(guestSecret !== undefined ? { secret: guestSecret } : {}),
|
|
156
160
|
...(input.email !== undefined ? { email: input.email } : {}),
|
|
157
161
|
},
|
|
158
162
|
ctx,
|
|
@@ -173,7 +177,7 @@ export class CartService {
|
|
|
173
177
|
// read any cart's contents, prices, line items, and metadata. This now
|
|
174
178
|
// requires either an authenticated owner/staff actor OR the cart's
|
|
175
179
|
// secret token (returned only at createGuest()).
|
|
176
|
-
const orgId =
|
|
180
|
+
const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.deps.config);
|
|
177
181
|
const cart = await this.repo.findById(orgId, id, ctx);
|
|
178
182
|
if (!cart) return Err(new CommerceNotFoundError("Cart not found."));
|
|
179
183
|
|
|
@@ -195,10 +199,31 @@ export class CartService {
|
|
|
195
199
|
});
|
|
196
200
|
}
|
|
197
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Reads a cart for a trusted server pipeline after the request boundary has
|
|
204
|
+
* already authenticated the caller. Organization scope is explicit; caller
|
|
205
|
+
* identity and guest secrets are intentionally not consulted here.
|
|
206
|
+
*/
|
|
207
|
+
async getByIdForInternalUse(
|
|
208
|
+
id: string,
|
|
209
|
+
organizationId: string,
|
|
210
|
+
ctx?: TxContext,
|
|
211
|
+
): Promise<Result<Cart & { lineItems: CartLineItem[] }>> {
|
|
212
|
+
const cart = await this.repo.findById(organizationId, id, ctx);
|
|
213
|
+
if (!cart) return Err(new CommerceNotFoundError("Cart not found."));
|
|
214
|
+
|
|
215
|
+
const lineItems = await this.repo.findLineItemsByCartId(id, ctx);
|
|
216
|
+
return Ok({
|
|
217
|
+
...cart,
|
|
218
|
+
lineItems,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
198
222
|
async addItem(
|
|
199
223
|
input: AddCartItemInput,
|
|
200
224
|
actor?: Actor | null,
|
|
201
225
|
ctx?: TxContext,
|
|
226
|
+
presentedSecret?: string,
|
|
202
227
|
): Promise<Result<CartLineItem>> {
|
|
203
228
|
try {
|
|
204
229
|
assertPermission(actor ?? null, "cart:update");
|
|
@@ -206,12 +231,12 @@ export class CartService {
|
|
|
206
231
|
return Err(toCommerceError(error));
|
|
207
232
|
}
|
|
208
233
|
|
|
209
|
-
const orgId =
|
|
234
|
+
const orgId = resolveOrgIdForCommerce(actor ?? null, this.deps.config);
|
|
210
235
|
const cart = await this.repo.findById(orgId, input.cartId, ctx);
|
|
211
236
|
if (!cart) return Err(new CommerceNotFoundError("Cart not found."));
|
|
212
237
|
|
|
213
238
|
try {
|
|
214
|
-
await this.assertCartOwnership(actor ?? null, cart, ctx);
|
|
239
|
+
await this.assertCartOwnership(actor ?? null, cart, ctx, presentedSecret);
|
|
215
240
|
} catch (error) {
|
|
216
241
|
return Err(toCommerceError(error));
|
|
217
242
|
}
|
|
@@ -253,7 +278,7 @@ export class CartService {
|
|
|
253
278
|
);
|
|
254
279
|
}
|
|
255
280
|
|
|
256
|
-
const context = makeContext(actor ?? null, this.deps.services, this.deps.database, ctx?.tx);
|
|
281
|
+
const context = makeContext(actor ?? null, this.deps.services, this.deps.database, this.deps.config, ctx?.tx);
|
|
257
282
|
const beforeHooks = this.deps.hooks.resolve(
|
|
258
283
|
"cart.beforeAddItem",
|
|
259
284
|
) as CartAddBeforeHook[];
|
|
@@ -319,6 +344,7 @@ export class CartService {
|
|
|
319
344
|
itemId: string,
|
|
320
345
|
actor?: Actor | null,
|
|
321
346
|
ctx?: TxContext,
|
|
347
|
+
presentedSecret?: string,
|
|
322
348
|
): Promise<Result<void>> {
|
|
323
349
|
try {
|
|
324
350
|
assertPermission(actor ?? null, "cart:update");
|
|
@@ -326,12 +352,12 @@ export class CartService {
|
|
|
326
352
|
return Err(toCommerceError(error));
|
|
327
353
|
}
|
|
328
354
|
|
|
329
|
-
const orgId =
|
|
355
|
+
const orgId = resolveOrgIdForCommerce(actor ?? null, this.deps.config);
|
|
330
356
|
const cart = await this.repo.findById(orgId, cartId, ctx);
|
|
331
357
|
if (!cart) return Err(new CommerceNotFoundError("Cart not found."));
|
|
332
358
|
|
|
333
359
|
try {
|
|
334
|
-
await this.assertCartOwnership(actor ?? null, cart, ctx);
|
|
360
|
+
await this.assertCartOwnership(actor ?? null, cart, ctx, presentedSecret);
|
|
335
361
|
} catch (error) {
|
|
336
362
|
return Err(toCommerceError(error));
|
|
337
363
|
}
|
|
@@ -341,7 +367,7 @@ export class CartService {
|
|
|
341
367
|
return Err(new CommerceNotFoundError("Cart item not found."));
|
|
342
368
|
}
|
|
343
369
|
|
|
344
|
-
const context = makeContext(actor ?? null, this.deps.services, this.deps.database, ctx?.tx);
|
|
370
|
+
const context = makeContext(actor ?? null, this.deps.services, this.deps.database, this.deps.config, ctx?.tx);
|
|
345
371
|
const beforeHooks = this.deps.hooks.resolve(
|
|
346
372
|
"cart.beforeRemoveItem",
|
|
347
373
|
) as CartRemoveBeforeHook[];
|
|
@@ -360,6 +386,7 @@ export class CartService {
|
|
|
360
386
|
input: UpdateCartItemInput,
|
|
361
387
|
actor?: Actor | null,
|
|
362
388
|
ctx?: TxContext,
|
|
389
|
+
presentedSecret?: string,
|
|
363
390
|
): Promise<Result<CartLineItem>> {
|
|
364
391
|
try {
|
|
365
392
|
assertPermission(actor ?? null, "cart:update");
|
|
@@ -367,12 +394,12 @@ export class CartService {
|
|
|
367
394
|
return Err(toCommerceError(error));
|
|
368
395
|
}
|
|
369
396
|
|
|
370
|
-
const orgId =
|
|
397
|
+
const orgId = resolveOrgIdForCommerce(actor ?? null, this.deps.config);
|
|
371
398
|
const cart = await this.repo.findById(orgId, input.cartId, ctx);
|
|
372
399
|
if (!cart) return Err(new CommerceNotFoundError("Cart not found."));
|
|
373
400
|
|
|
374
401
|
try {
|
|
375
|
-
await this.assertCartOwnership(actor ?? null, cart, ctx);
|
|
402
|
+
await this.assertCartOwnership(actor ?? null, cart, ctx, presentedSecret);
|
|
376
403
|
} catch (error) {
|
|
377
404
|
return Err(toCommerceError(error));
|
|
378
405
|
}
|
|
@@ -388,7 +415,7 @@ export class CartService {
|
|
|
388
415
|
);
|
|
389
416
|
}
|
|
390
417
|
|
|
391
|
-
const context = makeContext(actor ?? null, this.deps.services, this.deps.database, ctx?.tx);
|
|
418
|
+
const context = makeContext(actor ?? null, this.deps.services, this.deps.database, this.deps.config, ctx?.tx);
|
|
392
419
|
const beforeHooks = this.deps.hooks.resolve(
|
|
393
420
|
"cart.beforeUpdateQuantity",
|
|
394
421
|
) as CartUpdateBeforeHook[];
|
|
@@ -424,7 +451,7 @@ export class CartService {
|
|
|
424
451
|
return Err(toCommerceError(error));
|
|
425
452
|
}
|
|
426
453
|
|
|
427
|
-
const orgId =
|
|
454
|
+
const orgId = resolveOrgIdForCommerce(actor ?? null, this.deps.config);
|
|
428
455
|
const source = await this.repo.findById(orgId, sourceCartId, ctx);
|
|
429
456
|
const target = await this.repo.findById(orgId, targetCartId, ctx);
|
|
430
457
|
if (!source || !target)
|
|
@@ -479,7 +506,7 @@ export class CartService {
|
|
|
479
506
|
} catch (error) {
|
|
480
507
|
return Err(toCommerceError(error));
|
|
481
508
|
}
|
|
482
|
-
const orgId =
|
|
509
|
+
const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.deps.config);
|
|
483
510
|
const rows = await this.repo.list(
|
|
484
511
|
orgId,
|
|
485
512
|
{
|
|
@@ -518,7 +545,7 @@ export class CartService {
|
|
|
518
545
|
} catch (error) {
|
|
519
546
|
return Err(toCommerceError(error));
|
|
520
547
|
}
|
|
521
|
-
const orgId =
|
|
548
|
+
const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.deps.config);
|
|
522
549
|
const cart = await this.repo.findById(orgId, cartId, ctx);
|
|
523
550
|
if (!cart) return Err(new CommerceNotFoundError("Cart not found."));
|
|
524
551
|
if (cart.status === "checked_out" || cart.status === "merged" || cart.status === "checking_out") {
|
|
@@ -544,7 +571,7 @@ export class CartService {
|
|
|
544
571
|
null,
|
|
545
572
|
updated ?? cart,
|
|
546
573
|
"recover",
|
|
547
|
-
makeContext(actor ?? null, this.deps.services, this.deps.database, ctx?.tx ?? null),
|
|
574
|
+
makeContext(actor ?? null, this.deps.services, this.deps.database, this.deps.config, ctx?.tx ?? null),
|
|
548
575
|
);
|
|
549
576
|
|
|
550
577
|
return Ok({
|
|
@@ -558,7 +585,7 @@ export class CartService {
|
|
|
558
585
|
}
|
|
559
586
|
|
|
560
587
|
async abandon(cartId: string, actor?: Actor | null, ctx?: TxContext): Promise<Result<void>> {
|
|
561
|
-
const orgId =
|
|
588
|
+
const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.deps.config);
|
|
562
589
|
const cart = await this.repo.findById(orgId, cartId, ctx);
|
|
563
590
|
if (!cart) return Err(new CommerceNotFoundError("Cart not found."));
|
|
564
591
|
|
|
@@ -571,24 +598,27 @@ export class CartService {
|
|
|
571
598
|
actor?: Actor | null,
|
|
572
599
|
ctx?: TxContext,
|
|
573
600
|
): Promise<Result<void>> {
|
|
574
|
-
const orgId =
|
|
601
|
+
const orgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.deps.config);
|
|
575
602
|
const cart = await this.repo.findById(orgId, cartId, ctx);
|
|
576
603
|
if (!cart) return Err(new CommerceNotFoundError("Cart not found."));
|
|
577
604
|
|
|
578
|
-
await this.repo.
|
|
605
|
+
await this.repo.update(cartId, { status: "checked_out", checkoutClaimToken: null }, ctx);
|
|
579
606
|
return Ok(undefined);
|
|
580
607
|
}
|
|
581
608
|
|
|
582
609
|
/**
|
|
583
|
-
* Atomically transitions a cart from "active" to "checking_out"
|
|
584
|
-
* Returns Err if the cart was already claimed by
|
|
585
|
-
* This prevents TOCTOU race conditions on
|
|
610
|
+
* Atomically transitions a cart from "active" to "checking_out", recording
|
|
611
|
+
* `claimToken` as the holder. Returns Err if the cart was already claimed by
|
|
612
|
+
* a concurrent checkout. This prevents TOCTOU race conditions on
|
|
613
|
+
* double-checkout, and the token is what lets {@link releaseCheckoutClaim}
|
|
614
|
+
* tell the winner's release from a loser's.
|
|
586
615
|
*/
|
|
587
616
|
async claimForCheckout(
|
|
588
617
|
cartId: string,
|
|
618
|
+
claimToken: string,
|
|
589
619
|
ctx?: TxContext,
|
|
590
620
|
): Promise<Result<Cart>> {
|
|
591
|
-
const claimed = await this.repo.transitionToCheckingOut(cartId, ctx);
|
|
621
|
+
const claimed = await this.repo.transitionToCheckingOut(cartId, claimToken, ctx);
|
|
592
622
|
if (!claimed) {
|
|
593
623
|
return Err(
|
|
594
624
|
new CommerceValidationError(
|
|
@@ -601,13 +631,17 @@ export class CartService {
|
|
|
601
631
|
|
|
602
632
|
/**
|
|
603
633
|
* Atomically returns a failed checkout claim to `active`. A checked-out,
|
|
604
|
-
* merged, abandoned, or already-active cart is left untouched
|
|
634
|
+
* merged, abandoned, or already-active cart is left untouched — and so is a
|
|
635
|
+
* claim held by a different checkout attempt, so a losing attempt's failure
|
|
636
|
+
* path cannot reopen the cart the winner is still checking out. Returns null
|
|
637
|
+
* when nothing was released.
|
|
605
638
|
*/
|
|
606
639
|
async releaseCheckoutClaim(
|
|
607
640
|
cartId: string,
|
|
641
|
+
claimToken: string,
|
|
608
642
|
ctx?: TxContext,
|
|
609
643
|
): Promise<Result<Cart | null>> {
|
|
610
|
-
return Ok((await this.repo.releaseCheckoutClaim(cartId, ctx)) ?? null);
|
|
644
|
+
return Ok((await this.repo.releaseCheckoutClaim(cartId, claimToken, ctx)) ?? null);
|
|
611
645
|
}
|
|
612
646
|
|
|
613
647
|
/**
|
|
@@ -625,7 +659,8 @@ export class CartService {
|
|
|
625
659
|
|
|
626
660
|
const cart = await this.repo.create(
|
|
627
661
|
{
|
|
628
|
-
|
|
662
|
+
// Actor-less by design; resolves to the deployment's declared organization.
|
|
663
|
+
organizationId: resolveOrgIdForCommerce(null, this.deps.config),
|
|
629
664
|
customerId: undefined,
|
|
630
665
|
status: "active",
|
|
631
666
|
currency,
|
|
@@ -651,7 +686,7 @@ export class CartService {
|
|
|
651
686
|
actor: Actor,
|
|
652
687
|
ctx?: TxContext,
|
|
653
688
|
): Promise<Result<Cart>> {
|
|
654
|
-
const orgId =
|
|
689
|
+
const orgId = resolveOrgIdForCommerce(actor, this.deps.config);
|
|
655
690
|
const sourceCart = await this.repo.findById(orgId, sourceCartId, ctx);
|
|
656
691
|
if (!sourceCart || sourceCart.secret !== sourceSecret) {
|
|
657
692
|
return Err(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveOrgIdForCommerce } from "../../auth/org.js";
|
|
2
2
|
import { assertPermission } from "../../auth/permissions.js";
|
|
3
3
|
import type { Actor } from "../../auth/types.js";
|
|
4
4
|
import {
|
|
@@ -23,14 +23,14 @@ export class BrandService {
|
|
|
23
23
|
|
|
24
24
|
private assertSameOrg(resource: { organizationId?: string | null } | undefined, actor: Actor | null): void {
|
|
25
25
|
if (!resource) return;
|
|
26
|
-
const orgId =
|
|
26
|
+
const orgId = resolveOrgIdForCommerce(actor, this.deps.config);
|
|
27
27
|
if (resource.organizationId && resource.organizationId !== orgId) {
|
|
28
28
|
throw new CommerceNotFoundError("Entity not found.");
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
async listBrands(ctx?: TxContext): Promise<Result<Brand[]>> {
|
|
33
|
-
const allBrands = await this.repo.findAllBrands(
|
|
33
|
+
const allBrands = await this.repo.findAllBrands(resolveOrgIdForCommerce(ctx?.actor ?? null, this.deps.config), ctx);
|
|
34
34
|
return Ok(allBrands.sort((a, b) => a.displayName.localeCompare(b.displayName)));
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -40,7 +40,7 @@ export class BrandService {
|
|
|
40
40
|
const existingById = await this.repo.findBrandById(input.id, ctx);
|
|
41
41
|
if (existingById) return Err(new CommerceConflictError(`Brand with id ${input.id} already exists.`));
|
|
42
42
|
}
|
|
43
|
-
const orgId =
|
|
43
|
+
const orgId = resolveOrgIdForCommerce(actor, this.deps.config);
|
|
44
44
|
const existingBySlug = await this.repo.findBrandBySlug(orgId, input.slug, ctx);
|
|
45
45
|
if (existingBySlug) return Err(new CommerceConflictError(`Brand with slug ${input.slug} already exists.`));
|
|
46
46
|
return Ok(await this.repo.createBrand({ organizationId: orgId, ...(input.id ? { id: input.id } : {}), slug: input.slug, displayName: input.displayName, metadata: input.metadata ?? {} }, ctx));
|
|
@@ -52,7 +52,7 @@ export class BrandService {
|
|
|
52
52
|
if (!existing) return Err(new CommerceNotFoundError("Brand not found."));
|
|
53
53
|
try { this.assertSameOrg(existing, actor); } catch (error) { return Err(toCommerceError(error)); }
|
|
54
54
|
if (input.slug) {
|
|
55
|
-
const existingBySlug = await this.repo.findBrandBySlug(
|
|
55
|
+
const existingBySlug = await this.repo.findBrandBySlug(resolveOrgIdForCommerce(actor, this.deps.config), input.slug, ctx);
|
|
56
56
|
if (existingBySlug && existingBySlug.id !== id) return Err(new CommerceConflictError(`Brand with slug ${input.slug} already exists.`));
|
|
57
57
|
}
|
|
58
58
|
const updated = await this.repo.updateBrand(id, { ...(input.slug !== undefined ? { slug: input.slug } : {}), ...(input.displayName !== undefined ? { displayName: input.displayName } : {}), ...(input.metadata !== undefined ? { metadata: input.metadata } : {}) }, ctx);
|
|
@@ -75,7 +75,7 @@ export class BrandService {
|
|
|
75
75
|
const entity = await this.deps.repository.findEntityById(entityId, ctx);
|
|
76
76
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
77
77
|
try { this.assertSameOrg(entity, actor); } catch (error) { return Err(toCommerceError(error)); }
|
|
78
|
-
const addBrandOrgId =
|
|
78
|
+
const addBrandOrgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.deps.config);
|
|
79
79
|
const isBrandUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(brandId);
|
|
80
80
|
let brand = isBrandUuid ? await this.repo.findBrandById(brandId, ctx) : null;
|
|
81
81
|
if (!brand) brand = await this.repo.findBrandBySlug(addBrandOrgId, brandId, ctx);
|
|
@@ -93,7 +93,7 @@ export class BrandService {
|
|
|
93
93
|
try { this.assertSameOrg(entity, actor); } catch (error) { return Err(toCommerceError(error)); }
|
|
94
94
|
const isBrUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(brandId);
|
|
95
95
|
let brand = isBrUuid ? await this.repo.findBrandById(brandId, ctx) : null;
|
|
96
|
-
if (!brand) brand = await this.repo.findBrandBySlug(
|
|
96
|
+
if (!brand) brand = await this.repo.findBrandBySlug(resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.deps.config), brandId, ctx);
|
|
97
97
|
const removed = await this.repo.removeEntityFromBrand(entityId, brand?.id ?? brandId, ctx);
|
|
98
98
|
if (!removed) return Err(new CommerceNotFoundError("Brand assignment not found."));
|
|
99
99
|
return Ok(undefined);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveOrgIdForCommerce } from "../../auth/org.js";
|
|
2
2
|
import { assertPermission } from "../../auth/permissions.js";
|
|
3
3
|
import type { Actor } from "../../auth/types.js";
|
|
4
4
|
import {
|
|
@@ -23,14 +23,14 @@ export class CategoryService {
|
|
|
23
23
|
|
|
24
24
|
private assertSameOrg(resource: { organizationId?: string | null } | undefined, actor: Actor | null): void {
|
|
25
25
|
if (!resource) return;
|
|
26
|
-
const orgId =
|
|
26
|
+
const orgId = resolveOrgIdForCommerce(actor, this.deps.config);
|
|
27
27
|
if (resource.organizationId && resource.organizationId !== orgId) {
|
|
28
28
|
throw new CommerceNotFoundError("Entity not found.");
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
async listCategories(ctx?: TxContext, opts?: { includeArchived?: boolean }): Promise<Result<CategorySummary[]>> {
|
|
33
|
-
const allCategories = await this.repo.findAllCategories(
|
|
33
|
+
const allCategories = await this.repo.findAllCategories(resolveOrgIdForCommerce(ctx?.actor ?? null, this.deps.config), ctx);
|
|
34
34
|
const visible = opts?.includeArchived
|
|
35
35
|
? allCategories
|
|
36
36
|
: allCategories.filter((c) => (c.status ?? "active") === "active");
|
|
@@ -62,7 +62,7 @@ export class CategoryService {
|
|
|
62
62
|
const existingById = await this.repo.findCategoryById(input.id, ctx);
|
|
63
63
|
if (existingById) return Err(new CommerceConflictError(`Category with id ${input.id} already exists.`));
|
|
64
64
|
}
|
|
65
|
-
const orgId =
|
|
65
|
+
const orgId = resolveOrgIdForCommerce(actor, this.deps.config);
|
|
66
66
|
const existingBySlug = await this.repo.findCategoryBySlug(orgId, input.slug, ctx);
|
|
67
67
|
if (existingBySlug) return Err(new CommerceConflictError(`Category with slug ${input.slug} already exists.`));
|
|
68
68
|
const category = await this.repo.createCategory({ organizationId: orgId, ...(input.id ? { id: input.id } : {}), slug: input.slug, sortOrder: input.sortOrder ?? 0, metadata: input.metadata ?? {}, ...(input.parentId !== undefined ? { parentId: input.parentId } : {}) }, ctx);
|
|
@@ -75,7 +75,7 @@ export class CategoryService {
|
|
|
75
75
|
if (!existing) return Err(new CommerceNotFoundError("Category not found."));
|
|
76
76
|
try { this.assertSameOrg(existing, actor); } catch (error) { return Err(toCommerceError(error)); }
|
|
77
77
|
if (input.slug) {
|
|
78
|
-
const existingBySlug = await this.repo.findCategoryBySlug(
|
|
78
|
+
const existingBySlug = await this.repo.findCategoryBySlug(resolveOrgIdForCommerce(actor, this.deps.config), input.slug, ctx);
|
|
79
79
|
if (existingBySlug && existingBySlug.id !== id) return Err(new CommerceConflictError(`Category with slug ${input.slug} already exists.`));
|
|
80
80
|
}
|
|
81
81
|
const updated = await this.repo.updateCategory(id, { ...(input.slug !== undefined ? { slug: input.slug } : {}), ...(input.sortOrder !== undefined ? { sortOrder: input.sortOrder } : {}), ...(input.metadata !== undefined ? { metadata: input.metadata } : {}), ...(input.parentId !== undefined ? { parentId: input.parentId } : {}) }, ctx);
|
|
@@ -98,7 +98,7 @@ export class CategoryService {
|
|
|
98
98
|
const entity = await this.deps.repository.findEntityById(entityId, ctx);
|
|
99
99
|
if (!entity) return Err(new CommerceNotFoundError("Entity not found."));
|
|
100
100
|
try { this.assertSameOrg(entity, actor); } catch (error) { return Err(toCommerceError(error)); }
|
|
101
|
-
const addCatOrgId =
|
|
101
|
+
const addCatOrgId = resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.deps.config);
|
|
102
102
|
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(categoryId);
|
|
103
103
|
let category = isUuid ? await this.repo.findCategoryById(categoryId, ctx) : null;
|
|
104
104
|
if (!category) category = await this.repo.findCategoryBySlug(addCatOrgId, categoryId, ctx);
|
|
@@ -116,7 +116,7 @@ export class CategoryService {
|
|
|
116
116
|
try { this.assertSameOrg(entity, actor); } catch (error) { return Err(toCommerceError(error)); }
|
|
117
117
|
const isCatUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(categoryId);
|
|
118
118
|
let category = isCatUuid ? await this.repo.findCategoryById(categoryId, ctx) : null;
|
|
119
|
-
if (!category) category = await this.repo.findCategoryBySlug(
|
|
119
|
+
if (!category) category = await this.repo.findCategoryBySlug(resolveOrgIdForCommerce(actor ?? ctx?.actor ?? null, this.deps.config), categoryId, ctx);
|
|
120
120
|
const removed = await this.repo.removeEntityFromCategory(entityId, category?.id ?? categoryId, ctx);
|
|
121
121
|
if (!removed) return Err(new CommerceNotFoundError("Category assignment not found."));
|
|
122
122
|
return Ok(undefined);
|