/** * GET /api/data?cid=&cs= * * Verifies the checksum, resolves the org from the contact via the * Primary Contact relationship, reads the org's Framework Stage * (`Food_Co_op_Organizing.Stage` on the Organization contact), and walks * past `Check-in (organizing)` activities for per-field most-recent prefill. * * Also fetches the OptionValue rows for any option_group_ids referenced by * the form (so radio/select/multiselect fields render with real CRM-defined * options) and returns them in `payload.options`. * * Returns FormDataPayload (see types/form.ts). * * STUB MODE: if CiviCRM env vars are unset, returns mock data + a small set * of mock options so the UI is exercisable without a live CRM. */ import { NextRequest, NextResponse } from "next/server"; import { civi, verifyChecksum } from "@/lib/civicrm"; import { loadPrefill } from "@/lib/prefill"; import { allFields, optionGroupIds, ACTIVITY_TYPE_NAME, FORM_CONTACT_RELATIONSHIP } from "@/config/form"; import type { FormDataPayload, SelectOption } from "@/types/form"; const STUB_PAYLOAD: FormDataPayload = { orgName: "Sample Co-op (stub)", currentStage: "Organizing", prefill: { Peer_Group_Participation: "Yes", Members__current_: 87, Total_members_at_opening: null, Projected_Year_1_Sales: 2400000, Projected_Year_2_Sales: 2950000, Total_cost_of_project: 4200000, Vision: "2024-09-15", Business_Concept: "2024-12-02", }, options: { 140: [{ value: "Yes", label: "Yes" }, { value: "No", label: "No" }, { value: "Considering", label: "Considering" }], 132: [{ value: "Strong", label: "Strong" }, { value: "Moderate", label: "Moderate" }, { value: "Needs Work", label: "Needs work" }], 141: [{ value: "Yes", label: "Yes" }, { value: "No", label: "No" }], 142: [{ value: "Yes", label: "Yes" }, { value: "No", label: "No" }], 133: [{ value: "Viable", label: "Viable" }, { value: "Marginal", label: "Marginal" }, { value: "Not viable", label: "Not viable" }], 134: [{ value: "Member equity", label: "Member equity" }, { value: "Member loans", label: "Member loans" }, { value: "Bank debt", label: "Bank debt" }, { value: "Grants", label: "Grants" }], 139: [{ value: "Yes", label: "Yes" }, { value: "No", label: "No" }], 135: [{ value: "Co-op", label: "Co-op grocery" }, { value: "Conventional", label: "Conventional grocery" }, { value: "Other", label: "Other" }], 136: [{ value: "Member", label: "Member" }, { value: "Considering", label: "Considering" }, { value: "Not a member", label: "Not a member" }], 137: [{ value: "Member", label: "Member" }, { value: "Considering", label: "Considering" }, { value: "Not a member", label: "Not a member" }], 138: [{ value: "UNFI", label: "UNFI" }, { value: "KeHE", label: "KeHE" }, { value: "Other", label: "Other" }, { value: "None", label: "None" }], 143: [{ value: "Northeast", label: "Northeast" }, { value: "Mid-Atlantic", label: "Mid-Atlantic" }, { value: "Midwest", label: "Midwest" }, { value: "South", label: "South" }, { value: "West", label: "West" }], 75: [ { value: "Inquiry", label: "Inquiry" }, { value: "Organizing", label: "Stage 1 — Convene & Prepare" }, { value: "Feasibility", label: "Stage 2 — Grow & Plan" }, { value: "Business feasibility", label: "Stage 3 — Connect & Gather" }, { value: "Store Implementation", label: "Stage 4 — Excite & Build" }, { value: "Stabilize newly opened co-op", label: "Stage 5 — Fulfill & Stabilize" }, ], }, }; function isStubMode(): boolean { return !( process.env.CIVI_BASE_URL && process.env.CIVI_API_KEY && process.env.CIVI_SITE_KEY ); } interface OptionValueRow { value: string; label: string; option_group_id: number; is_active: boolean; } async function fetchOptionGroups(): Promise> { if (optionGroupIds.length === 0) return {}; const res = await civi("OptionValue", "get", { select: ["value", "label", "option_group_id", "is_active"], where: [ ["option_group_id", "IN", optionGroupIds], ["is_active", "=", true], ], orderBy: { weight: "ASC" }, limit: 500, }); const out: Record = {}; for (const row of res.values ?? []) { if (!out[row.option_group_id]) out[row.option_group_id] = []; out[row.option_group_id].push({ value: row.value, label: row.label }); } return out; } export async function GET(req: NextRequest) { const url = new URL(req.url); const cid = url.searchParams.get("cid"); const cs = url.searchParams.get("cs"); if (!cid || !cs) { return NextResponse.json( { error: "Missing cid or cs parameter." }, { status: 400 }, ); } if (isStubMode()) { return NextResponse.json(STUB_PAYLOAD); } // Verify checksum first. const ok = await verifyChecksum(cid, cs); if (!ok) { return NextResponse.json( { error: "Link is invalid or has expired. Please request a fresh one." }, { status: 401 }, ); } // Resolve the org from the contact's "Primary Contact" relationship. // The relationship is Individual (A) → Organization (B); fetch contact_id_b. 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 === 0) { return NextResponse.json( { error: `No active "${FORM_CONTACT_RELATIONSHIP}" relationship found for your contact.` }, { status: 404 }, ); } if (orgs.length > 1) { return NextResponse.json( { error: `Your contact has multiple active "${FORM_CONTACT_RELATIONSHIP}" relationships; staff must resolve before this link will work.` }, { status: 409 }, ); } const orgId = orgs[0].contact_id_b; // Fetch org name + Framework Stage (org-side, on the organization contact). const orgRes = await civi<{ id: number; display_name: string; "Food_Co_op_Organizing.Stage": string | null; }>("Contact", "get", { select: ["id", "display_name", "Food_Co_op_Organizing.Stage"], where: [["id", "=", orgId]], }); const org = orgRes.values?.[0]; if (!org) { return NextResponse.json({ error: "Organization not found." }, { status: 404 }); } const currentStage = org["Food_Co_op_Organizing.Stage"] ?? ""; // Per-field-most-recent prefill across past Check-in activities, plus // option-group fetch — kicked off in parallel. const [{ values: prefill }, options] = await Promise.all([ loadPrefill(orgId, allFields, ACTIVITY_TYPE_NAME), fetchOptionGroups(), ]); const payload: FormDataPayload = { orgName: org.display_name, currentStage: typeof currentStage === "string" ? currentStage : "", prefill, options, }; return NextResponse.json(payload); }