105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
/**
|
|
* POST /api/submit
|
|
*
|
|
* Body: SubmitPayload { cid, cs, values }.
|
|
*
|
|
* Verifies checksum, resolves org from cid (mirror of /api/data), then
|
|
* creates a new `Check-in (organizing)` activity. The activity's own Stage
|
|
* field (`Check_in_data__organizing_.Stage`) is auto-populated from the
|
|
* org's current `Food_Co_op_Organizing.Stage` so each check-in carries a
|
|
* snapshot of where the org was at submission time.
|
|
*
|
|
* STUB MODE: if CiviCRM env vars are unset, returns success without writing.
|
|
*/
|
|
|
|
import { NextResponse } from "next/server";
|
|
import { civi, verifyChecksum } from "@/lib/civicrm";
|
|
import { allFields, ACTIVITY_TYPE_NAME, ACTIVITY_STAGE_FIELD, FORM_CONTACT_RELATIONSHIP } 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 Check-in (organizing) 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_a_b", "=", FORM_CONTACT_RELATIONSHIP],
|
|
["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 snapshot it on the activity.
|
|
const orgRes = await civi<{ "Food_Co_op_Organizing.Stage": string | null }>(
|
|
"Contact",
|
|
"get",
|
|
{
|
|
select: ["Food_Co_op_Organizing.Stage"],
|
|
where: [["id", "=", orgId]],
|
|
},
|
|
);
|
|
const stageAtSubmission = orgRes.values?.[0]?.["Food_Co_op_Organizing.Stage"] ?? null;
|
|
|
|
// Build the activity record. Each form-side field name maps to its
|
|
// configured `civiField` for the activity-side write.
|
|
const activityRecord: Record<string, unknown> = {
|
|
"activity_type_id:name": ACTIVITY_TYPE_NAME,
|
|
"status_id:name": "Completed",
|
|
target_contact_id: orgId,
|
|
source_contact_id: Number(cid),
|
|
subject: "Co-op Check-in (form submission)",
|
|
[ACTIVITY_STAGE_FIELD]: stageAtSubmission,
|
|
};
|
|
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
|
|
if (name === "current_stage") continue; // org-side, never written to activity
|
|
if (name === "stage_at_submission") continue; // handled above via ACTIVITY_STAGE_FIELD
|
|
activityRecord[field.civiField] = value;
|
|
}
|
|
|
|
await civi("Activity", "create", { values: activityRecord });
|
|
|
|
return NextResponse.json({ ok: true });
|
|
}
|