125 lines
4.0 KiB
TypeScript
125 lines
4.0 KiB
TypeScript
/**
|
|
* GET /api/data?cid=<cid>&cs=<cs>
|
|
*
|
|
* Verifies the checksum, resolves the org from the contact via the
|
|
* Primary Form Contact relationship, reads the org's Framework Stage,
|
|
* and walks past Org Engagement Submission activities for per-field
|
|
* most-recent prefill.
|
|
*
|
|
* Returns FormDataPayload (see types/form.ts).
|
|
*
|
|
* STUB MODE: if CiviCRM env vars are unset, returns mock data so the UI
|
|
* is exercisable without a live CRM. The mock data uses the actual stage
|
|
* values and a couple of populated example fields so the form looks alive
|
|
* during development.
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from "next/server";
|
|
import { civi, verifyChecksum } from "@/lib/civicrm";
|
|
import { loadPrefill } from "@/lib/prefill";
|
|
import { allFields } from "@/config/form";
|
|
import type { FormDataPayload } from "@/types/form";
|
|
|
|
const STUB_PAYLOAD: FormDataPayload = {
|
|
orgName: "Sample Co-op (stub)",
|
|
currentStage: "Organizing",
|
|
prefill: {
|
|
stage_0_peer_group_participation: "Peer cohort 4",
|
|
stage_0_members_current: 87,
|
|
stage_0_total_members_at_opening: null,
|
|
stage_0_projected_y1_sales: 2400000,
|
|
stage_0_projected_y2_sales: 2950000,
|
|
stage_0_total_cost_of_project: 4200000,
|
|
stage_1_vision: "A neighborhood-rooted co-op grocery prioritizing local farmers and equity in food access.",
|
|
stage_1_business_concept: "5,500 sq ft full-service co-op in a transit-adjacent storefront.",
|
|
},
|
|
};
|
|
|
|
function isStubMode(): boolean {
|
|
return !(
|
|
process.env.CIVI_BASE_URL &&
|
|
process.env.CIVI_API_KEY &&
|
|
process.env.CIVI_SITE_KEY
|
|
);
|
|
}
|
|
|
|
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 Form Contact relationship.
|
|
// Requires that the relationship exists and is active. We fetch contact_id_b
|
|
// because the relationship is Individual (A) → Organization (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", "=", "Primary Form Contact of"],
|
|
["is_active", "=", true],
|
|
],
|
|
limit: 2,
|
|
});
|
|
const orgs = relRes.values ?? [];
|
|
if (orgs.length === 0) {
|
|
return NextResponse.json(
|
|
{ error: "No active Primary Form Contact relationship found for your contact." },
|
|
{ status: 404 },
|
|
);
|
|
}
|
|
if (orgs.length > 1) {
|
|
return NextResponse.json(
|
|
{ error: "Your contact has multiple active Primary Form Contact relationships; staff must resolve before this link will work." },
|
|
{ status: 409 },
|
|
);
|
|
}
|
|
const orgId = orgs[0].contact_id_b;
|
|
|
|
// Fetch org name + Framework Stage value.
|
|
const orgRes = await civi<{
|
|
id: number;
|
|
display_name: string;
|
|
"Food_Co_op_Organizing.Stage": string | null;
|
|
custom_1?: string | null;
|
|
}>("Contact", "get", {
|
|
select: ["id", "display_name", "custom_1", "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"] ?? org.custom_1 ?? "";
|
|
|
|
// Per-field-most-recent prefill across past submission activities.
|
|
const { values: prefill } = await loadPrefill(orgId, allFields);
|
|
|
|
const payload: FormDataPayload = {
|
|
orgName: org.display_name,
|
|
currentStage: typeof currentStage === "string" ? currentStage : "",
|
|
prefill,
|
|
};
|
|
return NextResponse.json(payload);
|
|
}
|