/** * POST /api/submit * * Body: SubmitPayload { cid, cs, values }. * * Verifies checksum, resolves org from cid (same as /api/data), then creates a * new Org Engagement Submission activity with all visible-field values written * to their custom-field bindings + stage_at_submission set to the org's * current Framework Stage. * * STUB MODE: if CiviCRM env vars are unset, returns success without writing * anywhere. Useful for UI dev. */ import { NextResponse } from "next/server"; import { civi, verifyChecksum } from "@/lib/civicrm"; import { allFields } from "@/config/form"; import type { SubmitPayload } from "@/types/form"; function isStubMode(): boolean { return !( process.env.CIVI_BASE_URL && process.env.CIVI_API_KEY && process.env.CIVI_SITE_KEY ); } const FIELD_BY_NAME = new Map(allFields.map((f) => [f.name, f])); export async function POST(req: Request) { let body: SubmitPayload; try { body = (await req.json()) as SubmitPayload; } catch { return NextResponse.json({ error: "Malformed JSON body." }, { status: 400 }); } const { cid, cs, values } = body; if (!cid || !cs) { return NextResponse.json({ error: "Missing cid or cs." }, { status: 400 }); } if (isStubMode()) { console.warn("[submit:STUB] would create Org Engagement Submission activity with values:", values); return NextResponse.json({ ok: true, stub: true }); } const ok = await verifyChecksum(cid, cs); if (!ok) { return NextResponse.json( { error: "Link is invalid or has expired." }, { status: 401 }, ); } // Resolve org via the relationship (mirror of /api/data). const relRes = await civi<{ contact_id_b: number }>("Relationship", "get", { select: ["contact_id_b"], where: [ ["contact_id_a", "=", Number(cid)], ["relationship_type_id:name", "=", "Primary Form Contact of"], ["is_active", "=", true], ], limit: 2, }); const orgs = relRes.values ?? []; if (orgs.length !== 1) { return NextResponse.json( { error: "Could not resolve a unique organization for your contact." }, { status: orgs.length === 0 ? 404 : 409 }, ); } const orgId = orgs[0].contact_id_b; // Read the org's current Framework Stage so we can stamp `stage_at_submission`. const orgRes = await civi<{ "Food_Co_op_Organizing.Stage": string | null; custom_1?: string | null; }>("Contact", "get", { select: ["custom_1", "Food_Co_op_Organizing.Stage"], where: [["id", "=", orgId]], }); const stageAtSubmission = orgRes.values?.[0]?.["Food_Co_op_Organizing.Stage"] ?? orgRes.values?.[0]?.custom_1 ?? null; // Build the activity record. Each form-side field name maps to its // configured `civiField` for the activity-side write. const activityRecord: Record = { "activity_type_id:name": "Org Engagement Submission", "status_id:name": "Completed", target_contact_id: orgId, source_contact_id: Number(cid), }; for (const [name, value] of Object.entries(values)) { const field = FIELD_BY_NAME.get(name); if (!field || !field.civiField) continue; if (field.type === "readonly") continue; // never write read-only fields activityRecord[field.civiField] = value; } // Stamp the stage-at-submission audit field. Convention: a field named // `Submission_Audit.stage_at_submission` on the activity. Adjust to your // actual machine name if different. activityRecord["Submission_Audit.stage_at_submission"] = stageAtSubmission; await civi("Activity", "create", { values: activityRecord }); return NextResponse.json({ ok: true }); }