Use existing 'Primary Contact' relationship type via FORM_CONTACT_RELATIONSHIP constant
This commit is contained in:
@@ -18,7 +18,7 @@ https://check-in.fci.coop/?cid=<contactId>&cs=<checksum>
|
|||||||
The app:
|
The app:
|
||||||
|
|
||||||
1. Verifies the checksum against CiviCRM.
|
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`.
|
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).
|
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.
|
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.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* GET /api/data?cid=<cid>&cs=<cs>
|
* GET /api/data?cid=<cid>&cs=<cs>
|
||||||
*
|
*
|
||||||
* Verifies the checksum, resolves the org from the contact via the
|
* 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
|
* (`Food_Co_op_Organizing.Stage` on the Organization contact), and walks
|
||||||
* past `Check-in (organizing)` activities for per-field most-recent prefill.
|
* past `Check-in (organizing)` activities for per-field most-recent prefill.
|
||||||
*
|
*
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { civi, verifyChecksum } from "@/lib/civicrm";
|
import { civi, verifyChecksum } from "@/lib/civicrm";
|
||||||
import { loadPrefill } from "@/lib/prefill";
|
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";
|
import type { FormDataPayload, SelectOption } from "@/types/form";
|
||||||
|
|
||||||
const STUB_PAYLOAD: FormDataPayload = {
|
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.
|
// The relationship is Individual (A) → Organization (B); fetch contact_id_b.
|
||||||
const relRes = await civi<{ contact_id_b: number }>("Relationship", "get", {
|
const relRes = await civi<{ contact_id_b: number }>("Relationship", "get", {
|
||||||
select: ["contact_id_b"],
|
select: ["contact_id_b"],
|
||||||
where: [
|
where: [
|
||||||
["contact_id_a", "=", Number(cid)],
|
["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],
|
["is_active", "=", true],
|
||||||
],
|
],
|
||||||
limit: 2,
|
limit: 2,
|
||||||
@@ -132,13 +132,13 @@ export async function GET(req: NextRequest) {
|
|||||||
const orgs = relRes.values ?? [];
|
const orgs = relRes.values ?? [];
|
||||||
if (orgs.length === 0) {
|
if (orgs.length === 0) {
|
||||||
return NextResponse.json(
|
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 },
|
{ status: 404 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (orgs.length > 1) {
|
if (orgs.length > 1) {
|
||||||
return NextResponse.json(
|
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 },
|
{ status: 409 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { civi, verifyChecksum } from "@/lib/civicrm";
|
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 {
|
interface CheckResult {
|
||||||
check: string;
|
check: string;
|
||||||
@@ -114,59 +114,40 @@ async function probeContactGet(): Promise<CheckResult> {
|
|||||||
|
|
||||||
async function probeRelationshipType(): Promise<CheckResult> {
|
async function probeRelationshipType(): Promise<CheckResult> {
|
||||||
try {
|
try {
|
||||||
const exact = await civi<{ id: number; name_a_b: string; name_b_a: string; label_a_b: string }>(
|
const exact = await civi<{
|
||||||
"RelationshipType",
|
id: number;
|
||||||
"get",
|
name_a_b: string;
|
||||||
{
|
name_b_a: string;
|
||||||
select: ["id", "name_a_b", "name_b_a", "label_a_b"],
|
label_a_b: string;
|
||||||
where: [["name_a_b", "=", "Primary Form Contact of"]],
|
contact_type_a: string;
|
||||||
},
|
contact_type_b: string;
|
||||||
);
|
}>("RelationshipType", "get", {
|
||||||
if ((exact.values ?? []).length === 1) {
|
select: ["id", "name_a_b", "name_b_a", "label_a_b", "contact_type_a", "contact_type_b"],
|
||||||
const r = exact.values[0];
|
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 {
|
return {
|
||||||
check: "relationship_type_primary_form_contact_of",
|
check: `relationship_type_${FORM_CONTACT_RELATIONSHIP.replace(/\W+/g, "_").toLowerCase()}`,
|
||||||
ok: true,
|
ok: directionOk,
|
||||||
detail: `Found id=${r.id}, name_a_b="${r.name_a_b}", name_b_a="${r.name_b_a}".`,
|
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 {
|
return {
|
||||||
check: "relationship_type_primary_form_contact_of",
|
check: `relationship_type_${FORM_CONTACT_RELATIONSHIP.replace(/\W+/g, "_").toLowerCase()}`,
|
||||||
ok: false,
|
ok: false,
|
||||||
detail:
|
detail:
|
||||||
"No relationship type with name_a_b='Primary Form Contact of' on this site.\n" +
|
rows.length === 0
|
||||||
"Existing relationship types matching Form/Primary/Contact:\n" +
|
? `No relationship type with name_a_b="${FORM_CONTACT_RELATIONSHIP}" found.`
|
||||||
(closeMatches || " (none)") +
|
: `Multiple relationship types named "${FORM_CONTACT_RELATIONSHIP}" (${rows.length}). Should be exactly one.`,
|
||||||
"\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.",
|
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
check: "relationship_type_primary_form_contact_of",
|
check: `relationship_type_${FORM_CONTACT_RELATIONSHIP.replace(/\W+/g, "_").toLowerCase()}`,
|
||||||
ok: false,
|
ok: false,
|
||||||
detail: e instanceof Error ? e.message : String(e),
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { civi, verifyChecksum } from "@/lib/civicrm";
|
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";
|
import type { SubmitPayload } from "@/types/form";
|
||||||
|
|
||||||
function isStubMode(): boolean {
|
function isStubMode(): boolean {
|
||||||
@@ -54,7 +54,7 @@ export async function POST(req: Request) {
|
|||||||
select: ["contact_id_b"],
|
select: ["contact_id_b"],
|
||||||
where: [
|
where: [
|
||||||
["contact_id_a", "=", Number(cid)],
|
["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],
|
["is_active", "=", true],
|
||||||
],
|
],
|
||||||
limit: 2,
|
limit: 2,
|
||||||
|
|||||||
@@ -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). */
|
/** Stage-snapshot field on the activity (so the form's stage_at_submission writes here). */
|
||||||
export const ACTIVITY_STAGE_FIELD = `${G0}.Stage`;
|
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. */
|
/** All distinct option_group_ids referenced by fields in this form. */
|
||||||
export const optionGroupIds = Array.from(
|
export const optionGroupIds = Array.from(
|
||||||
new Set(
|
new Set(
|
||||||
|
|||||||
Reference in New Issue
Block a user