src/features/reviews/search-fields.ts
Linked repository file from this build.
import type { SnapPolicy } from "@/programs/snap-negative/policy";
import { demoRoster } from "@/lib/auth/roster";
import { PA_LOCATIONS } from "@/programs/snap-negative/seed-generator-locations";
import {
codedFieldChoices,
normalizeReviewAnswers,
fieldLabel,
reviewFieldDefinitions,
} from "@/programs/snap-negative/field-catalog";
import {
workflowLabels,
roleLabels,
type Actor,
type Activity,
type Note,
type ReviewDocument,
type StoredFinding,
} from "./types";
import type {
ComputationResult,
FinalResult,
HouseholdMember,
IncomeStream,
Scenario,
SnapReview,
ReviewAnswers,
} from "@/programs/snap-negative/types";
export interface SearchField {
key: string;
label: string;
kind: "text" | "number" | "date" | "month" | "datetime" | "boolean" | "choice";
choices?: { value: string; label: string }[];
unit?: "cents";
}
type Kind = SearchField["kind"] | "cents" | "staff" | "county" | "state" | "outcome" | "role";
type Shape = Kind | { [key: string]: Shape } | readonly [Shape];
// Exhaustive typed manifests keep optional and empty-array fields available before they contain data.
const actorShape = {
name: "text",
role: "role",
staffId: "staff",
reportsToStaffId: "staff",
subordinateStaffIds: ["staff"],
program: "text",
} satisfies Partial<Record<keyof Required<Actor>, Shape>>;
const incomeShape = {
id: "text",
memberId: "text",
typeCode: "text",
category: "text",
amountCents: "cents",
expensesCents: "cents",
selfEmploymentKind: "text",
description: "text",
} satisfies Record<keyof IncomeStream, Shape>;
const memberShape = {
id: "text",
line: "text",
name: "text",
relationship: "text",
} satisfies Record<keyof HouseholdMember, Shape>;
const scenarioShape = {
populated: "boolean",
householdSize: "number",
income: [incomeShape],
educationGrantCents: "cents",
educationExpensesCents: "cents",
applicationDay: "number",
medicalDeductionCents: "cents",
dependentCareCents: "cents",
childSupportCents: "cents",
rentCents: "cents",
taxesInsuranceCents: "cents",
installationCents: "cents",
otherShelterCents: "cents",
sua: "text",
elderlyDisabled: "boolean",
categoricalEligibility: "boolean",
homeless: "boolean",
} satisfies Record<keyof Required<Scenario>, Shape>;
const resultShape = {
policyId: "text",
householdSize: "number",
earnedCents: "cents",
selfEmploymentProfitCents: "cents",
selfEmploymentLossCents: "cents",
educationNetCents: "cents",
earnedAndEducationCents: "cents",
monthlyBenefitCents: "cents",
unearnedCents: "cents",
grossCents: "cents",
grossLimitCents: "cents",
earnedDeductionCents: "cents",
standardDeductionCents: "cents",
adjustedIncomeCents: "cents",
suaCents: "cents",
shelterCents: "cents",
shelterDeductionCents: "cents",
netCents: "cents",
netLimitCents: "cents",
allotmentCents: "cents",
benefitCents: "cents",
eligible: "boolean",
} satisfies Record<keyof Required<ComputationResult>, Shape>;
const finalShape = {
inputs: scenarioShape,
scenario: "text",
reason: "text",
result: resultShape,
errorCents: "cents",
income: [incomeShape],
inputsRevision: "number",
} satisfies Record<keyof Required<FinalResult>, Shape>;
const answersShape: Record<string, Shape> = Object.fromEntries(
reviewFieldDefinitions.map(({ key }) => [
key,
key.startsWith("date") ? "date" : key.startsWith("days") ? "number" : "text",
]),
);
answersShape.variances = [{ element: "text", nature: "text" }];
const noteShape = {
id: "text",
body: "text",
actor: actorShape,
createdAt: "datetime",
kind: "text",
updatedAt: "datetime",
fieldLink: "text",
} satisfies Record<keyof Required<Note>, Shape>;
const documentShape = {
id: "text",
filename: "text",
name: "text",
category: "text",
description: "text",
mimeType: "text",
size: "number",
storageKey: "text",
actor: actorShape,
createdAt: "datetime",
} satisfies Record<keyof Required<ReviewDocument>, Shape>;
const activityShape = {
id: "text",
kind: "text",
actor: actorShape,
createdAt: "datetime",
description: "text",
from: "state",
to: "state",
noteId: "text",
policyNote: "text",
policyDecision: "text",
} satisfies Record<keyof Required<Activity>, Shape>;
const findingShape = {
id: "text",
field: "text",
code: "text",
message: "text",
refused: { reason: "text", actor: actorShape, at: "datetime" },
} satisfies Record<keyof Required<StoredFinding>, Shape>;
const factsShape = {
negativeAction: "text",
caoIssuedBenefitCents: "cents",
noticeDate: "date",
actionDate: "date",
paCap: "text",
members: [memberShape],
scenarios: { eligibility: scenarioShape, compI: scenarioShape, compII: scenarioShape },
inputsRevision: "number",
answers: answersShape,
final: finalShape,
} satisfies Shape;
const approvalShape = { actor: actorShape, at: "datetime" } satisfies Shape;
const receivedShape = {
facts: factsShape,
outcome: "outcome",
dropKind: "text",
identity: {
clientName: "text",
address: "text",
sampleMonth: "month",
caseNumber: "text",
county: "county",
district: "text",
localAgency: "text",
examiner: "staff",
supervisor: "staff",
},
} satisfies Shape;
const policyShape = {
id: "text",
effectiveFrom: "month",
source: "text",
verification: "text",
grossLimits: ["number"],
netLimits: ["number"],
standardDeductions: ["number"],
allotments: ["number"],
sua: {
heating: "number",
non_heating: "number",
limited: "number",
telephone: "number",
none: "number",
},
shelterCap: "number",
shelterDeductionPercent: "number",
minimumBenefit: "number",
homelessDeduction: "number",
earnedDeductionPercent: "number",
farmExpenseAllowancePercent: "number",
nonFarmExpenseAllowancePercent: "number",
errorThreshold: "number",
memoResponseDays: "number",
} satisfies Record<keyof SnapPolicy, Shape>;
const shape = {
id: "text",
reviewNumber: "number",
caseNumber: "text",
program: "text",
subtype: "text",
sampleMonth: "month",
clientName: "text",
address: "text",
county: "county",
district: "text",
localAgency: "text",
examiner: "staff",
supervisor: "staff",
state: "state",
outcome: "outcome",
dropKind: "text",
supervisorApproved: approvalShape,
pecApproved: approvalShape,
memoApproved: approvalShape,
errorSummaryReleased: approvalShape,
dropApproved: approvalShape,
caoResponse: {
status: "text",
rootCause: "text",
note: "text",
actor: actorShape,
at: "datetime",
},
caoResponseDeadline: "date",
receivedSnapshots: {
examiner: receivedShape,
supervisor: receivedShape,
program_manager: receivedShape,
},
policyId: "text",
policy: policyShape,
revision: "number",
facts: factsShape,
notes: [noteShape],
documents: [documentShape],
activity: [activityShape],
validation: { revision: "number", findings: [findingShape], createdAt: "datetime" },
pecDecision: { decision: "text", policyNote: "text", actor: actorShape, createdAt: "datetime" },
conditionalNotes: "text",
appointment: "datetime",
memos: [
{
id: "text",
body: "text",
actor: actorShape,
createdAt: "datetime",
reviewRevision: "number",
type: "text",
outboundAt: "datetime",
},
],
calcVersion: "text",
checkVersion: "text",
lineage: [{ sourceSystem: "text", role: "text", fileName: "text", rowNumber: "number" }],
intake: {
batchId: "text",
sources: {
fileNames: { closedSuspended: "text", rejected: "text" },
},
},
} satisfies Record<keyof Required<SnapReview>, Shape>;
const rootLabels: Record<string, string> = {
reviewNumber: "Review number",
caseNumber: "Case number",
clientName: "Client name",
examiner: "Examiner",
supervisor: "Supervisor",
sampleMonth: "Sample month",
state: "Workflow state",
outcome: "Outcome",
appointment: "Appointment",
county: "County",
facts: "Workbook",
scenarios: "Computation",
eligibility: "Eligibility",
compI: "Comp I",
compII: "Comp II",
final: "Final SAQC",
answers: "Review answers",
inputs: "Inputs",
paCap: "Legacy PA CAP",
size: "File size (bytes)",
};
const countyChoices = [
...new Map(
PA_LOCATIONS.map((row) => [row.county, { value: row.county, label: row.countyName }]),
).values(),
];
const staffChoices = demoRoster.map((actor) => ({ value: actor.staffId!, label: actor.name }));
function readable(key: string) {
return (
rootLabels[key] ??
key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/^./, (letter) => letter.toUpperCase())
);
}
function labelFor(key: string) {
return key
.split(".")
.map((part, index, parts) => {
const clean = part
.replace(/\[\]/g, "")
.replace(/~4/g, "")
.replace(/~3/g, "]")
.replace(/~2/g, "[")
.replace(/~1/g, ".")
.replace(/~0/g, "~");
return parts[index - 1] === "answers" && clean in normalizeReviewAnswers({})
? fieldLabel(clean as keyof ReviewAnswers)
: readable(clean);
})
.join(" › ");
}
function fieldFor(key: string, kind: Kind): SearchField {
const choices =
kind === "staff"
? key === "examiner" || key === "supervisor"
? demoRoster
.filter((actor) => actor.role === key)
.map((actor) => ({ value: actor.staffId!, label: actor.name }))
: staffChoices
: kind === "county"
? countyChoices
: kind === "state"
? Object.entries(workflowLabels).map(([value, label]) => ({ value, label }))
: kind === "role"
? Object.entries(roleLabels).map(([value, label]) => ({ value, label }))
: kind === "outcome"
? [
{ value: "", label: "Unclassified" },
{ value: "clean", label: "Valid" },
{ value: "error", label: "Invalid" },
{ value: "drop", label: "Drop" },
]
: key.startsWith("facts.answers.")
? codedFieldChoices[key.slice(14) as keyof ReviewAnswers]
: undefined;
return {
key,
label: `${labelFor(key)}${kind === "cents" ? " (cents)" : ""}`,
kind: choices ? "choice" : kind === "cents" ? "number" : (kind as SearchField["kind"]),
...(choices ? { choices } : {}),
...(kind === "cents" ? { unit: "cents" as const } : {}),
};
}
const fields = new Map<string, SearchField>();
function visitShape(node: Shape, key = "") {
if (key) fields.set(key, fieldFor(key, typeof node === "string" ? node : "text"));
if (typeof node === "string") return;
if (Array.isArray(node)) {
visitShape(node[0], `${key}[]`);
return;
}
for (const [part, child] of Object.entries(node))
visitShape(child, key ? `${key}.${part}` : part);
}
visitShape(shape);
const leading = [
"reviewNumber",
"caseNumber",
"clientName",
"examiner",
"appointment",
"state",
"outcome",
"sampleMonth",
"county",
];
export const searchFields = [
...leading.map((key) => fields.get(key)!),
...[...fields.values()].filter((field) => !leading.includes(field.key)),
];
/** ~0 and ~1 escape literal tilde and dot in dynamically discovered map keys. */
export function validSearchField(key: string) {
if (key.length > 400 || !key) return false;
const parts = key.split(".");
return (
Object.hasOwn(shape, parts[0].replace(/\[\]$/, "")) &&
parts.every((part) => {
const name = part.endsWith("[]") ? part.slice(0, -2) : part;
return (
!!name &&
!name.includes("[") &&
!name.includes("]") &&
![...name].some((char) => char.charCodeAt(0) < 32) &&
!/~(?![0-4])/.test(name)
);
})
);
}
export function isKnownSearchField(key: string) {
return fields.has(key);
}
export function getSearchField(key: string): SearchField {
return fields.get(key) ?? fieldFor(key, key.endsWith("Cents") ? "cents" : "text");
}
export function searchPath(key: string): string {
return (
"$" +
key
.split(".")
.map((part) => {
const repeated = part.endsWith("[]");
const name = (repeated ? part.slice(0, -2) : part)
.replace(/~4/g, "")
.replace(/~3/g, "]")
.replace(/~2/g, "[")
.replace(/~1/g, ".")
.replace(/~0/g, "~");
return `.${JSON.stringify(name)}${repeated ? "[*]" : ""}`;
})
.join("")
);
}
export function fieldRawValues(review: SnapReview, key: string): unknown[] {
let values: unknown[] = [review];
for (const part of key.split(".")) {
const repeated = part.endsWith("[]");
const name = (repeated ? part.slice(0, -2) : part)
.replace(/~4/g, "")
.replace(/~3/g, "]")
.replace(/~2/g, "[")
.replace(/~1/g, ".")
.replace(/~0/g, "~");
values = values.flatMap((value) => {
if (!value || typeof value !== "object" || !Object.hasOwn(value, name)) return [];
const child = (value as Record<string, unknown>)[name];
return repeated ? (Array.isArray(child) ? child : []) : [child];
});
}
return values;
}
export function fieldDisplayValues(review: SnapReview, key: string): string[] {
return fieldDisplayRawValues(fieldRawValues(review, key), key);
}
export function fieldDisplayRawValues(values: unknown[], key: string): string[] {
const field = getSearchField(key);
return [
...new Set(
values.map((value) => {
if (value === null || value === undefined || value === "")
return value === ""
? (field.choices?.find((choice) => choice.value === "")?.label ?? "Not set")
: "Not set";
if (field.unit === "cents" && typeof value === "number")
return (value / 100).toLocaleString("en-US", { style: "currency", currency: "USD" });
if (typeof value === "boolean") return value ? "Yes" : "No";
const text = typeof value === "object" ? JSON.stringify(value) : String(value);
return field.choices?.find((choice) => choice.value === text)?.label ?? text;
}),
),
];
}