@open-mercato/core 0.10.1-develop.7785.1.ec0898da36 → 0.10.1-develop.7787.1.96c7cfc66f
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/modules/configs/api/cache/route.js +18 -45
- package/dist/modules/configs/api/cache/route.js.map +2 -2
- package/dist/modules/integrations/api/guards.js +37 -7
- package/dist/modules/integrations/api/guards.js.map +2 -2
- package/dist/modules/messages/api/guards.js +37 -7
- package/dist/modules/messages/api/guards.js.map +2 -2
- package/dist/modules/payment_gateways/api/guards.js +37 -7
- package/dist/modules/payment_gateways/api/guards.js.map +2 -2
- package/dist/modules/push_notifications/api/custom-send/route.js +18 -38
- package/dist/modules/push_notifications/api/custom-send/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/convert/route.js +17 -49
- package/dist/modules/sales/api/quotes/convert/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/send/route.js +17 -49
- package/dist/modules/sales/api/quotes/send/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/configs/api/cache/route.ts +19 -48
- package/src/modules/integrations/api/guards.ts +45 -7
- package/src/modules/messages/api/guards.ts +45 -7
- package/src/modules/payment_gateways/api/guards.ts +45 -7
- package/src/modules/push_notifications/api/custom-send/route.ts +18 -52
- package/src/modules/sales/api/quotes/convert/route.ts +17 -75
- package/src/modules/sales/api/quotes/send/route.ts +17 -75
|
@@ -7,10 +7,7 @@ import { resolveSingleOrganizationIdOrDeny } from "@open-mercato/core/modules/di
|
|
|
7
7
|
import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
|
|
8
8
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
9
9
|
import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
10
|
-
import {
|
|
11
|
-
bridgeLegacyGuard,
|
|
12
|
-
runMutationGuards
|
|
13
|
-
} from "@open-mercato/shared/lib/crud/mutation-guard-registry";
|
|
10
|
+
import { runRouteMutationGuards } from "@open-mercato/shared/lib/crud/route-mutation-guard";
|
|
14
11
|
import { withScopedPayload } from "../../utils.js";
|
|
15
12
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
16
13
|
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
@@ -23,29 +20,6 @@ const convertSchema = z.object({
|
|
|
23
20
|
const metadata = {
|
|
24
21
|
POST: { requireAuth: true, requireFeatures: ["sales.quotes.manage", "sales.orders.manage"] }
|
|
25
22
|
};
|
|
26
|
-
function resolveUserFeatures(auth) {
|
|
27
|
-
const features = auth?.features;
|
|
28
|
-
if (!Array.isArray(features)) return [];
|
|
29
|
-
return features.filter((value) => typeof value === "string");
|
|
30
|
-
}
|
|
31
|
-
async function runGuards(ctx, input) {
|
|
32
|
-
const legacyGuard = bridgeLegacyGuard(ctx.container);
|
|
33
|
-
if (!legacyGuard) {
|
|
34
|
-
return { ok: true, afterSuccessCallbacks: [] };
|
|
35
|
-
}
|
|
36
|
-
return runMutationGuards([legacyGuard], input, {
|
|
37
|
-
userFeatures: resolveUserFeatures(ctx.auth)
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
async function runGuardAfterSuccessCallbacks(callbacks, input) {
|
|
41
|
-
for (const callback of callbacks) {
|
|
42
|
-
if (!callback.guard.afterSuccess) continue;
|
|
43
|
-
await callback.guard.afterSuccess({
|
|
44
|
-
...input,
|
|
45
|
-
metadata: callback.metadata ?? null
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
23
|
async function resolveRequestContext(req) {
|
|
50
24
|
const container = await createRequestContainer();
|
|
51
25
|
const auth = await getAuthFromRequest(req);
|
|
@@ -77,18 +51,23 @@ async function POST(req) {
|
|
|
77
51
|
const payload = await req.json().catch(() => ({}));
|
|
78
52
|
const scoped = withScopedPayload(payload ?? {}, ctx, translate);
|
|
79
53
|
const input = convertSchema.parse(scoped);
|
|
80
|
-
const guardResult = await
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
54
|
+
const guardResult = await runRouteMutationGuards({
|
|
55
|
+
container: ctx.container,
|
|
56
|
+
req,
|
|
57
|
+
auth: {
|
|
58
|
+
tenantId: ctx.auth?.tenantId ?? "",
|
|
59
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
60
|
+
userId: ctx.auth?.sub ?? ""
|
|
61
|
+
},
|
|
62
|
+
input: {
|
|
63
|
+
resourceKind: "sales.quote",
|
|
64
|
+
resourceId: input.quoteId,
|
|
65
|
+
operation: "update",
|
|
66
|
+
mutationPayload: input
|
|
67
|
+
}
|
|
89
68
|
});
|
|
90
69
|
if (!guardResult.ok) {
|
|
91
|
-
return
|
|
70
|
+
return guardResult.response;
|
|
92
71
|
}
|
|
93
72
|
const commandBus = ctx.container.resolve("commandBus");
|
|
94
73
|
const { result, logEntry } = await commandBus.execute("sales.quotes.convert_to_order", { input, ctx });
|
|
@@ -108,18 +87,7 @@ async function POST(req) {
|
|
|
108
87
|
})
|
|
109
88
|
);
|
|
110
89
|
}
|
|
111
|
-
|
|
112
|
-
await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {
|
|
113
|
-
tenantId: ctx.auth?.tenantId ?? "",
|
|
114
|
-
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
115
|
-
userId: ctx.auth?.sub ?? "",
|
|
116
|
-
resourceKind: "sales.quote",
|
|
117
|
-
resourceId: input.quoteId,
|
|
118
|
-
operation: "update",
|
|
119
|
-
requestMethod: req.method,
|
|
120
|
-
requestHeaders: req.headers
|
|
121
|
-
});
|
|
122
|
-
}
|
|
90
|
+
await guardResult.runAfterSuccess();
|
|
123
91
|
return jsonResponse;
|
|
124
92
|
} catch (err) {
|
|
125
93
|
if (isCrudHttpError(err)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/sales/api/quotes/convert/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,yCAAyC;AAClD,SAAS,kCAAkC;AAE3C,SAAS,2BAA2B;AACpC,SAAS,eAAe,uBAAuB;AAE/C
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { withScopedPayload } from '../../utils'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\n\nconst logger = createLogger('sales')\n\nconst convertSchema = z.object({\n quoteId: z.string().uuid(),\n orderId: z.string().uuid().optional(),\n orderNumber: z.string().trim().max(191).optional(),\n})\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['sales.quotes.manage', 'sales.orders.manage'] },\n}\n\ntype RequestContext = {\n ctx: CommandRuntimeContext\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('sales.documents.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = resolveSingleOrganizationIdOrDeny(scope, auth) ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('sales.documents.errors.organization_required', 'Organization context is required'),\n })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return { ctx }\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx } = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(payload ?? {}, ctx, translate)\n const input = convertSchema.parse(scoped)\n const guardResult = await runRouteMutationGuards({\n container: ctx.container,\n req,\n auth: {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n },\n input: {\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n mutationPayload: input,\n },\n })\n if (!guardResult.ok) {\n return guardResult.response\n }\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<\n { quoteId: string; orderId?: string; orderNumber?: string },\n { orderId: string }\n >('sales.quotes.convert_to_order', { input, ctx })\n\n const orderId = result?.orderId ?? input.orderId ?? input.quoteId\n const jsonResponse = NextResponse.json({ orderId })\n\n if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {\n jsonResponse.headers.set(\n 'x-om-operation',\n serializeOperationMetadata({\n id: logEntry.id,\n undoToken: logEntry.undoToken,\n commandId: logEntry.commandId,\n actionLabel: logEntry.actionLabel ?? null,\n resourceKind: logEntry.resourceKind ?? 'sales.order',\n resourceId: logEntry.resourceId ?? orderId,\n executedAt: logEntry.createdAt instanceof Date\n ? logEntry.createdAt.toISOString()\n : typeof logEntry.createdAt === 'string'\n ? logEntry.createdAt\n : new Date().toISOString(),\n })\n )\n }\n\n await guardResult.runAfterSuccess()\n\n return jsonResponse\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const interceptorRejection = getCommandInterceptorHttpRejection(err)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('sales.quotes.convert failed', { err })\n return NextResponse.json(\n { error: translate('sales.documents.detail.convertError', 'Failed to convert quote.') },\n { status: 400 }\n )\n }\n}\n\nconst convertResponseSchema = z.object({\n orderId: z.string().uuid(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Sales',\n summary: 'Convert quote to order',\n methods: {\n POST: {\n summary: 'Convert quote',\n description: 'Creates a sales order from a quote and removes the original quote record.',\n requestBody: {\n contentType: 'application/json',\n schema: convertSchema,\n },\n responses: [\n { status: 200, description: 'Conversion succeeded', schema: convertResponseSchema },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n { status: 409, description: 'Conflict detected', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n { status: 423, description: 'Record locked', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -6,10 +6,7 @@ import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/d
|
|
|
6
6
|
import { resolveSingleOrganizationIdOrDeny } from "@open-mercato/core/modules/directory/utils/organizationScopeFilter";
|
|
7
7
|
import { resolveTranslations, detectLocale } from "@open-mercato/shared/lib/i18n/server";
|
|
8
8
|
import { CrudHttpError, isCrudHttpError, notFound } from "@open-mercato/shared/lib/crud/errors";
|
|
9
|
-
import {
|
|
10
|
-
bridgeLegacyGuard,
|
|
11
|
-
runMutationGuards
|
|
12
|
-
} from "@open-mercato/shared/lib/crud/mutation-guard-registry";
|
|
9
|
+
import { runRouteMutationGuards } from "@open-mercato/shared/lib/crud/route-mutation-guard";
|
|
13
10
|
import crypto from "node:crypto";
|
|
14
11
|
import { withScopedPayload } from "../../utils.js";
|
|
15
12
|
import { hashAuthToken } from "../../../../auth/lib/tokenHash.js";
|
|
@@ -25,29 +22,6 @@ const logger = createLogger("sales");
|
|
|
25
22
|
const metadata = {
|
|
26
23
|
POST: { requireAuth: true, requireFeatures: ["sales.quotes.manage"] }
|
|
27
24
|
};
|
|
28
|
-
function resolveUserFeatures(auth) {
|
|
29
|
-
const features = auth?.features;
|
|
30
|
-
if (!Array.isArray(features)) return [];
|
|
31
|
-
return features.filter((value) => typeof value === "string");
|
|
32
|
-
}
|
|
33
|
-
async function runGuards(ctx, input) {
|
|
34
|
-
const legacyGuard = bridgeLegacyGuard(ctx.container);
|
|
35
|
-
if (!legacyGuard) {
|
|
36
|
-
return { ok: true, afterSuccessCallbacks: [] };
|
|
37
|
-
}
|
|
38
|
-
return runMutationGuards([legacyGuard], input, {
|
|
39
|
-
userFeatures: resolveUserFeatures(ctx.auth)
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
async function runGuardAfterSuccessCallbacks(callbacks, input) {
|
|
43
|
-
for (const callback of callbacks) {
|
|
44
|
-
if (!callback.guard.afterSuccess) continue;
|
|
45
|
-
await callback.guard.afterSuccess({
|
|
46
|
-
...input,
|
|
47
|
-
metadata: callback.metadata ?? null
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
25
|
async function resolveRequestContext(req) {
|
|
52
26
|
const container = await createRequestContainer();
|
|
53
27
|
const auth = await getAuthFromRequest(req);
|
|
@@ -95,18 +69,23 @@ async function POST(req) {
|
|
|
95
69
|
const payload = await req.json().catch(() => ({}));
|
|
96
70
|
const scoped = withScopedPayload(payload ?? {}, ctx, translate);
|
|
97
71
|
const input = quoteSendSchema.parse(scoped);
|
|
98
|
-
const guardResult = await
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
72
|
+
const guardResult = await runRouteMutationGuards({
|
|
73
|
+
container: ctx.container,
|
|
74
|
+
req,
|
|
75
|
+
auth: {
|
|
76
|
+
tenantId: ctx.auth?.tenantId ?? "",
|
|
77
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
78
|
+
userId: ctx.auth?.sub ?? ""
|
|
79
|
+
},
|
|
80
|
+
input: {
|
|
81
|
+
resourceKind: "sales.quote",
|
|
82
|
+
resourceId: input.quoteId,
|
|
83
|
+
operation: "update",
|
|
84
|
+
mutationPayload: input
|
|
85
|
+
}
|
|
107
86
|
});
|
|
108
87
|
if (!guardResult.ok) {
|
|
109
|
-
return
|
|
88
|
+
return guardResult.response;
|
|
110
89
|
}
|
|
111
90
|
const em = ctx.container.resolve("em").fork();
|
|
112
91
|
const tenantScope = ctx.auth?.tenantId ? { tenantId: ctx.auth.tenantId, organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null } : void 0;
|
|
@@ -167,18 +146,7 @@ async function POST(req) {
|
|
|
167
146
|
tenantId: quote.tenantId,
|
|
168
147
|
organizationId: quote.organizationId
|
|
169
148
|
});
|
|
170
|
-
|
|
171
|
-
await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {
|
|
172
|
-
tenantId: ctx.auth?.tenantId ?? "",
|
|
173
|
-
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
174
|
-
userId: ctx.auth?.sub ?? "",
|
|
175
|
-
resourceKind: "sales.quote",
|
|
176
|
-
resourceId: input.quoteId,
|
|
177
|
-
operation: "update",
|
|
178
|
-
requestMethod: req.method,
|
|
179
|
-
requestHeaders: req.headers
|
|
180
|
-
});
|
|
181
|
-
}
|
|
149
|
+
await guardResult.runAfterSuccess();
|
|
182
150
|
return NextResponse.json({ ok: true });
|
|
183
151
|
} catch (err) {
|
|
184
152
|
if (isCrudHttpError(err)) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/sales/api/quotes/send/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations, detectLocale } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport crypto from 'node:crypto'\nimport { withScopedPayload } from '../../utils'\nimport { hashAuthToken } from '../../../../auth/lib/tokenHash'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { SalesQuote } from '../../../data/entities'\nimport { quoteSendSchema } from '../../../data/validators'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { resolveStatusEntryIdByValue } from '../../../lib/statusHelpers'\nimport { QuoteSentEmail } from '../../../emails/QuoteSentEmail'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['sales.quotes.manage'] },\n}\n\ntype RequestContext = {\n ctx: CommandRuntimeContext\n}\n\nfunction resolveUserFeatures(auth: unknown): string[] {\n const features = (auth as { features?: unknown })?.features\n if (!Array.isArray(features)) return []\n return features.filter((value): value is string => typeof value === 'string')\n}\n\nasync function runGuards(\n ctx: CommandRuntimeContext,\n input: MutationGuardInput,\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>\n}> {\n const legacyGuard = bridgeLegacyGuard(ctx.container)\n if (!legacyGuard) {\n return { ok: true, afterSuccessCallbacks: [] }\n }\n\n return runMutationGuards([legacyGuard], input, {\n userFeatures: resolveUserFeatures(ctx.auth),\n })\n}\n\nasync function runGuardAfterSuccessCallbacks(\n callbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>,\n input: {\n tenantId: string\n organizationId: string | null\n userId: string\n resourceKind: string\n resourceId: string\n operation: 'create' | 'update' | 'delete'\n requestMethod: string\n requestHeaders: Headers\n },\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n }\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('sales.documents.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = resolveSingleOrganizationIdOrDeny(scope, auth) ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('sales.documents.errors.organization_required', 'Organization context is required'),\n })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return { ctx }\n}\n\nfunction resolveQuoteEmail(quote: SalesQuote): string | null {\n const normalizeRecord = (value: unknown): Record<string, unknown> | null => {\n if (value && typeof value === 'object' && !Array.isArray(value)) return value as Record<string, unknown>\n if (typeof value !== 'string') return null\n const parsed = parseDecryptedFieldValue(value)\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? parsed as Record<string, unknown>\n : null\n }\n const snapshot = normalizeRecord(quote.customerSnapshot)\n const metadata = normalizeRecord(quote.metadata)\n const contact = snapshot?.contact as Record<string, unknown> | undefined\n const customer = snapshot?.customer as Record<string, unknown> | undefined\n const candidate =\n (typeof contact?.email === 'string' && contact.email.trim()) ||\n (typeof customer?.primaryEmail === 'string' && customer.primaryEmail.trim()) ||\n (typeof metadata?.customerEmail === 'string' && metadata.customerEmail.trim()) ||\n null\n if (!candidate) return null\n const parsed = z.string().email().safeParse(candidate)\n return parsed.success ? parsed.data : null\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx } = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(payload ?? {}, ctx, translate)\n const input = quoteSendSchema.parse(scoped)\n const guardResult = await runGuards(ctx, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n if (!guardResult.ok) {\n return NextResponse.json(guardResult.errorBody ?? { error: 'Operation blocked by guard' }, { status: guardResult.errorStatus ?? 422 })\n }\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tenantScope = ctx.auth?.tenantId\n ? { tenantId: ctx.auth.tenantId, organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null }\n : undefined\n const quote = await findOneWithDecryption(em, SalesQuote, { id: input.quoteId, deletedAt: null }, {}, tenantScope)\n if (!quote) {\n throw notFound(translate('sales.documents.detail.error', 'Document not found or inaccessible.'))\n }\n if (quote.tenantId !== ctx.auth?.tenantId || quote.organizationId !== ctx.selectedOrganizationId) {\n throw new CrudHttpError(403, { error: translate('sales.documents.errors.forbidden', 'Forbidden') })\n }\n\n if ((quote.status ?? null) === 'canceled') {\n throw new CrudHttpError(400, { error: translate('sales.quotes.send.canceled', 'Canceled quotes cannot be sent.') })\n }\n\n const email = resolveQuoteEmail(quote)\n if (!email) {\n throw new CrudHttpError(400, { error: translate('sales.quotes.send.missingEmail', 'Customer email is required to send a quote.') })\n }\n\n const now = new Date()\n const validUntil = new Date(now)\n validUntil.setUTCDate(validUntil.getUTCDate() + input.validForDays)\n\n const rawAcceptanceToken = crypto.randomUUID()\n\n // Persist the send state (status/token/sentAt) atomically and commit it\n // BEFORE the email goes out, so a customer never receives a link whose\n // acceptance token was not durably stored.\n await em.transactional(async (tx) => {\n quote.validUntil = validUntil\n quote.acceptanceToken = hashAuthToken(rawAcceptanceToken)\n quote.sentAt = now\n quote.status = 'sent'\n quote.statusEntryId = await resolveStatusEntryIdByValue(tx, {\n tenantId: quote.tenantId,\n organizationId: quote.organizationId,\n value: 'sent',\n })\n quote.updatedAt = now\n tx.persist(quote)\n })\n\n const appUrl = process.env.APP_URL || ''\n const url = appUrl ? `${appUrl.replace(/\\/$/, '')}/quote/${rawAcceptanceToken}` : `/quote/${rawAcceptanceToken}`\n\n const locale = await detectLocale()\n const validUntilFormatted = validUntil.toLocaleDateString(locale, {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n })\n\n const copy = {\n preview: translate('sales.quotes.email.preview', 'Quote {quoteNumber} is ready for review', { quoteNumber: quote.quoteNumber }),\n heading: translate('sales.quotes.email.heading', 'Quote {quoteNumber}', { quoteNumber: quote.quoteNumber }),\n total: translate('sales.quotes.email.total', 'Total: {amount} {currency}', {\n amount: quote.grandTotalGrossAmount ?? quote.grandTotalNetAmount ?? '0',\n currency: quote.currencyCode,\n }),\n validUntil: translate('sales.quotes.email.validUntil', 'Valid until: {date}', { date: validUntilFormatted }),\n cta: translate('sales.quotes.email.cta', 'View quote'),\n footer: translate('sales.quotes.email.footer', 'Open Mercato'),\n }\n\n // Side effect after commit: an email failure must not roll back the send state.\n await sendEmail({\n to: email,\n subject: translate('sales.quotes.email.subject', 'Quote {quoteNumber}', { quoteNumber: quote.quoteNumber }),\n react: QuoteSentEmail({ url, copy }),\n tenantId: quote.tenantId,\n organizationId: quote.organizationId,\n })\n\n if (guardResult.afterSuccessCallbacks.length) {\n await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n }\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('sales.quotes.send failed', { err })\n return NextResponse.json(\n { error: translate('sales.quotes.send.failed', 'Failed to send quote.') },\n { status: 400 }\n )\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Sales',\n summary: 'Send quote to customer',\n methods: {\n POST: {\n summary: 'Send quote',\n requestBody: {\n contentType: 'application/json',\n schema: quoteSendSchema,\n },\n responses: [\n { status: 200, description: 'Email queued', schema: z.object({ ok: z.literal(true) }) },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'Not found', schema: z.object({ error: z.string() }) },\n { status: 409, description: 'Conflict detected', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n { status: 423, description: 'Record locked', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,yCAAyC;AAElD,SAAS,qBAAqB,oBAAoB;AAClD,SAAS,eAAe,iBAAiB,gBAAgB;AAEzD
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveSingleOrganizationIdOrDeny } from '@open-mercato/core/modules/directory/utils/organizationScopeFilter'\nimport type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { resolveTranslations, detectLocale } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport crypto from 'node:crypto'\nimport { withScopedPayload } from '../../utils'\nimport { hashAuthToken } from '../../../../auth/lib/tokenHash'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'\nimport { SalesQuote } from '../../../data/entities'\nimport { quoteSendSchema } from '../../../data/validators'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { resolveStatusEntryIdByValue } from '../../../lib/statusHelpers'\nimport { QuoteSentEmail } from '../../../emails/QuoteSentEmail'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['sales.quotes.manage'] },\n}\n\ntype RequestContext = {\n ctx: CommandRuntimeContext\n}\n\nasync function resolveRequestContext(req: Request): Promise<RequestContext> {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, { error: translate('sales.documents.errors.unauthorized', 'Unauthorized') })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = resolveSingleOrganizationIdOrDeny(scope, auth) ?? null\n if (!organizationId) {\n throw new CrudHttpError(400, {\n error: translate('sales.documents.errors.organization_required', 'Organization context is required'),\n })\n }\n\n const ctx: CommandRuntimeContext = {\n container,\n auth,\n organizationScope: scope,\n selectedOrganizationId: organizationId,\n organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),\n request: req,\n }\n\n return { ctx }\n}\n\nfunction resolveQuoteEmail(quote: SalesQuote): string | null {\n const normalizeRecord = (value: unknown): Record<string, unknown> | null => {\n if (value && typeof value === 'object' && !Array.isArray(value)) return value as Record<string, unknown>\n if (typeof value !== 'string') return null\n const parsed = parseDecryptedFieldValue(value)\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? parsed as Record<string, unknown>\n : null\n }\n const snapshot = normalizeRecord(quote.customerSnapshot)\n const metadata = normalizeRecord(quote.metadata)\n const contact = snapshot?.contact as Record<string, unknown> | undefined\n const customer = snapshot?.customer as Record<string, unknown> | undefined\n const candidate =\n (typeof contact?.email === 'string' && contact.email.trim()) ||\n (typeof customer?.primaryEmail === 'string' && customer.primaryEmail.trim()) ||\n (typeof metadata?.customerEmail === 'string' && metadata.customerEmail.trim()) ||\n null\n if (!candidate) return null\n const parsed = z.string().email().safeParse(candidate)\n return parsed.success ? parsed.data : null\n}\n\nexport async function POST(req: Request) {\n try {\n const { ctx } = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await req.json().catch(() => ({}))\n const scoped = withScopedPayload(payload ?? {}, ctx, translate)\n const input = quoteSendSchema.parse(scoped)\n const guardResult = await runRouteMutationGuards({\n container: ctx.container,\n req,\n auth: {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n },\n input: {\n resourceKind: 'sales.quote',\n resourceId: input.quoteId,\n operation: 'update',\n mutationPayload: input,\n },\n })\n if (!guardResult.ok) {\n return guardResult.response\n }\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tenantScope = ctx.auth?.tenantId\n ? { tenantId: ctx.auth.tenantId, organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId ?? null }\n : undefined\n const quote = await findOneWithDecryption(em, SalesQuote, { id: input.quoteId, deletedAt: null }, {}, tenantScope)\n if (!quote) {\n throw notFound(translate('sales.documents.detail.error', 'Document not found or inaccessible.'))\n }\n if (quote.tenantId !== ctx.auth?.tenantId || quote.organizationId !== ctx.selectedOrganizationId) {\n throw new CrudHttpError(403, { error: translate('sales.documents.errors.forbidden', 'Forbidden') })\n }\n\n if ((quote.status ?? null) === 'canceled') {\n throw new CrudHttpError(400, { error: translate('sales.quotes.send.canceled', 'Canceled quotes cannot be sent.') })\n }\n\n const email = resolveQuoteEmail(quote)\n if (!email) {\n throw new CrudHttpError(400, { error: translate('sales.quotes.send.missingEmail', 'Customer email is required to send a quote.') })\n }\n\n const now = new Date()\n const validUntil = new Date(now)\n validUntil.setUTCDate(validUntil.getUTCDate() + input.validForDays)\n\n const rawAcceptanceToken = crypto.randomUUID()\n\n // Persist the send state (status/token/sentAt) atomically and commit it\n // BEFORE the email goes out, so a customer never receives a link whose\n // acceptance token was not durably stored.\n await em.transactional(async (tx) => {\n quote.validUntil = validUntil\n quote.acceptanceToken = hashAuthToken(rawAcceptanceToken)\n quote.sentAt = now\n quote.status = 'sent'\n quote.statusEntryId = await resolveStatusEntryIdByValue(tx, {\n tenantId: quote.tenantId,\n organizationId: quote.organizationId,\n value: 'sent',\n })\n quote.updatedAt = now\n tx.persist(quote)\n })\n\n const appUrl = process.env.APP_URL || ''\n const url = appUrl ? `${appUrl.replace(/\\/$/, '')}/quote/${rawAcceptanceToken}` : `/quote/${rawAcceptanceToken}`\n\n const locale = await detectLocale()\n const validUntilFormatted = validUntil.toLocaleDateString(locale, {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n })\n\n const copy = {\n preview: translate('sales.quotes.email.preview', 'Quote {quoteNumber} is ready for review', { quoteNumber: quote.quoteNumber }),\n heading: translate('sales.quotes.email.heading', 'Quote {quoteNumber}', { quoteNumber: quote.quoteNumber }),\n total: translate('sales.quotes.email.total', 'Total: {amount} {currency}', {\n amount: quote.grandTotalGrossAmount ?? quote.grandTotalNetAmount ?? '0',\n currency: quote.currencyCode,\n }),\n validUntil: translate('sales.quotes.email.validUntil', 'Valid until: {date}', { date: validUntilFormatted }),\n cta: translate('sales.quotes.email.cta', 'View quote'),\n footer: translate('sales.quotes.email.footer', 'Open Mercato'),\n }\n\n // Side effect after commit: an email failure must not roll back the send state.\n await sendEmail({\n to: email,\n subject: translate('sales.quotes.email.subject', 'Quote {quoteNumber}', { quoteNumber: quote.quoteNumber }),\n react: QuoteSentEmail({ url, copy }),\n tenantId: quote.tenantId,\n organizationId: quote.organizationId,\n })\n\n await guardResult.runAfterSuccess()\n\n return NextResponse.json({ ok: true })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const { translate } = await resolveTranslations()\n logger.error('sales.quotes.send failed', { err })\n return NextResponse.json(\n { error: translate('sales.quotes.send.failed', 'Failed to send quote.') },\n { status: 400 }\n )\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Sales',\n summary: 'Send quote to customer',\n methods: {\n POST: {\n summary: 'Send quote',\n requestBody: {\n contentType: 'application/json',\n schema: quoteSendSchema,\n },\n responses: [\n { status: 200, description: 'Email queued', schema: z.object({ ok: z.literal(true) }) },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'Not found', schema: z.object({ error: z.string() }) },\n { status: 409, description: 'Conflict detected', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n { status: 423, description: 'Record locked', schema: z.object({ error: z.string(), code: z.string().optional() }) },\n ],\n },\n },\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,8BAA8B;AACvC,SAAS,0BAA0B;AACnC,SAAS,0CAA0C;AACnD,SAAS,yCAAyC;AAElD,SAAS,qBAAqB,oBAAoB;AAClD,SAAS,eAAe,iBAAiB,gBAAgB;AAEzD,SAAS,8BAA8B;AAEvC,OAAO,YAAY;AACnB,SAAS,yBAAyB;AAClC,SAAS,qBAAqB;AAC9B,SAAS,6BAA6B;AACtC,SAAS,gCAAgC;AACzC,SAAS,kBAAkB;AAC3B,SAAS,uBAAuB;AAChC,SAAS,iBAAiB;AAC1B,SAAS,mCAAmC;AAC5C,SAAS,sBAAsB;AAC/B,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,OAAO;AAE5B,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,qBAAqB,EAAE;AACtE;AAMA,eAAe,sBAAsB,KAAuC;AAC1E,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAEhD,MAAI,CAAC,QAAQ,CAAC,KAAK,UAAU;AAC3B,UAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,uCAAuC,cAAc,EAAE,CAAC;AAAA,EAC1G;AAEA,QAAM,QAAQ,MAAM,mCAAmC,EAAE,WAAW,MAAM,SAAS,IAAI,CAAC;AACxF,QAAM,iBAAiB,kCAAkC,OAAO,IAAI,KAAK;AACzE,MAAI,CAAC,gBAAgB;AACnB,UAAM,IAAI,cAAc,KAAK;AAAA,MAC3B,OAAO,UAAU,gDAAgD,kCAAkC;AAAA,IACrG,CAAC;AAAA,EACH;AAEA,QAAM,MAA6B;AAAA,IACjC;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,IACnB,wBAAwB;AAAA,IACxB,iBAAiB,OAAO,cAAc,KAAK,QAAQ,CAAC,KAAK,KAAK,IAAI;AAAA,IAClE,SAAS;AAAA,EACX;AAEA,SAAO,EAAE,IAAI;AACf;AAEA,SAAS,kBAAkB,OAAkC;AAC3D,QAAM,kBAAkB,CAAC,UAAmD;AAC1E,QAAI,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO;AACxE,QAAI,OAAO,UAAU,SAAU,QAAO;AACtC,UAAMA,UAAS,yBAAyB,KAAK;AAC7C,WAAOA,WAAU,OAAOA,YAAW,YAAY,CAAC,MAAM,QAAQA,OAAM,IAChEA,UACA;AAAA,EACN;AACA,QAAM,WAAW,gBAAgB,MAAM,gBAAgB;AACvD,QAAMC,YAAW,gBAAgB,MAAM,QAAQ;AAC/C,QAAM,UAAU,UAAU;AAC1B,QAAM,WAAW,UAAU;AAC3B,QAAM,YACH,OAAO,SAAS,UAAU,YAAY,QAAQ,MAAM,KAAK,KACzD,OAAO,UAAU,iBAAiB,YAAY,SAAS,aAAa,KAAK,KACzE,OAAOA,WAAU,kBAAkB,YAAYA,UAAS,cAAc,KAAK,KAC5E;AACF,MAAI,CAAC,UAAW,QAAO;AACvB,QAAM,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,SAAS;AACrD,SAAO,OAAO,UAAU,OAAO,OAAO;AACxC;AAEA,eAAsB,KAAK,KAAc;AACvC,MAAI;AACF,UAAM,EAAE,IAAI,IAAI,MAAM,sBAAsB,GAAG;AAC/C,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,UAAU,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACjD,UAAM,SAAS,kBAAkB,WAAW,CAAC,GAAG,KAAK,SAAS;AAC9D,UAAM,QAAQ,gBAAgB,MAAM,MAAM;AAC1C,UAAM,cAAc,MAAM,uBAAuB;AAAA,MAC/C,WAAW,IAAI;AAAA,MACf;AAAA,MACA,MAAM;AAAA,QACJ,UAAU,IAAI,MAAM,YAAY;AAAA,QAChC,gBAAgB,IAAI,0BAA0B,IAAI,MAAM,SAAS;AAAA,QACjE,QAAQ,IAAI,MAAM,OAAO;AAAA,MAC3B;AAAA,MACA,OAAO;AAAA,QACL,cAAc;AAAA,QACd,YAAY,MAAM;AAAA,QAClB,WAAW;AAAA,QACX,iBAAiB;AAAA,MACnB;AAAA,IACF,CAAC;AACD,QAAI,CAAC,YAAY,IAAI;AACnB,aAAO,YAAY;AAAA,IACrB;AAEA,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,cAAc,IAAI,MAAM,WAC1B,EAAE,UAAU,IAAI,KAAK,UAAU,gBAAgB,IAAI,0BAA0B,IAAI,KAAK,SAAS,KAAK,IACpG;AACJ,UAAM,QAAQ,MAAM,sBAAsB,IAAI,YAAY,EAAE,IAAI,MAAM,SAAS,WAAW,KAAK,GAAG,CAAC,GAAG,WAAW;AACjH,QAAI,CAAC,OAAO;AACV,YAAM,SAAS,UAAU,gCAAgC,qCAAqC,CAAC;AAAA,IACjG;AACA,QAAI,MAAM,aAAa,IAAI,MAAM,YAAY,MAAM,mBAAmB,IAAI,wBAAwB;AAChG,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,oCAAoC,WAAW,EAAE,CAAC;AAAA,IACpG;AAEA,SAAK,MAAM,UAAU,UAAU,YAAY;AACzC,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,8BAA8B,iCAAiC,EAAE,CAAC;AAAA,IACpH;AAEA,UAAM,QAAQ,kBAAkB,KAAK;AACrC,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,UAAU,kCAAkC,6CAA6C,EAAE,CAAC;AAAA,IACpI;AAEA,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,aAAa,IAAI,KAAK,GAAG;AAC/B,eAAW,WAAW,WAAW,WAAW,IAAI,MAAM,YAAY;AAElE,UAAM,qBAAqB,OAAO,WAAW;AAK7C,UAAM,GAAG,cAAc,OAAO,OAAO;AACnC,YAAM,aAAa;AACnB,YAAM,kBAAkB,cAAc,kBAAkB;AACxD,YAAM,SAAS;AACf,YAAM,SAAS;AACf,YAAM,gBAAgB,MAAM,4BAA4B,IAAI;AAAA,QAC1D,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,OAAO;AAAA,MACT,CAAC;AACD,YAAM,YAAY;AAClB,SAAG,QAAQ,KAAK;AAAA,IAClB,CAAC;AAED,UAAM,SAAS,QAAQ,IAAI,WAAW;AACtC,UAAM,MAAM,SAAS,GAAG,OAAO,QAAQ,OAAO,EAAE,CAAC,UAAU,kBAAkB,KAAK,UAAU,kBAAkB;AAE9G,UAAM,SAAS,MAAM,aAAa;AAClC,UAAM,sBAAsB,WAAW,mBAAmB,QAAQ;AAAA,MAChE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,KAAK;AAAA,IACP,CAAC;AAED,UAAM,OAAO;AAAA,MACX,SAAS,UAAU,8BAA8B,2CAA2C,EAAE,aAAa,MAAM,YAAY,CAAC;AAAA,MAC9H,SAAS,UAAU,8BAA8B,uBAAuB,EAAE,aAAa,MAAM,YAAY,CAAC;AAAA,MAC1G,OAAO,UAAU,4BAA4B,8BAA8B;AAAA,QACzE,QAAQ,MAAM,yBAAyB,MAAM,uBAAuB;AAAA,QACpE,UAAU,MAAM;AAAA,MAClB,CAAC;AAAA,MACD,YAAY,UAAU,iCAAiC,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAAA,MAC3G,KAAK,UAAU,0BAA0B,YAAY;AAAA,MACrD,QAAQ,UAAU,6BAA6B,cAAc;AAAA,IAC/D;AAGA,UAAM,UAAU;AAAA,MACd,IAAI;AAAA,MACJ,SAAS,UAAU,8BAA8B,uBAAuB,EAAE,aAAa,MAAM,YAAY,CAAC;AAAA,MAC1G,OAAO,eAAe,EAAE,KAAK,KAAK,CAAC;AAAA,MACnC,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,IACxB,CAAC;AAED,UAAM,YAAY,gBAAgB;AAElC,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC,SAAS,KAAK;AACZ,QAAI,gBAAgB,GAAG,GAAG;AACxB,aAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC3D;AACA,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,WAAO,MAAM,4BAA4B,EAAE,IAAI,CAAC;AAChD,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,UAAU,4BAA4B,uBAAuB,EAAE;AAAA,MACxE,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,aAAa;AAAA,QACX,aAAa;AAAA,QACb,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,IAAI,EAAE,CAAC,EAAE;AAAA,QACtF,EAAE,QAAQ,KAAK,aAAa,mBAAmB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,QACvF,EAAE,QAAQ,KAAK,aAAa,gBAAgB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,QACpF,EAAE,QAAQ,KAAK,aAAa,aAAa,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,QACjF,EAAE,QAAQ,KAAK,aAAa,aAAa,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE;AAAA,QACjF,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE;AAAA,QACtH,EAAE,QAAQ,KAAK,aAAa,iBAAiB,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE;AAAA,MACpH;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": ["parsed", "metadata"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7787.1.96c7cfc66f",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -266,16 +266,16 @@
|
|
|
266
266
|
"zod": "^4.4.3"
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7787.1.96c7cfc66f",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7787.1.96c7cfc66f",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7787.1.96c7cfc66f",
|
|
272
272
|
"react": "^19.0.0",
|
|
273
273
|
"react-dom": "^19.0.0"
|
|
274
274
|
},
|
|
275
275
|
"devDependencies": {
|
|
276
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7787.1.96c7cfc66f",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7787.1.96c7cfc66f",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7787.1.96c7cfc66f",
|
|
279
279
|
"@testing-library/dom": "^10.4.1",
|
|
280
280
|
"@testing-library/jest-dom": "^7.0.1",
|
|
281
281
|
"@testing-library/react": "^16.3.3",
|
|
@@ -5,11 +5,7 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
5
5
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
6
6
|
import { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'
|
|
7
7
|
import { collectCrudCacheStats, purgeCrudCacheSegment } from '@open-mercato/shared/lib/crud/cache-stats'
|
|
8
|
-
import {
|
|
9
|
-
bridgeLegacyGuard,
|
|
10
|
-
runMutationGuards,
|
|
11
|
-
type MutationGuardInput,
|
|
12
|
-
} from '@open-mercato/shared/lib/crud/mutation-guard-registry'
|
|
8
|
+
import { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'
|
|
13
9
|
import {
|
|
14
10
|
configsTag,
|
|
15
11
|
cacheStatsResponseSchema,
|
|
@@ -27,11 +23,6 @@ export const metadata = {
|
|
|
27
23
|
POST: { requireAuth: true, requireFeatures: ['configs.cache.manage'] },
|
|
28
24
|
} as const
|
|
29
25
|
|
|
30
|
-
function resolveUserFeatures(auth: unknown): string[] {
|
|
31
|
-
const features = (auth as { features?: unknown } | null)?.features
|
|
32
|
-
return Array.isArray(features) ? features.filter((value): value is string => typeof value === 'string') : []
|
|
33
|
-
}
|
|
34
|
-
|
|
35
26
|
export async function GET(req: Request) {
|
|
36
27
|
const auth = await getAuthFromRequest(req)
|
|
37
28
|
if (!auth?.sub) {
|
|
@@ -107,30 +98,23 @@ export async function POST(req: Request) {
|
|
|
107
98
|
// → API Routes). A cache purge carries no per-record optimistic-lock version, so
|
|
108
99
|
// the default OSS guard short-circuits; wiring it keeps the route on the shared
|
|
109
100
|
// write-guard interception path for any tenant-registered guard.
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (!guardResult.ok) {
|
|
128
|
-
return NextResponse.json(
|
|
129
|
-
guardResult.errorBody ?? { error: translate('configs.cache.purgeError', 'Failed to purge cache segment.') },
|
|
130
|
-
{ status: guardResult.errorStatus ?? 422 },
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
afterSuccessCallbacks = guardResult.afterSuccessCallbacks
|
|
101
|
+
const guardResult = await runRouteMutationGuards({
|
|
102
|
+
container,
|
|
103
|
+
req,
|
|
104
|
+
auth: {
|
|
105
|
+
tenantId: tenantScope ?? '',
|
|
106
|
+
organizationId: auth.orgId ?? null,
|
|
107
|
+
userId: auth.sub,
|
|
108
|
+
},
|
|
109
|
+
input: {
|
|
110
|
+
resourceKind: 'configs.cache',
|
|
111
|
+
resourceId: segment ?? tenantScope ?? 'all',
|
|
112
|
+
operation: 'delete',
|
|
113
|
+
mutationPayload: { action, ...(segment ? { segment } : {}) },
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
if (!guardResult.ok) {
|
|
117
|
+
return guardResult.response
|
|
134
118
|
}
|
|
135
119
|
|
|
136
120
|
let responseBody: Record<string, unknown>
|
|
@@ -144,20 +128,7 @@ export async function POST(req: Request) {
|
|
|
144
128
|
responseBody = { action: 'purgeAll', stats }
|
|
145
129
|
}
|
|
146
130
|
|
|
147
|
-
|
|
148
|
-
if (!callback.guard.afterSuccess) continue
|
|
149
|
-
await callback.guard.afterSuccess({
|
|
150
|
-
tenantId: tenantScope ?? '',
|
|
151
|
-
organizationId: auth.orgId ?? null,
|
|
152
|
-
userId: auth.sub,
|
|
153
|
-
resourceKind: 'configs.cache',
|
|
154
|
-
resourceId: segment ?? tenantScope ?? 'all',
|
|
155
|
-
operation: 'delete',
|
|
156
|
-
requestMethod: req.method,
|
|
157
|
-
requestHeaders: req.headers,
|
|
158
|
-
metadata: callback.metadata ?? null,
|
|
159
|
-
})
|
|
160
|
-
}
|
|
131
|
+
await guardResult.runAfterSuccess()
|
|
161
132
|
|
|
162
133
|
return NextResponse.json(responseBody)
|
|
163
134
|
} catch (error) {
|
|
@@ -5,12 +5,18 @@ import {
|
|
|
5
5
|
type MutationGuard,
|
|
6
6
|
type MutationGuardInput,
|
|
7
7
|
} from '@open-mercato/shared/lib/crud/mutation-guard-registry'
|
|
8
|
+
import { getAllMutationGuardInstances } from '@open-mercato/shared/lib/crud/mutation-guard-store'
|
|
9
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
10
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
11
|
+
|
|
12
|
+
const logger = createLogger('integrations')
|
|
8
13
|
|
|
9
14
|
type GuardAfterCallback = {
|
|
10
15
|
guard: MutationGuard
|
|
11
16
|
metadata: Record<string, unknown> | null
|
|
12
17
|
}
|
|
13
18
|
|
|
19
|
+
/** @deprecated Mutation guard grants are resolved from RBAC by `runIntegrationMutationGuards`. */
|
|
14
20
|
export function resolveUserFeatures(auth: unknown): string[] {
|
|
15
21
|
const features = (auth as { features?: unknown })?.features
|
|
16
22
|
if (!Array.isArray(features)) return []
|
|
@@ -20,7 +26,7 @@ export function resolveUserFeatures(auth: unknown): string[] {
|
|
|
20
26
|
export async function runIntegrationMutationGuards(
|
|
21
27
|
container: AwilixContainer,
|
|
22
28
|
input: MutationGuardInput,
|
|
23
|
-
|
|
29
|
+
extraFeatures: string[] = [],
|
|
24
30
|
): Promise<{
|
|
25
31
|
ok: boolean
|
|
26
32
|
errorBody?: Record<string, unknown>
|
|
@@ -28,12 +34,35 @@ export async function runIntegrationMutationGuards(
|
|
|
28
34
|
modifiedPayload?: Record<string, unknown>
|
|
29
35
|
afterSuccessCallbacks: GuardAfterCallback[]
|
|
30
36
|
}> {
|
|
37
|
+
const allGuards: MutationGuard[] = [...getAllMutationGuardInstances()]
|
|
31
38
|
const legacyGuard = bridgeLegacyGuard(container)
|
|
32
|
-
if (
|
|
39
|
+
if (legacyGuard) allGuards.push(legacyGuard)
|
|
40
|
+
if (!allGuards.length) {
|
|
33
41
|
return { ok: true, afterSuccessCallbacks: [] }
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
let grantedFeatures: string[] = []
|
|
45
|
+
try {
|
|
46
|
+
const rbacService = container.resolve('rbacService') as {
|
|
47
|
+
getGrantedFeatures?: (
|
|
48
|
+
userId: string,
|
|
49
|
+
scope: { tenantId: string | null; organizationId: string | null },
|
|
50
|
+
) => Promise<string[]>
|
|
51
|
+
} | undefined
|
|
52
|
+
grantedFeatures = await rbacService?.getGrantedFeatures?.(input.userId, {
|
|
53
|
+
tenantId: input.tenantId,
|
|
54
|
+
organizationId: input.organizationId,
|
|
55
|
+
}) ?? []
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logger.error('Failed to resolve mutation guard feature grants', { err })
|
|
58
|
+
getTelemetryRuntime()?.reportError(err, {
|
|
59
|
+
module: 'integrations',
|
|
60
|
+
code: 'integrations.mutation_guard_feature_resolution_failed',
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const userFeatures = Array.from(new Set([...grantedFeatures, ...extraFeatures]))
|
|
65
|
+
return runMutationGuards(allGuards, input, { userFeatures })
|
|
37
66
|
}
|
|
38
67
|
|
|
39
68
|
export async function runIntegrationMutationGuardAfterSuccess(
|
|
@@ -51,9 +80,18 @@ export async function runIntegrationMutationGuardAfterSuccess(
|
|
|
51
80
|
): Promise<void> {
|
|
52
81
|
for (const callback of callbacks) {
|
|
53
82
|
if (!callback.guard.afterSuccess) continue
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
83
|
+
try {
|
|
84
|
+
await callback.guard.afterSuccess({
|
|
85
|
+
...input,
|
|
86
|
+
metadata: callback.metadata ?? null,
|
|
87
|
+
})
|
|
88
|
+
} catch (err) {
|
|
89
|
+
logger.error('Mutation guard afterSuccess failed', { guardId: callback.guard.id, err })
|
|
90
|
+
getTelemetryRuntime()?.reportError(err, {
|
|
91
|
+
module: 'integrations',
|
|
92
|
+
code: 'integrations.mutation_guard_after_success_failed',
|
|
93
|
+
attributes: { guardId: callback.guard.id },
|
|
94
|
+
})
|
|
95
|
+
}
|
|
58
96
|
}
|
|
59
97
|
}
|