diff --git a/app/api/0pt/er/batch/route.ts b/app/api/0pt/er/batch/route.ts new file mode 100644 index 0000000..38e0eaa --- /dev/null +++ b/app/api/0pt/er/batch/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { assertTenantMatchesVerified } from "@/src/http/tenant-guard"; +import { siaxErrorResponse } from "@/src/http/siax-error-response"; +import { SiaxError } from "@/src/platform/errors"; +import { getBatchEntityResolutionService } from "@/src/services/entity-resolution"; + +function required(body: Record, field: string): string { + const value = body[field]; + if (typeof value !== "string" || !value.trim()) { + throw new SiaxError("INVALID_REQUEST", "0pt.er", `${field} is required`); + } + return value.trim(); +} + +export async function POST(request: NextRequest) { + try { + const raw = await request.json(); + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + throw new SiaxError("INVALID_REQUEST", "0pt.er", "JSON object body is required"); + } + const body = raw as Record; + const tenantId = required(body, "tenant_id"); + const guard = assertTenantMatchesVerified(request, tenantId); + if (guard) return guard; + + const idempotencyKey = request.headers.get("idempotency-key")?.trim() + || (typeof body.idempotency_key === "string" ? body.idempotency_key.trim() : "") + || undefined; + const job = await getBatchEntityResolutionService().submitBatchResolution({ + tenant_id: tenantId, + project_id: required(body, "project_id"), + records_artifact_ref: required(body, "records_artifact_ref"), + actor_id: request.headers.get("x-0pt-verified-sub") ?? "local-dev", + ...(typeof body.correlation_id === "string" && body.correlation_id.trim() + ? { correlation_id: body.correlation_id.trim() } + : {}), + ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}), + }); + return NextResponse.json(job, { status: 202 }); + } catch (error: unknown) { + return siaxErrorResponse(error); + } +}