Add /api/health diagnostic route; remove stale .js route duplicates
This commit is contained in:
@@ -1,60 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { civicrmApi } from '@/lib/civicrm';
|
||||
import mapping from '@/config/mapping.json';
|
||||
|
||||
export async function GET(request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const orgId = searchParams.get('orgId');
|
||||
const contactId = searchParams.get('contactId');
|
||||
|
||||
if (!orgId || !contactId) {
|
||||
return NextResponse.json({ error: 'Missing orgId or contactId' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Fetch current stage from Organization (Contact record with type Organization)
|
||||
const orgResult = await civicrmApi('Contact', 'get', {
|
||||
select: [mapping.stageField],
|
||||
where: [['id', '=', parseInt(orgId)]],
|
||||
});
|
||||
|
||||
const currentStage = orgResult.values[0]?.[mapping.stageField] || 0;
|
||||
|
||||
// 2. Prepare select fields for pre-filling contact data
|
||||
const selectFields = ['id'];
|
||||
mapping.stages.forEach(stage => {
|
||||
stage.fields.forEach(field => {
|
||||
if (field.entity === 'Contact') {
|
||||
selectFields.push(field.crmField);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 3. Fetch contact data for pre-filling
|
||||
const contactResult = await civicrmApi('Contact', 'get', {
|
||||
select: selectFields,
|
||||
where: [['id', '=', parseInt(contactId)]],
|
||||
});
|
||||
|
||||
const contactData = contactResult.values[0] || {};
|
||||
|
||||
return NextResponse.json({
|
||||
currentStage: parseInt(currentStage),
|
||||
prefillData: contactData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
// Return mock data if CiviCRM is not reachable (for development/demo)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return NextResponse.json({
|
||||
currentStage: 2,
|
||||
prefillData: {
|
||||
first_name: 'John',
|
||||
last_name: 'Doe',
|
||||
'email_primary.email': 'john@example.com'
|
||||
}
|
||||
});
|
||||
}
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
263
app/api/health/route.ts
Normal file
263
app/api/health/route.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* GET /api/health
|
||||
*
|
||||
* Diagnostic endpoint that probes the configured CiviCRM instance and
|
||||
* reports back exactly which capability checks pass or fail. Useful for:
|
||||
* - confirming env vars are loaded
|
||||
* - confirming auth headers are accepted
|
||||
* - confirming the expected entities (relationship type, activity type,
|
||||
* stage option group) exist with the names this app expects
|
||||
* - confirming Contact.validateChecksum is callable (so the auth path
|
||||
* in /api/data won't fail)
|
||||
*
|
||||
* Returns a JSON object with one entry per check: `{ check, ok, detail }`.
|
||||
* No secrets are echoed back — only the base URL and presence-of-key
|
||||
* booleans.
|
||||
*
|
||||
* Safe to expose. If you want to lock it down later, gate behind
|
||||
* `process.env.NODE_ENV !== "production"` or an admin token check.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { civi, verifyChecksum } from "@/lib/civicrm";
|
||||
import { ACTIVITY_TYPE_NAME } from "@/config/form";
|
||||
|
||||
interface CheckResult {
|
||||
check: string;
|
||||
ok: boolean;
|
||||
detail: string;
|
||||
}
|
||||
|
||||
function envSummary(): CheckResult {
|
||||
const have = {
|
||||
CIVI_BASE_URL: Boolean(process.env.CIVI_BASE_URL),
|
||||
CIVI_API_KEY: Boolean(process.env.CIVI_API_KEY),
|
||||
CIVI_SITE_KEY: Boolean(process.env.CIVI_SITE_KEY),
|
||||
};
|
||||
const ok = have.CIVI_BASE_URL && have.CIVI_API_KEY && have.CIVI_SITE_KEY;
|
||||
const baseUrl = process.env.CIVI_BASE_URL ?? "(unset)";
|
||||
const detail = ok
|
||||
? `All three env vars present. CIVI_BASE_URL=${baseUrl}`
|
||||
: `Missing: ${Object.entries(have).filter(([, v]) => !v).map(([k]) => k).join(", ") || "(none)"}. STUB MODE active.`;
|
||||
return { check: "env_vars", ok, detail };
|
||||
}
|
||||
|
||||
async function probeContactGet(): Promise<CheckResult> {
|
||||
try {
|
||||
const res = await civi("Contact", "get", {
|
||||
select: ["id"],
|
||||
where: [["id", "=", 1]],
|
||||
limit: 1,
|
||||
});
|
||||
return {
|
||||
check: "civi_auth_and_contact_get",
|
||||
ok: true,
|
||||
detail: `Contact.get succeeded. Returned ${(res.values ?? []).length} row(s).`,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
check: "civi_auth_and_contact_get",
|
||||
ok: false,
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function probeRelationshipType(): Promise<CheckResult> {
|
||||
try {
|
||||
const res = await civi<{ id: number; name_a_b: string; name_b_a: string }>(
|
||||
"RelationshipType",
|
||||
"get",
|
||||
{
|
||||
select: ["id", "name_a_b", "name_b_a"],
|
||||
where: [["name_a_b", "=", "Primary Form Contact of"]],
|
||||
},
|
||||
);
|
||||
const rows = res.values ?? [];
|
||||
return {
|
||||
check: "relationship_type_primary_form_contact_of",
|
||||
ok: rows.length === 1,
|
||||
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.`,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
check: "relationship_type_primary_form_contact_of",
|
||||
ok: false,
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function probeActivityType(): Promise<CheckResult> {
|
||||
try {
|
||||
const res = await civi<{ value: string; name: string; label: string }>(
|
||||
"OptionValue",
|
||||
"get",
|
||||
{
|
||||
select: ["value", "name", "label"],
|
||||
where: [
|
||||
["option_group_id:name", "=", "activity_type"],
|
||||
["name", "=", ACTIVITY_TYPE_NAME],
|
||||
],
|
||||
},
|
||||
);
|
||||
const rows = res.values ?? [];
|
||||
return {
|
||||
check: "activity_type_check_in_organizing",
|
||||
ok: rows.length === 1,
|
||||
detail:
|
||||
rows.length === 1
|
||||
? `Found value=${rows[0].value}, name="${rows[0].name}".`
|
||||
: `Expected exactly one activity type named "${ACTIVITY_TYPE_NAME}", got ${rows.length}.`,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
check: "activity_type_check_in_organizing",
|
||||
ok: false,
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function probeStageOptionGroup(): Promise<CheckResult> {
|
||||
try {
|
||||
const res = await civi<{ value: string; label: string }>("OptionValue", "get", {
|
||||
select: ["value", "label"],
|
||||
where: [
|
||||
["option_group_id:name", "=", "Stage"],
|
||||
["is_active", "=", true],
|
||||
],
|
||||
orderBy: { weight: "ASC" },
|
||||
limit: 50,
|
||||
});
|
||||
const rows = res.values ?? [];
|
||||
const expected = [
|
||||
"Inquiry",
|
||||
"Organizing",
|
||||
"Feasibility",
|
||||
"Business feasibility",
|
||||
"Store Implementation",
|
||||
"Stabilize newly opened co-op",
|
||||
];
|
||||
const present = new Set(rows.map((r) => r.value));
|
||||
const missing = expected.filter((v) => !present.has(v));
|
||||
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.`,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
check: "stage_option_group_values",
|
||||
ok: false,
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function probeChecksumApi(): Promise<CheckResult> {
|
||||
// Tries to call validateChecksum with a deliberately invalid checksum.
|
||||
// If the API exists, we get back {valid: false} or similar — that's
|
||||
// success for THIS check (the API responded). If the API doesn't exist
|
||||
// we fall back to a Contact.get probe in verifyChecksum, which will
|
||||
// also fail gracefully.
|
||||
try {
|
||||
await civi("Contact", "validateChecksum", {
|
||||
contactId: 1,
|
||||
checksum: "invalid-deliberate-test",
|
||||
});
|
||||
return {
|
||||
check: "contact_validate_checksum_api",
|
||||
ok: true,
|
||||
detail: "Contact.validateChecksum action is callable.",
|
||||
};
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
// If the error is "API action not found" we know it's missing.
|
||||
// If it's "invalid checksum" or similar, the API IS present and rejecting our test value (that's fine).
|
||||
if (/not\s*found|unknown\s*action|does not exist/i.test(msg)) {
|
||||
return {
|
||||
check: "contact_validate_checksum_api",
|
||||
ok: false,
|
||||
detail: `validateChecksum action not present in this Civi version. The verifyChecksum() helper falls back to a Contact.get probe with cs param. Detail: ${msg}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
check: "contact_validate_checksum_api",
|
||||
ok: true,
|
||||
detail: `Endpoint reachable; expected rejection of test value. Detail: ${msg.slice(0, 160)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function probeChecksumWithSampleContact(): Promise<CheckResult> {
|
||||
// Optional: lets the user supply ?cid=&cs= to actually verify a real
|
||||
// checksum against the configured CiviCRM. Skipped if not provided.
|
||||
return {
|
||||
check: "checksum_live_test",
|
||||
ok: true,
|
||||
detail: "Pass ?cid=...&cs=... to /api/health to test a live checksum (skipped — no params).",
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const env = envSummary();
|
||||
const summary = { mode: env.ok ? "live" : "stub" };
|
||||
|
||||
if (!env.ok) {
|
||||
// No live calls in stub mode — return early.
|
||||
return NextResponse.json({ ...summary, checks: [env] });
|
||||
}
|
||||
|
||||
const url = new URL(req.url);
|
||||
const cid = url.searchParams.get("cid");
|
||||
const cs = url.searchParams.get("cs");
|
||||
|
||||
const checks: CheckResult[] = [env];
|
||||
|
||||
// Run independent probes in parallel.
|
||||
const probes = await Promise.all([
|
||||
probeContactGet(),
|
||||
probeRelationshipType(),
|
||||
probeActivityType(),
|
||||
probeStageOptionGroup(),
|
||||
probeChecksumApi(),
|
||||
]);
|
||||
checks.push(...probes);
|
||||
|
||||
// If a sample cid+cs was provided, verify that specific checksum.
|
||||
if (cid && cs) {
|
||||
try {
|
||||
const ok = await verifyChecksum(cid, cs);
|
||||
checks.push({
|
||||
check: "checksum_live_test",
|
||||
ok,
|
||||
detail: ok
|
||||
? `verifyChecksum(cid=${cid}, cs=...) returned true.`
|
||||
: `verifyChecksum(cid=${cid}, cs=...) returned false. Either the checksum is invalid or the verifier path failed silently.`,
|
||||
});
|
||||
} catch (e) {
|
||||
checks.push({
|
||||
check: "checksum_live_test",
|
||||
ok: false,
|
||||
detail: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
checks.push(await probeChecksumWithSampleContact());
|
||||
}
|
||||
|
||||
const allOk = checks.every((c) => c.ok);
|
||||
return NextResponse.json(
|
||||
{ ...summary, allOk, checks },
|
||||
{ status: allOk ? 200 : 500 },
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { civicrmApi } from '@/lib/civicrm';
|
||||
import mapping from '@/config/mapping.json';
|
||||
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { contactId, orgId, formData } = body;
|
||||
|
||||
if (!contactId || !orgId || !formData) {
|
||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 1. Separate fields by entity for updating
|
||||
const contactUpdates = {};
|
||||
|
||||
mapping.stages.forEach(stage => {
|
||||
stage.fields.forEach(field => {
|
||||
if (formData[field.name] !== undefined) {
|
||||
if (field.entity === 'Contact') {
|
||||
contactUpdates[field.crmField] = formData[field.name];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 2. Update Contact in CiviCRM
|
||||
if (Object.keys(contactUpdates).length > 0) {
|
||||
await civicrmApi('Contact', 'save', {
|
||||
records: [{ id: parseInt(contactId), ...contactUpdates }],
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Create Activity in CiviCRM to record the submission
|
||||
await civicrmApi('Activity', 'create', {
|
||||
values: {
|
||||
activity_type_id: 'Meeting', // Or a custom "Form Submission" activity type
|
||||
subject: `Stage Progression Form Submission (Org ID: ${orgId})`,
|
||||
target_contact_id: [parseInt(contactId)],
|
||||
source_contact_id: parseInt(contactId), // Assuming contact submits for themselves or adjust
|
||||
status_id: 'Completed',
|
||||
details: JSON.stringify(formData, null, 2),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true, message: 'Data saved and activity created' });
|
||||
} catch (error) {
|
||||
console.error('Submission Error:', error);
|
||||
// Mock success for development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return NextResponse.json({ success: true, message: 'Mock Success' });
|
||||
}
|
||||
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user