diff --git a/README.md b/README.md index 32c8a11..230a731 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ https://check-in.fci.coop/?cid=&cs= The app: 1. Verifies the checksum against CiviCRM. -2. Resolves the org from the contact's **Primary Form Contact** relationship. +2. Resolves the org from the contact's **Primary Contact** relationship (Individual → Organization). Configurable via `FORM_CONTACT_RELATIONSHIP` in `config/form.ts`. 3. Reads the org's **Framework Stage** (`Food_Co_op_Organizing.Stage`) — text-keyed values like `Inquiry`, `Organizing`, `Feasibility`, `Business feasibility`, `Store Implementation`, `Stabilize newly opened co-op`. 4. Walks the org's past `Org Engagement Submission` activities DESC and assembles per-field-most-recent prefill values (the exact behaviour the WCM admin UI cannot express). 5. Renders the form with stage-conditional sections — Stage 0 (Inquiry / core check-in fields, ~32 of them) is always visible; Stages 1–5 each appear when `current_stage` is in their visibility set. diff --git a/app/api/data/route.ts b/app/api/data/route.ts index e8699e1..80cf00d 100644 --- a/app/api/data/route.ts +++ b/app/api/data/route.ts @@ -2,7 +2,7 @@ * GET /api/data?cid=&cs= * * Verifies the checksum, resolves the org from the contact via the - * Primary Form Contact relationship, reads the org's Framework Stage + * 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. * @@ -19,7 +19,7 @@ import { NextRequest, NextResponse } from "next/server"; import { civi, verifyChecksum } from "@/lib/civicrm"; import { loadPrefill } from "@/lib/prefill"; -import { allFields, optionGroupIds, ACTIVITY_TYPE_NAME } from "@/config/form"; +import { allFields, optionGroupIds, ACTIVITY_TYPE_NAME, FORM_CONTACT_RELATIONSHIP } from "@/config/form"; import type { FormDataPayload, SelectOption } from "@/types/form"; const STUB_PAYLOAD: FormDataPayload = { @@ -118,13 +118,13 @@ export async function GET(req: NextRequest) { ); } - // Resolve the org from the contact's Primary Form Contact relationship. + // 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", "=", "Primary Form Contact of"], + ["relationship_type_id:name_a_b", "=", FORM_CONTACT_RELATIONSHIP], ["is_active", "=", true], ], limit: 2, @@ -132,13 +132,13 @@ export async function GET(req: NextRequest) { const orgs = relRes.values ?? []; if (orgs.length === 0) { return NextResponse.json( - { error: "No active Primary Form Contact relationship found for your contact." }, + { 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 Primary Form Contact relationships; staff must resolve before this link will work." }, + { error: `Your contact has multiple active "${FORM_CONTACT_RELATIONSHIP}" relationships; staff must resolve before this link will work.` }, { status: 409 }, ); } diff --git a/app/api/health/route.ts b/app/api/health/route.ts index 76f32ab..2b6a9dd 100644 --- a/app/api/health/route.ts +++ b/app/api/health/route.ts @@ -20,7 +20,7 @@ import { NextResponse } from "next/server"; import { civi, verifyChecksum } from "@/lib/civicrm"; -import { ACTIVITY_TYPE_NAME } from "@/config/form"; +import { ACTIVITY_TYPE_NAME, FORM_CONTACT_RELATIONSHIP } from "@/config/form"; interface CheckResult { check: string; @@ -114,59 +114,40 @@ async function probeContactGet(): Promise { async function probeRelationshipType(): Promise { try { - const exact = await civi<{ id: number; name_a_b: string; name_b_a: string; label_a_b: string }>( - "RelationshipType", - "get", - { - select: ["id", "name_a_b", "name_b_a", "label_a_b"], - where: [["name_a_b", "=", "Primary Form Contact of"]], - }, - ); - if ((exact.values ?? []).length === 1) { - const r = exact.values[0]; + const exact = await civi<{ + id: number; + name_a_b: string; + name_b_a: string; + label_a_b: string; + contact_type_a: string; + contact_type_b: string; + }>("RelationshipType", "get", { + select: ["id", "name_a_b", "name_b_a", "label_a_b", "contact_type_a", "contact_type_b"], + where: [["name_a_b", "=", FORM_CONTACT_RELATIONSHIP]], + }); + const rows = exact.values ?? []; + if (rows.length === 1) { + const r = rows[0]; + const directionOk = r.contact_type_a === "Individual" && r.contact_type_b === "Organization"; return { - check: "relationship_type_primary_form_contact_of", - ok: true, - detail: `Found id=${r.id}, name_a_b="${r.name_a_b}", name_b_a="${r.name_b_a}".`, + check: `relationship_type_${FORM_CONTACT_RELATIONSHIP.replace(/\W+/g, "_").toLowerCase()}`, + ok: directionOk, + detail: directionOk + ? `Found id=${r.id}, name_a_b="${r.name_a_b}", direction Individual → Organization. ✓` + : `Found id=${r.id} but direction is ${r.contact_type_a} → ${r.contact_type_b} (expected Individual → Organization).`, }; } - - // Not found by exact name — list close matches so we know what's available. - const candidates = await civi<{ id: number; name_a_b: string; label_a_b: string; contact_type_a: string; contact_type_b: string }>( - "RelationshipType", - "get", - { - select: ["id", "name_a_b", "label_a_b", "contact_type_a", "contact_type_b"], - where: [ - [ - "OR", - [ - ["name_a_b", "LIKE", "%Form%"], - ["name_a_b", "LIKE", "%Primary%"], - ["name_a_b", "LIKE", "%Contact%"], - ["label_a_b", "LIKE", "%Form%"], - ["label_a_b", "LIKE", "%Primary%"], - ], - ], - ], - limit: 25, - }, - ); - const closeMatches = (candidates.values ?? []) - .map((r) => ` • id=${r.id} name_a_b="${r.name_a_b}" label="${r.label_a_b}" (${r.contact_type_a} ↔ ${r.contact_type_b})`) - .join("\n"); return { - check: "relationship_type_primary_form_contact_of", + check: `relationship_type_${FORM_CONTACT_RELATIONSHIP.replace(/\W+/g, "_").toLowerCase()}`, ok: false, detail: - "No relationship type with name_a_b='Primary Form Contact of' on this site.\n" + - "Existing relationship types matching Form/Primary/Contact:\n" + - (closeMatches || " (none)") + - "\nFix: create a new RelationshipType (Individual A ↔ Organization B) with name_a_b='Primary Form Contact of', or change the expected name in /api/data and /api/submit to match an existing type.", + rows.length === 0 + ? `No relationship type with name_a_b="${FORM_CONTACT_RELATIONSHIP}" found.` + : `Multiple relationship types named "${FORM_CONTACT_RELATIONSHIP}" (${rows.length}). Should be exactly one.`, }; } catch (e) { return { - check: "relationship_type_primary_form_contact_of", + check: `relationship_type_${FORM_CONTACT_RELATIONSHIP.replace(/\W+/g, "_").toLowerCase()}`, ok: false, detail: e instanceof Error ? e.message : String(e), }; diff --git a/app/api/submit/route.ts b/app/api/submit/route.ts index 7d101e0..8b5d347 100644 --- a/app/api/submit/route.ts +++ b/app/api/submit/route.ts @@ -14,7 +14,7 @@ import { NextResponse } from "next/server"; import { civi, verifyChecksum } from "@/lib/civicrm"; -import { allFields, ACTIVITY_TYPE_NAME, ACTIVITY_STAGE_FIELD } from "@/config/form"; +import { allFields, ACTIVITY_TYPE_NAME, ACTIVITY_STAGE_FIELD, FORM_CONTACT_RELATIONSHIP } from "@/config/form"; import type { SubmitPayload } from "@/types/form"; function isStubMode(): boolean { @@ -54,7 +54,7 @@ export async function POST(req: Request) { select: ["contact_id_b"], where: [ ["contact_id_a", "=", Number(cid)], - ["relationship_type_id:name", "=", "Primary Form Contact of"], + ["relationship_type_id:name_a_b", "=", FORM_CONTACT_RELATIONSHIP], ["is_active", "=", true], ], limit: 2, diff --git a/config/form.ts b/config/form.ts index 762e253..731e423 100644 --- a/config/form.ts +++ b/config/form.ts @@ -537,6 +537,17 @@ export const ACTIVITY_TYPE_NAME = "Check-in (organizing)"; /** Stage-snapshot field on the activity (so the form's stage_at_submission writes here). */ export const ACTIVITY_STAGE_FIELD = `${G0}.Stage`; +/** + * RelationshipType.name_a_b for the relationship that links the form-filler + * (Individual side) to the organization the form is about (Organization side). + * + * On `client.crm.fci.coop` we reuse the existing "Primary Contact" + * relationship type (id 24) rather than introducing a dedicated + * "Primary Form Contact" type. If you change this, also update the docs in + * the parent repo's spec. + */ +export const FORM_CONTACT_RELATIONSHIP = "Primary Contact"; + /** All distinct option_group_ids referenced by fields in this form. */ export const optionGroupIds = Array.from( new Set(