src/features/reviews/downstream.ts
Linked repository file from this build.
import type { SnapReview, EligibilityReview } from "@/programs/snap-negative/types";
import type { Actor, ActorRef, ReviewMemo } from "./types";
import { can } from "@/lib/auth/scope";
import { PEC_ROUTING_PENDING } from "./workflow";
import { RuleError } from "@/lib/errors";
export const CURRENT_TEMPLATE_FNS_DISCLAIMER =
"Current-template FNS-245 TSV field export from the FS Negative workbook inventory. Not a legacy federal wire-format or portal-acceptance file.";
export const EPP_ADAPTER_DISCLAIMER =
"Adapter payload only. External EPP connection is pending; this application does not write to tblTANF_MA_SNAP_Records.";
/** Federal fiscal year from a YYYY-MM sample month (Oct–Sep). */
export function deriveFfyFromSampleMonth(sampleMonth: string): number {
const match = /^(\d{4})-(0[1-9]|1[0-2])$/.exec(sampleMonth.trim());
if (!match) throw new RuleError(`Sample month must be YYYY-MM; received "${sampleMonth}".`);
const year = Number(match[1]);
const month = Number(match[2]);
return month >= 10 ? year + 1 : year;
}
function parseSampleMonthParts(sampleMonth: string): { month: string; year: string } {
const match = /^(\d{4})-(0[1-9]|1[0-2])$/.exec(sampleMonth.trim());
if (!match) throw new RuleError(`Sample month must be YYYY-MM; received "${sampleMonth}".`);
return { month: match[2]!, year: match[1]! };
}
export interface DownstreamGateResult {
ok: boolean;
reasons: string[];
}
/** Both reporting products require the manager's release flag. Drops use their own approval. */
export function downstreamGateStatus(review: EligibilityReview): DownstreamGateResult {
const reasons: string[] = [];
if (!review.errorSummaryReleased)
reasons.push("The program manager must release this review to reporting.");
if (review.outcome === "drop") reasons.push("Program Error Summary excludes drops.");
return { ok: reasons.length === 0, reasons };
}
export function transmittalEligible(review: SnapReview): boolean {
return review.outcome === "drop" ? Boolean(review.dropApproved) : downstreamGateStatus(review).ok;
}
export function assertDownstreamEligible(review: SnapReview): void {
const status = downstreamGateStatus(review);
if (!status.ok) throw new RuleError(status.reasons.join(" "));
}
function assertSenior(actor: Actor): void {
if (!can(actor, "seniorReview"))
throw new RuleError("This adapter requires a supervisor or program manager.");
}
export interface MemoEligibility {
canCreate: boolean;
showEditor: boolean;
message: string;
}
export function memoEligibility(review: SnapReview, actor: Actor): MemoEligibility {
const showEditor = actor.role === "program_manager" && review.outcome !== "drop";
const canCreate =
showEditor &&
Boolean(review.pecApproved) &&
review.state === "awaiting_manager" &&
!review.memoApproved;
return {
canCreate,
showEditor,
message:
review.outcome === "drop"
? "Drops skip memos and Program Error Summary."
: actor.role !== "program_manager"
? "The program manager edits and approves the memo."
: !review.pecApproved
? "The memo becomes editable after PEC approval."
: review.memoApproved
? "The approved letter is fixed. Track the CAO response below."
: "Verify the letter and its error assignment before approval.",
};
}
/** Generate the letter from recorded facts. Type stays unresolved until management supplies the map. */
export function autoGenerateMemo(
review: SnapReview,
actor: ActorRef,
meta: { id: string; at: string },
): ReviewMemo {
if (review.outcome === "drop" || !review.pecApproved)
throw new RuleError("PEC approval is required to generate a memo.");
const variance = review.facts.answers.variances[0];
return {
id: meta.id,
actor,
createdAt: meta.at,
reviewRevision: review.revision,
body: [
`CAO: ${review.localAgency}`,
`Review: ${review.reviewNumber}`,
`Case: ${review.caseNumber}`,
`Finding: ${review.outcome === "clean" ? "Valid" : "Invalid"}`,
variance ? `Element: ${variance.element}. Nature: ${variance.nature}.` : "",
review.facts.answers.narrative,
]
.filter(Boolean)
.join("\n\n"),
};
}
export function assertSummaryRelease(review: SnapReview, at: string): void {
if (review.outcome === "drop" || review.state !== "awaiting_cao_response" || !review.memoApproved)
throw new RuleError("Approve the memo before releasing this review to reporting.");
const response = review.caoResponse;
if (response?.status === "agree" && response.rootCause.trim()) return;
if (
response?.status === "no_response" &&
review.caoResponseDeadline &&
at.slice(0, 10) > review.caoResponseDeadline
)
return;
throw new RuleError(
response?.status === "dispute"
? "Resolve the CAO dispute before release. Reassignment rules need management confirmation."
: "Record a CAO agreement with root cause, or no response after the deadline.",
);
}
export { PEC_ROUTING_PENDING };
export const FNS_TEMPLATE_COLUMNS = [
{ header: "reviewNumber", item: "1 Review Number" },
{ header: "caseNumber", item: "2 Case Number" },
{ header: "stateCode", item: "3 State Code" },
{ header: "localAgency", item: "3 Local Agency" },
{ header: "sampleMonth", item: "4 Sample Month" },
{ header: "sampleYear", item: "4 Sample Year" },
{ header: "stratum", item: "5 Stratum" },
{ header: "noticeDate", item: "6 Notice Date" },
{ header: "actionDate", item: "7 Action Date" },
{ header: "actionType", item: "8 Action Type" },
{
header: "caseClassification",
item: "9 Case Classification",
},
{ header: "disposition", item: "10a Disposition" },
{ header: "nstrReason", item: "10b NSTR Reason" },
{ header: "finding", item: "11 Finding" },
{ header: "recordedReason", item: "12a Recorded Reason" },
{
header: "recordedReasonAccuracy",
item: "12b Recorded Reason Accuracy",
},
{ header: "noticeRequirement", item: "13 Notice Requirement" },
{ header: "noticeLanguage", item: "14a Required Language" },
{
header: "noticeClearUnderstandable",
item: "14b Specific Clear Understandable",
},
{ header: "noticeReasonToHh", item: "14c Reason to HH" },
{
header: "noticeReasonAccuracy",
item: "14d Accuracy of Reason to HH",
},
{
header: "missedInterviewNotice",
item: "15a Notice of Missed Interview",
},
{ header: "contactRequest", item: "15b Request for Contact" },
{
header: "verificationRequest",
item: "15c Request for Verification",
},
{ header: "periodicReport", item: "15d Periodic Report" },
{
header: "actionTimeliness",
item: "16a Timeliness of Action",
},
{
header: "daysActionEarlyLate",
item: "16b Days Action Early/Late",
},
{
header: "noticeTimeliness",
item: "16c Timeliness of Notice",
},
{ header: "daysNoticeLate", item: "16d Days Notice Late" },
{ header: "element1", item: "17 Element Code 1" },
{ header: "nature1", item: "18 Nature Code 1" },
{
header: "contactResponse",
item: "19 Collateral/Household Contact",
},
{
header: "actionInitiatedBy",
item: "20 Action Initiated By",
},
] as const;
export function currentTemplateFnsRow(review: SnapReview): string[] {
const answers = review.facts.answers;
const { month, year } = parseSampleMonthParts(review.sampleMonth);
const variances = answers.variances;
return [
review.reviewNumber,
review.caseNumber,
"42",
review.localAgency,
month,
year,
answers.stratum ?? "",
review.facts.noticeDate,
review.facts.actionDate,
answers.actionType,
answers.caseClassification,
answers.disposition,
answers.nstrReason,
answers.finding,
answers.recordedReason,
answers.recordedReasonAccuracy,
answers.noticeRequirement,
answers.noticeLanguage,
answers.noticeClearUnderstandable,
answers.noticeReasonToHh,
answers.noticeReasonAccuracy,
answers.missedInterviewNotice,
answers.contactRequest,
answers.verificationRequest,
answers.periodicReport,
answers.actionTimeliness,
answers.daysActionEarlyLate,
answers.noticeTimeliness,
answers.daysNoticeLate,
variances[0]?.element ?? "",
variances[0]?.nature ?? "",
answers.contactResponse,
answers.actionInitiatedBy,
];
}
/** Verified EPP columns only; MA-specific mappings stay documented, not invented. */
export const EPP_VERIFIED_COLUMN_KEYS = [
"QC_Record_Number",
"Program_Type",
"FFY",
"Record_Number",
"Review_Month",
] as const;
export const EPP_UNVERIFIED_MA_MAPPINGS = [
"Confirm Program_Type SNAP for the receiving tracker",
"Record_Last_Name (separate structured surname is not available)",
"Area_County_District (processing-area mapping not verified)",
"Date_QCMemo_Sent_CAO / CAO_Resp_QC_Memo_Due (no memo delivery occurred)",
"QC_Memo_Hyperlink / QC_InfoMem_Hyperlinks (external memo location not configured)",
"Home_County_Area (E56)",
"Home_County_Digits (F56/G56)",
"Home_County_District (H56/I56)",
"FS_75 (J56)",
"Rejection_Reasons_ESAP (K56)",
"Worker_Experience (L56)",
"EPP root-cause classification fields",
"EPP corrective-action tracker fields",
"Corrective Action ownership and status columns",
] as const;
export interface EppHandoffPayload {
adapterStatus: "pending_external_connection";
disclaimer: string;
table: "tblTANF_MA_SNAP_Records";
operation: "upsert";
key: {
QC_Record_Number: string;
Program_Type: "SNAP";
FFY: number;
};
columns: Record<(typeof EPP_VERIFIED_COLUMN_KEYS)[number], string | number>;
missingMapping: readonly string[];
}
/** JSON handoff for tblTANF_MA_SNAP_Records; no external DB write from this app. */
export function buildEppHandoff(review: SnapReview, actor: Actor): EppHandoffPayload {
assertSenior(actor);
assertDownstreamEligible(review);
const ffy = deriveFfyFromSampleMonth(review.sampleMonth);
return {
adapterStatus: "pending_external_connection",
disclaimer: EPP_ADAPTER_DISCLAIMER,
table: "tblTANF_MA_SNAP_Records",
operation: "upsert",
key: {
QC_Record_Number: review.reviewNumber,
Program_Type: "SNAP",
FFY: ffy,
},
columns: {
QC_Record_Number: review.reviewNumber,
Program_Type: "SNAP",
FFY: ffy,
Record_Number: review.caseNumber,
Review_Month: `${review.sampleMonth}-01`,
},
missingMapping: [...EPP_UNVERIFIED_MA_MAPPINGS],
};
}
export interface ErcQueryResult {
cleanReviewCount: number;
errorReviewCount: number;
eligibleReviewCount: number;
ineligibleApprovedCount: number;
note: string;
}
/** The same current-result gates apply to reports and downloadable handoffs. */
export function queryErcOutcomes(reviews: SnapReview[]): ErcQueryResult {
let cleanReviewCount = 0;
let errorReviewCount = 0;
let ineligibleApprovedCount = 0;
for (const review of reviews) {
if (!downstreamGateStatus(review).ok || !["clean", "error"].includes(review.outcome)) {
if (review.memoApproved && !review.errorSummaryReleased) ineligibleApprovedCount++;
continue;
}
if (review.outcome === "clean") cleanReviewCount++;
if (review.outcome === "error") errorReviewCount++;
}
return {
eligibleReviewCount: cleanReviewCount + errorReviewCount,
ineligibleApprovedCount,
cleanReviewCount,
errorReviewCount,
note: "Released reviews only. Root cause comes from the CAO response.",
};
}