src/programs/snap-negative/validation.ts
Linked repository file from this build.
import {
fieldLabel,
reviewFieldDefinitions,
normalizeReviewAnswers,
requiredFieldsForAnswers,
validateRequiredField,
validateVariancePairs,
} from "./field-catalog";
import {
assertCheckVersion,
CURRENT_CHECK_VERSION,
isLegacyCheckVersion,
LEGACY_CHECK_VERSION,
} from "./rule-versions";
import type { InputBlocker, StoredFinding } from "@/features/reviews/types";
import type { SnapReview, EligibilityReview } from "./types";
function accessAnswers(review: EligibilityReview) {
return normalizeReviewAnswers(review.facts.answers);
}
function blocker(field: string, code: string, message: string): InputBlocker {
return { field, code, message };
}
function storedFinding(id: string, field: string, code: string, message: string): StoredFinding {
return { id, field, code, message };
}
/** Required-field and valid-value checks that block submit. */
export function validateRequiredInput(review: EligibilityReview): InputBlocker[] {
assertCheckVersion(review.checkVersion ?? LEGACY_CHECK_VERSION);
if (isLegacyCheckVersion(review.checkVersion ?? LEGACY_CHECK_VERSION)) {
return validateLegacyRequiredInput(review);
}
const answers = accessAnswers(review);
const blockers: InputBlocker[] = [];
const seen = new Set<string>();
for (const key of new Set([
...requiredFieldsForAnswers(answers),
...reviewFieldDefinitions.map((f) => f.key),
])) {
const message = validateRequiredField(
key,
answers,
review.checkVersion === CURRENT_CHECK_VERSION,
);
if (message && !seen.has(key)) {
seen.add(key);
blockers.push(blocker(key, "REQUIRED", message));
}
}
for (const issue of validateVariancePairs(answers.variances)) {
const field = `variances.${issue.index}.${issue.field}`;
blockers.push(blocker(field, "INVALID_VARIANCE", issue.message));
}
return blockers;
}
function validateLegacyRequiredInput(review: EligibilityReview): InputBlocker[] {
const answers = accessAnswers(review);
const blockers: InputBlocker[] = [];
if (!answers.disposition.trim()) {
blockers.push(blocker("disposition", "REQUIRED", `${fieldLabel("disposition")} is required.`));
}
if (answers.disposition === "1") {
if (!answers.contactResponse.trim()) {
blockers.push(
blocker(
"contactResponse",
"REQUIRED",
`${fieldLabel("contactResponse")} is required for a completed review.`,
),
);
}
if (!answers.actionInitiatedBy.trim()) {
blockers.push(
blocker(
"actionInitiatedBy",
"REQUIRED",
`${fieldLabel("actionInitiatedBy")} is required for a completed review.`,
),
);
}
}
return blockers;
}
/** Cross-field edit-check findings stored on submit; they do not block Awaiting Review. */
export function runFullEditCheck(review: SnapReview): StoredFinding[] {
assertCheckVersion(review.checkVersion ?? LEGACY_CHECK_VERSION);
if (isLegacyCheckVersion(review.checkVersion ?? LEGACY_CHECK_VERSION)) {
return runLegacyBasicCheck(review);
}
const answers = accessAnswers(review);
const findings: StoredFinding[] = [];
let seq = 0;
if (answers.disposition !== "1") return findings;
if (answers.recordedReasonAccuracy === "2" && answers.finding !== "2") {
findings.push(
storedFinding(
`finding-${++seq}`,
"finding",
"FINDING_MUST_BE_INVALID",
`${fieldLabel("recordedReasonAccuracy")} is inaccurate; ${fieldLabel("finding")} must be coded invalid (2).`,
),
);
}
if (answers.noticeClearUnderstandable === "2" && !answers.narrative.trim()) {
findings.push(
storedFinding(
`finding-${++seq}`,
"narrative",
"NARRATIVE_REQUIRED",
`${fieldLabel("noticeClearUnderstandable")} is No; ${fieldLabel("narrative")} explanation is required.`,
),
);
}
return findings;
}
function runLegacyBasicCheck(review: SnapReview): StoredFinding[] {
return validateLegacyRequiredInput(review).map((missing, index) =>
storedFinding(`finding-${index + 1}`, missing.field, missing.code, missing.message),
);
}
/** Legacy validate API: returns simple findings for the bound check version. */
export function validateReview(review: SnapReview): StoredFinding[] {
assertCheckVersion(review.checkVersion ?? LEGACY_CHECK_VERSION);
if (!isLegacyCheckVersion(review.checkVersion ?? LEGACY_CHECK_VERSION)) {
let seq = 0;
return [
...validateRequiredInput(review).map((missing) =>
storedFinding(`blocker-${++seq}`, missing.field, missing.code, missing.message),
),
...runFullEditCheck(review).map((finding) =>
storedFinding(`finding-${++seq}`, finding.field, finding.code, finding.message),
),
];
}
return runLegacyBasicCheck(review);
}
/** ERC and downstream queries ignore refused findings. */
export function activeStoredFindings(findings: StoredFinding[]): StoredFinding[] {
return findings.filter((finding) => !finding.refused);
}
export function hasOpenStoredFindings(review: EligibilityReview): boolean {
if (!review.validation || review.validation.revision !== review.revision) return true;
return activeStoredFindings(review.validation.findings).length > 0;
}
export function requiredInputComplete(review: EligibilityReview): boolean {
return validateRequiredInput(review).length === 0;
}
export function finalCurrent(review: EligibilityReview): boolean {
return review.facts.final?.inputsRevision === review.facts.inputsRevision;
}
/** A stored check must match the current revision and have no active findings. */
export function validationCurrent(review: EligibilityReview): boolean {
return !hasOpenStoredFindings(review);
}