diff --git a/app/api/health/route.ts b/app/api/health/route.ts index 6f91b42..76f32ab 100644 --- a/app/api/health/route.ts +++ b/app/api/health/route.ts @@ -114,24 +114,55 @@ async function probeContactGet(): Promise { async function probeRelationshipType(): Promise { try { - const res = await civi<{ id: number; name_a_b: string; name_b_a: string }>( + 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"], + select: ["id", "name_a_b", "name_b_a", "label_a_b"], where: [["name_a_b", "=", "Primary Form Contact of"]], }, ); - const rows = res.values ?? []; + if ((exact.values ?? []).length === 1) { + const r = exact.values[0]; + 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}".`, + }; + } + + // 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", - ok: rows.length === 1, + ok: false, detail: - rows.length === 1 - ? `Found id=${rows[0].id}, name_a_b="${rows[0].name_a_b}", name_b_a="${rows[0].name_b_a}".` - : rows.length === 0 - ? `Not found. Create a relationship type named "Primary Form Contact of" (Individual ↔ Organization).` - : `Multiple matches (${rows.length}). Should be exactly one.`, + "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.", }; } catch (e) { return { @@ -174,17 +205,33 @@ async function probeActivityType(): Promise { } async function probeStageOptionGroup(): Promise { + // Resolve the option group via the Framework Stage field's metadata + // (custom field id=1 on Organization, in Food_Co_op_Organizing group). + // This is more robust than guessing the option group's machine name. try { - const res = await civi<{ value: string; label: string }>("OptionValue", "get", { + const fieldRes = await civi<{ option_group_id: number; name: string }>("CustomField", "get", { + select: ["option_group_id", "name"], + where: [["id", "=", 1]], + }); + const fieldRow = fieldRes.values?.[0]; + if (!fieldRow?.option_group_id) { + return { + check: "stage_option_group_values", + ok: false, + detail: `CustomField id=1 (Framework Stage) not found, or has no option_group_id.`, + }; + } + const ogId = fieldRow.option_group_id; + const valuesRes = await civi<{ value: string; label: string }>("OptionValue", "get", { select: ["value", "label"], where: [ - ["option_group_id:name", "=", "Stage"], + ["option_group_id", "=", ogId], ["is_active", "=", true], ], orderBy: { weight: "ASC" }, - limit: 50, + limit: 100, }); - const rows = res.values ?? []; + const rows = valuesRes.values ?? []; const expected = [ "Inquiry", "Organizing", @@ -195,13 +242,14 @@ async function probeStageOptionGroup(): Promise { ]; const present = new Set(rows.map((r) => r.value)); const missing = expected.filter((v) => !present.has(v)); + const sample = rows.slice(0, 10).map((r) => `${r.value}`).join(", "); return { check: "stage_option_group_values", ok: missing.length === 0, detail: missing.length === 0 - ? `All 6 in-scope stage values present (${expected.join(", ")}). Total active options: ${rows.length}.` - : `Missing expected stage value(s): ${missing.join(", ")}. Found ${rows.length} active options.`, + ? `All 6 in-scope stage values present in option_group_id=${ogId}. Total active options: ${rows.length}.` + : `option_group_id=${ogId} has ${rows.length} active option(s) but missing expected: ${missing.join(", ")}. First few present values: ${sample || "(none)"}`, }; } catch (e) { return {