@open-mercato/core 0.10.1-develop.7775.1.2a1f553826 → 0.10.1-develop.7776.1.3032ea0741
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/sales/api/order-lines/batch/route.js +227 -0
- package/dist/modules/sales/api/order-lines/batch/route.js.map +7 -0
- package/dist/modules/sales/commands/documents.js +356 -129
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/data/validators.js +53 -0
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/sales/api/order-lines/batch/route.ts +312 -0
- package/src/modules/sales/commands/documents.ts +504 -179
- package/src/modules/sales/data/validators.ts +71 -0
- package/src/modules/sales/i18n/de.json +9 -0
- package/src/modules/sales/i18n/en.json +9 -0
- package/src/modules/sales/i18n/es.json +9 -0
- package/src/modules/sales/i18n/ko.json +9 -0
- package/src/modules/sales/i18n/pl.json +9 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
4
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
5
|
+
import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/directory/utils/organizationScope";
|
|
6
|
+
import { serializeOperationMetadata } from "@open-mercato/shared/lib/commands/operationMetadata";
|
|
7
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
8
|
+
import { CrudHttpError, isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
9
|
+
import {
|
|
10
|
+
bridgeLegacyGuard,
|
|
11
|
+
runMutationGuards
|
|
12
|
+
} from "@open-mercato/shared/lib/crud/mutation-guard-registry";
|
|
13
|
+
import { getAllMutationGuardInstances } from "@open-mercato/shared/lib/crud/mutation-guard-store";
|
|
14
|
+
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
15
|
+
import { getCommandInterceptorHttpRejection } from "@open-mercato/shared/lib/commands/errors";
|
|
16
|
+
import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
|
|
17
|
+
import { getTelemetryRuntime } from "@open-mercato/shared/lib/telemetry/runtime";
|
|
18
|
+
import { orderLineBulkUpsertSchema } from "../../../data/validators.js";
|
|
19
|
+
import { withScopedPayload } from "../../utils.js";
|
|
20
|
+
const logger = createLogger("sales");
|
|
21
|
+
function reportBatchRouteError(error, code, attributes) {
|
|
22
|
+
try {
|
|
23
|
+
getTelemetryRuntime()?.reportError(
|
|
24
|
+
error,
|
|
25
|
+
attributes ? { module: "sales", code, attributes } : { module: "sales", code }
|
|
26
|
+
);
|
|
27
|
+
} catch (reportingError) {
|
|
28
|
+
logger.warn("Failed to report batch order-line error", { code, reportingError });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function localizeIssues(issues, translate) {
|
|
32
|
+
return issues.map((issue) => {
|
|
33
|
+
const key = issue.params?.omKey;
|
|
34
|
+
if (typeof key !== "string") return issue;
|
|
35
|
+
return { ...issue, message: translate(key, issue.message) };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const COMMAND_ID = "sales.orders.lines.upsert_many";
|
|
39
|
+
const RESOURCE_KIND = "sales.order";
|
|
40
|
+
const metadata = {
|
|
41
|
+
POST: { requireAuth: true, requireFeatures: ["sales.orders.manage"] }
|
|
42
|
+
};
|
|
43
|
+
async function resolveGrantedFeatures(ctx) {
|
|
44
|
+
if (!ctx.auth?.sub) return [];
|
|
45
|
+
try {
|
|
46
|
+
const rbac = ctx.container.resolve("rbacService");
|
|
47
|
+
const features = await rbac?.getGrantedFeatures?.(ctx.auth.sub, {
|
|
48
|
+
tenantId: ctx.auth.tenantId,
|
|
49
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId
|
|
50
|
+
});
|
|
51
|
+
return features ?? [];
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function runGuards(ctx, input) {
|
|
57
|
+
const guards = [...getAllMutationGuardInstances()];
|
|
58
|
+
const legacyGuard = bridgeLegacyGuard(ctx.container);
|
|
59
|
+
if (legacyGuard) guards.push(legacyGuard);
|
|
60
|
+
if (!guards.length || !ctx.auth?.tenantId) {
|
|
61
|
+
return { ok: true, afterSuccessCallbacks: [] };
|
|
62
|
+
}
|
|
63
|
+
return runMutationGuards(guards, input, {
|
|
64
|
+
userFeatures: await resolveGrantedFeatures(ctx)
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async function runGuardAfterSuccessCallbacks(callbacks, input) {
|
|
68
|
+
for (const callback of callbacks) {
|
|
69
|
+
if (!callback.guard.afterSuccess) continue;
|
|
70
|
+
try {
|
|
71
|
+
await callback.guard.afterSuccess({
|
|
72
|
+
...input,
|
|
73
|
+
metadata: callback.metadata ?? null
|
|
74
|
+
});
|
|
75
|
+
} catch (err) {
|
|
76
|
+
logger.error("Mutation guard afterSuccess failed", { guardId: callback.guard.id, err });
|
|
77
|
+
reportBatchRouteError(err, "sales.order_lines_guard_after_success_failed", {
|
|
78
|
+
guardId: callback.guard.id
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function resolveRequestContext(req) {
|
|
84
|
+
const container = await createRequestContainer();
|
|
85
|
+
const auth = await getAuthFromRequest(req);
|
|
86
|
+
const { translate } = await resolveTranslations();
|
|
87
|
+
if (!auth || !auth.tenantId) {
|
|
88
|
+
throw new CrudHttpError(401, { error: translate("sales.documents.errors.unauthorized", "Unauthorized") });
|
|
89
|
+
}
|
|
90
|
+
const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req });
|
|
91
|
+
if (scope?.selectionRejected) {
|
|
92
|
+
throw new CrudHttpError(422, {
|
|
93
|
+
error: translate(
|
|
94
|
+
"sales.documents.errors.organization_selection_invalid",
|
|
95
|
+
"Your selected organization is no longer available. Please re-select an organization and try again."
|
|
96
|
+
),
|
|
97
|
+
code: "organization_selection_invalid"
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const organizationId = scope ? scope.selectedId : auth.orgId ?? null;
|
|
101
|
+
if (!organizationId) {
|
|
102
|
+
throw new CrudHttpError(400, {
|
|
103
|
+
error: translate("sales.documents.errors.organization_required", "Organization context is required")
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
container,
|
|
108
|
+
auth,
|
|
109
|
+
organizationScope: scope,
|
|
110
|
+
selectedOrganizationId: organizationId,
|
|
111
|
+
organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),
|
|
112
|
+
request: req
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function POST(req) {
|
|
116
|
+
try {
|
|
117
|
+
const ctx = await resolveRequestContext(req);
|
|
118
|
+
const { translate } = await resolveTranslations();
|
|
119
|
+
const payload = await readJsonSafe(req, {});
|
|
120
|
+
const scoped = withScopedPayload(payload ?? {}, ctx, translate);
|
|
121
|
+
let input = orderLineBulkUpsertSchema.parse(scoped);
|
|
122
|
+
const guardScope = {
|
|
123
|
+
tenantId: ctx.auth?.tenantId ?? "",
|
|
124
|
+
organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,
|
|
125
|
+
userId: ctx.auth?.sub ?? "",
|
|
126
|
+
resourceKind: RESOURCE_KIND,
|
|
127
|
+
resourceId: input.orderId,
|
|
128
|
+
operation: "update",
|
|
129
|
+
requestMethod: req.method,
|
|
130
|
+
requestHeaders: req.headers
|
|
131
|
+
};
|
|
132
|
+
const guardResult = await runGuards(ctx, {
|
|
133
|
+
...guardScope,
|
|
134
|
+
mutationPayload: input
|
|
135
|
+
});
|
|
136
|
+
if (!guardResult.ok) {
|
|
137
|
+
return NextResponse.json(
|
|
138
|
+
guardResult.errorBody ?? { error: translate("sales.documents.errors.guard_blocked", "Operation blocked by guard") },
|
|
139
|
+
{ status: guardResult.errorStatus ?? 422 }
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (guardResult.modifiedPayload) {
|
|
143
|
+
input = orderLineBulkUpsertSchema.parse({ ...input, ...guardResult.modifiedPayload });
|
|
144
|
+
}
|
|
145
|
+
const commandBus = ctx.container.resolve("commandBus");
|
|
146
|
+
const { result, logEntry } = await commandBus.execute(COMMAND_ID, { input: { body: input }, ctx });
|
|
147
|
+
const orderId = result?.orderId ?? input.orderId;
|
|
148
|
+
const jsonResponse = NextResponse.json({ orderId, lineIds: result?.lineIds ?? [] });
|
|
149
|
+
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
150
|
+
jsonResponse.headers.set(
|
|
151
|
+
"x-om-operation",
|
|
152
|
+
serializeOperationMetadata({
|
|
153
|
+
id: logEntry.id,
|
|
154
|
+
undoToken: logEntry.undoToken,
|
|
155
|
+
commandId: logEntry.commandId,
|
|
156
|
+
actionLabel: logEntry.actionLabel ?? null,
|
|
157
|
+
resourceKind: logEntry.resourceKind ?? RESOURCE_KIND,
|
|
158
|
+
resourceId: logEntry.resourceId ?? orderId,
|
|
159
|
+
executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : typeof logEntry.createdAt === "string" ? logEntry.createdAt : (/* @__PURE__ */ new Date()).toISOString()
|
|
160
|
+
})
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (guardResult.afterSuccessCallbacks.length) {
|
|
164
|
+
await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, guardScope);
|
|
165
|
+
}
|
|
166
|
+
return jsonResponse;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (isCrudHttpError(err)) {
|
|
169
|
+
return NextResponse.json(err.body, { status: err.status });
|
|
170
|
+
}
|
|
171
|
+
if (err instanceof z.ZodError) {
|
|
172
|
+
const { translate: translate2 } = await resolveTranslations();
|
|
173
|
+
return NextResponse.json(
|
|
174
|
+
{
|
|
175
|
+
error: translate2("sales.documents.items.bulk.errorInvalidInput", "Invalid input"),
|
|
176
|
+
details: localizeIssues(err.issues, translate2)
|
|
177
|
+
},
|
|
178
|
+
{ status: 400 }
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const interceptorRejection = getCommandInterceptorHttpRejection(err);
|
|
182
|
+
if (interceptorRejection) {
|
|
183
|
+
return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status });
|
|
184
|
+
}
|
|
185
|
+
const { translate } = await resolveTranslations();
|
|
186
|
+
logger.error("sales.orders.lines.upsert_many failed", { err });
|
|
187
|
+
reportBatchRouteError(err, "sales.order_lines_batch_failed");
|
|
188
|
+
return NextResponse.json(
|
|
189
|
+
{ error: translate("sales.documents.items.errorSaveBatch", "Failed to save order lines.") },
|
|
190
|
+
{ status: 400 }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const bulkUpsertResponseSchema = z.object({
|
|
195
|
+
orderId: z.string().uuid(),
|
|
196
|
+
lineIds: z.array(z.string().uuid())
|
|
197
|
+
});
|
|
198
|
+
const openApi = {
|
|
199
|
+
tag: "Sales",
|
|
200
|
+
summary: "Bulk upsert order lines",
|
|
201
|
+
methods: {
|
|
202
|
+
POST: {
|
|
203
|
+
summary: "Upsert and delete order lines in one pass",
|
|
204
|
+
description: "Writes a whole order line set in one aggregate load: entries carrying an id update that line, entries without one are appended, and deleteIds removes lines. The batch is all-or-nothing \u2014 any refused entry aborts it \u2014 and the order is renumbered 1..n, its totals recalculated and one undoable audit entry recorded.",
|
|
205
|
+
requestBody: {
|
|
206
|
+
contentType: "application/json",
|
|
207
|
+
schema: orderLineBulkUpsertSchema
|
|
208
|
+
},
|
|
209
|
+
responses: [
|
|
210
|
+
{ status: 200, description: "Batch applied", schema: bulkUpsertResponseSchema },
|
|
211
|
+
{ status: 400, description: "Invalid payload", schema: z.object({ error: z.string(), details: z.array(z.unknown()).optional() }) },
|
|
212
|
+
{ status: 401, description: "Unauthorized", schema: z.object({ error: z.string() }) },
|
|
213
|
+
{ status: 403, description: "Forbidden", schema: z.object({ error: z.string() }) },
|
|
214
|
+
{ status: 404, description: "Order or line not found", schema: z.object({ error: z.string() }) },
|
|
215
|
+
{ status: 409, description: "Conflict detected", schema: z.object({ error: z.string(), code: z.string().optional() }) },
|
|
216
|
+
{ status: 422, description: "Selected organization is no longer available", schema: z.object({ error: z.string(), code: z.literal("organization_selection_invalid") }) },
|
|
217
|
+
{ status: 423, description: "Record locked", schema: z.object({ error: z.string(), code: z.string().optional() }) }
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
export {
|
|
223
|
+
POST,
|
|
224
|
+
metadata,
|
|
225
|
+
openApi
|
|
226
|
+
};
|
|
227
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../src/modules/sales/api/order-lines/batch/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 { 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 {\n bridgeLegacyGuard,\n runMutationGuards,\n type MutationGuard,\n type MutationGuardAfterInput,\n type MutationGuardInput,\n} from '@open-mercato/shared/lib/crud/mutation-guard-registry'\nimport { getAllMutationGuardInstances } from '@open-mercato/shared/lib/crud/mutation-guard-store'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { orderLineBulkUpsertSchema } from '../../../data/validators'\nimport { withScopedPayload } from '../../utils'\n\nconst logger = createLogger('sales')\n\nfunction reportBatchRouteError(\n error: unknown,\n code: 'sales.order_lines_batch_failed' | 'sales.order_lines_guard_after_success_failed',\n attributes?: Record<string, string | number | boolean | undefined>,\n): void {\n try {\n getTelemetryRuntime()?.reportError(\n error,\n attributes ? { module: 'sales', code, attributes } : { module: 'sales', code },\n )\n } catch (reportingError) {\n logger.warn('Failed to report batch order-line error', { code, reportingError })\n }\n}\n\n/**\n * The bulk schema tags its own refusals with a translation key in `params`\n * because it runs outside a request and cannot translate. Resolve those here,\n * leaving zod's own built-in issues as they are.\n */\nfunction localizeIssues(\n issues: z.ZodError['issues'],\n translate: (key: string, fallback?: string) => string,\n): z.ZodError['issues'] {\n return issues.map((issue) => {\n const key = (issue as { params?: { omKey?: unknown } }).params?.omKey\n if (typeof key !== 'string') return issue\n return { ...issue, message: translate(key, issue.message) }\n })\n}\n\nconst COMMAND_ID = 'sales.orders.lines.upsert_many'\nconst RESOURCE_KIND = 'sales.order'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['sales.orders.manage'] },\n}\n\ntype BulkUpsertResult = { orderId: string; lineIds: string[] }\n\ntype RbacServiceLike = {\n getGrantedFeatures?: (\n userId: string,\n scope: { tenantId?: string | null; organizationId?: string | null },\n ) => Promise<string[] | undefined>\n}\n\n/**\n * The features a guard's `features` gate is checked against. Read from RBAC\n * rather than off the token's `features` claim: a guard that only runs for\n * holders of a feature must not be skippable by a caller who can shape their\n * own claim.\n */\nasync function resolveGrantedFeatures(ctx: CommandRuntimeContext): Promise<string[]> {\n if (!ctx.auth?.sub) return []\n try {\n const rbac = ctx.container.resolve('rbacService') as RbacServiceLike | undefined\n const features = await rbac?.getGrantedFeatures?.(ctx.auth.sub, {\n tenantId: ctx.auth.tenantId,\n organizationId: ctx.selectedOrganizationId ?? ctx.auth.orgId,\n })\n return features ?? []\n } catch {\n // rbacService not registered \u2014 guards without a feature gate still run.\n return []\n }\n}\n\n/**\n * Every guard the shared CRUD factory would run for this mutation: the ones\n * modules register through `registerMutationGuards`, plus the legacy DI guard\n * bridged on top. Consulting only the legacy bridge let a registered policy\n * that denies an ordinary order write be bypassed through this route.\n */\nasync function runGuards(\n ctx: CommandRuntimeContext,\n input: MutationGuardInput,\n): Promise<{\n ok: boolean\n errorBody?: Record<string, unknown>\n errorStatus?: number\n modifiedPayload?: Record<string, unknown>\n afterSuccessCallbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>\n}> {\n const guards = [...getAllMutationGuardInstances()]\n const legacyGuard = bridgeLegacyGuard(ctx.container)\n if (legacyGuard) guards.push(legacyGuard)\n if (!guards.length || !ctx.auth?.tenantId) {\n return { ok: true, afterSuccessCallbacks: [] }\n }\n\n return runMutationGuards(guards, input, {\n userFeatures: await resolveGrantedFeatures(ctx),\n })\n}\n\n/**\n * These run after the batch has committed and after the success response \u2014\n * including its undo metadata \u2014 has been built. A throwing callback must not\n * turn a committed write into a reported failure: the caller would retry, and\n * entries without an id would append a second time. Each one is isolated and\n * logged, exactly as the shared CRUD factory does.\n */\nasync function runGuardAfterSuccessCallbacks(\n callbacks: Array<{ guard: MutationGuard; metadata: Record<string, unknown> | null }>,\n input: Omit<MutationGuardAfterInput, 'metadata'>,\n): Promise<void> {\n for (const callback of callbacks) {\n if (!callback.guard.afterSuccess) continue\n try {\n await callback.guard.afterSuccess({\n ...input,\n metadata: callback.metadata ?? null,\n })\n } catch (err) {\n logger.error('Mutation guard afterSuccess failed', { guardId: callback.guard.id, err })\n reportBatchRouteError(err, 'sales.order_lines_guard_after_success_failed', {\n guardId: callback.guard.id,\n })\n }\n }\n}\n\nasync function resolveRequestContext(req: Request): Promise<CommandRuntimeContext> {\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 if (scope?.selectionRejected) {\n throw new CrudHttpError(422, {\n error: translate(\n 'sales.documents.errors.organization_selection_invalid',\n 'Your selected organization is no longer available. Please re-select an organization and try again.',\n ),\n code: 'organization_selection_invalid',\n })\n }\n const organizationId = scope ? scope.selectedId : auth.orgId ?? 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 return {\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\nexport async function POST(req: Request) {\n try {\n const ctx = await resolveRequestContext(req)\n const { translate } = await resolveTranslations()\n const payload = await readJsonSafe(req, {})\n const scoped = withScopedPayload(payload ?? {}, ctx, translate)\n let input = orderLineBulkUpsertSchema.parse(scoped)\n\n const guardScope = {\n tenantId: ctx.auth?.tenantId ?? '',\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null,\n userId: ctx.auth?.sub ?? '',\n resourceKind: RESOURCE_KIND,\n resourceId: input.orderId,\n operation: 'update' as const,\n requestMethod: req.method,\n requestHeaders: req.headers,\n } satisfies Omit<MutationGuardAfterInput, 'metadata'>\n const guardResult = await runGuards(ctx, {\n ...guardScope,\n mutationPayload: input as Record<string, unknown>,\n })\n if (!guardResult.ok) {\n return NextResponse.json(\n guardResult.errorBody ?? { error: translate('sales.documents.errors.guard_blocked', 'Operation blocked by guard') },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n if (guardResult.modifiedPayload) {\n input = orderLineBulkUpsertSchema.parse({ ...input, ...guardResult.modifiedPayload })\n }\n\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<\n { body: z.infer<typeof orderLineBulkUpsertSchema> },\n BulkUpsertResult\n >(COMMAND_ID, { input: { body: input }, ctx })\n\n const orderId = result?.orderId ?? input.orderId\n const jsonResponse = NextResponse.json({ orderId, lineIds: result?.lineIds ?? [] })\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 ?? RESOURCE_KIND,\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 if (guardResult.afterSuccessCallbacks.length) {\n await runGuardAfterSuccessCallbacks(guardResult.afterSuccessCallbacks, guardScope)\n }\n\n return jsonResponse\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n const { translate } = await resolveTranslations()\n return NextResponse.json(\n {\n error: translate('sales.documents.items.bulk.errorInvalidInput', 'Invalid input'),\n details: localizeIssues(err.issues, translate),\n },\n { status: 400 },\n )\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.orders.lines.upsert_many failed', { err })\n reportBatchRouteError(err, 'sales.order_lines_batch_failed')\n return NextResponse.json(\n { error: translate('sales.documents.items.errorSaveBatch', 'Failed to save order lines.') },\n { status: 400 }\n )\n }\n}\n\nconst bulkUpsertResponseSchema = z.object({\n orderId: z.string().uuid(),\n lineIds: z.array(z.string().uuid()),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Sales',\n summary: 'Bulk upsert order lines',\n methods: {\n POST: {\n summary: 'Upsert and delete order lines in one pass',\n description:\n 'Writes a whole order line set in one aggregate load: entries carrying an id update that line, '\n + 'entries without one are appended, and deleteIds removes lines. The batch is all-or-nothing \u2014 '\n + 'any refused entry aborts it \u2014 and the order is renumbered 1..n, its totals recalculated and '\n + 'one undoable audit entry recorded.',\n requestBody: {\n contentType: 'application/json',\n schema: orderLineBulkUpsertSchema,\n },\n responses: [\n { status: 200, description: 'Batch applied', schema: bulkUpsertResponseSchema },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string(), details: z.array(z.unknown()).optional() }) },\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: 'Order or line 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: 422, description: 'Selected organization is no longer available', schema: z.object({ error: z.string(), code: z.literal('organization_selection_invalid') }) },\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
|
+
"names": ["translate"]
|
|
7
|
+
}
|