src/features/reviews/computation-discrepancies.ts
Linked repository file from this build.
import { calculate } from "@/programs/snap-negative/calculation";
import type { Scenario, SnapReview } from "@/programs/snap-negative/types";
import { resolveReviewPolicy } from "@/programs/snap-negative/policy";
const fields = {
householdSize: "Household size",
educationGrantCents: "Education income",
educationExpensesCents: "Education expenses",
applicationDay: "Application day",
medicalDeductionCents: "Medical deduction",
dependentCareCents: "Dependent care",
childSupportCents: "Child support",
rentCents: "Rent / mortgage",
taxesInsuranceCents: "Taxes / insurance",
installationCents: "Utility installation",
otherShelterCents: "Other shelter",
sua: "SUA",
elderlyDisabled: "Elderly / disabled",
categoricalEligibility: "Categorical eligibility",
homeless: "Homeless deduction",
} satisfies Partial<Record<keyof Scenario, string>>;
/** One changed input per full column. Rank by absolute benefit effect on Eligibility. */
export function buildDiscrepancies(review: SnapReview) {
const final = review.facts.final;
if (!final || final.inputsRevision !== review.facts.inputsRevision) return [];
const baseline = review.facts.scenarios.eligibility;
const selected = final.inputs ?? review.facts.scenarios[final.scenario];
const candidates: { label: string; scenario: Scenario }[] = [];
for (const [key, label] of Object.entries(fields) as [keyof typeof fields, string][]) {
if ((baseline[key] ?? 0) !== (selected[key] ?? 0))
candidates.push({ label, scenario: { ...baseline, [key]: selected[key] } });
}
const unmatched = [...selected.income];
const paired = baseline.income.map((before) => {
let index = unmatched.findIndex((after) => after.id === before.id);
if (index < 0)
index = unmatched.findIndex(
(after) =>
after.memberId === before.memberId &&
after.typeCode.trim().toLowerCase() === before.typeCode.trim().toLowerCase(),
);
return [before, index < 0 ? undefined : unmatched.splice(index, 1)[0]] as const;
});
const incomePairs = [...paired, ...unmatched.map((after) => [undefined, after] as const)];
for (const [before, after] of incomePairs) {
if (
before?.amountCents === after?.amountCents &&
before?.expensesCents === after?.expensesCents &&
before?.selfEmploymentKind === after?.selfEmploymentKind &&
before?.category === after?.category &&
before?.typeCode === after?.typeCode
)
continue;
const row = after ?? before!;
const member = review.facts.members.find((person) => person.id === row.memberId);
candidates.push({
label: `${member?.name ?? row.memberId} · ${row.typeCode}`,
scenario: {
...baseline,
income: [
...baseline.income.filter((entry) => entry.id !== before?.id),
...(after ? [after] : []),
],
},
});
}
try {
const policy = review.policy || review.policyId ? resolveReviewPolicy(review) : undefined;
const base = calculate(baseline, review.sampleMonth, review.calcVersion, policy);
return candidates
.flatMap((candidate) => {
try {
const result = calculate(
candidate.scenario,
review.sampleMonth,
review.calcVersion,
policy,
);
return [{ ...candidate, result, errorCents: base.benefitCents - result.benefitCents }];
} catch {
return [];
}
})
.sort((a, b) => Math.abs(b.errorCents) - Math.abs(a.errorCents))
.slice(0, 6);
} catch {
return [];
}
}