// F-1 to H-1B transition date estimates. Same rules as transition_dates.py. // Dates are "YYYY-MM-DD" strings, handled in UTC so time zones can't shift a day. // Estimates only. USCIS decides; the grace period is discretionary. const LABEL = "Estimate from the dates you entered. Not legal advice. " + "USCIS decides; the grace period is discretionary."; const FLAG = "FLAG: your last day paid differs from your last day worked. " + "USCIS's archived page says the period starts the day after " + "termination of employment, typically determined by the last day " + "for which a salary or wage is paid. Ask an immigration attorney " + "which date applies to you."; function parse(s) { if (typeof s !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(s)) { throw new Error(`Expected a YYYY-MM-DD date, got ${s}`); } const d = new Date(s + "T00:00:00Z"); if (isNaN(d) || d.toISOString().slice(0, 10) !== s) throw new Error(`Invalid date ${s}`); return d; } function addDays(s, n) { const d = parse(s); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10); } // YYYY-MM-DD strings sort the same as the dates they name. const minDate = (a, b) => (parse(a), parse(b), a < b ? a : b); const maxDate = (a, b) => (parse(a), parse(b), a > b ? a : b); // Day 1 = the day after lastDayOfEmployment; capped at the I-797/I-94 validity end. function gracePeriodEnd(lastDayOfEmployment, validityEnd) { if (validityEnd < lastDayOfEmployment) { throw new Error("validityEnd is before lastDayOfEmployment; check your dates"); } return minDate(addDays(lastDayOfEmployment, 60), validityEnd); } function gracePeriodReport(lastDayOfEmployment, validityEnd, lastDayPaid = null) { const report = { byLastDayWorked: gracePeriodEnd(lastDayOfEmployment, validityEnd), label: LABEL }; if (lastDayPaid && lastDayPaid !== lastDayOfEmployment) { report.byLastDayPaid = gracePeriodEnd(minDate(lastDayPaid, validityEnd), validityEnd); report.flag = FLAG; } return report; } // FY N runs Oct 1 (N-1) to Sep 30 N, so FY2027 -> "2027-04-01". function capGapEnd(fiscalYear, approvedValidityStart = null) { if (!Number.isInteger(fiscalYear)) throw new Error("fiscalYear must be an integer"); const april1 = `${fiscalYear}-04-01`; return approvedValidityStart ? minDate(april1, approvedValidityStart) : april1; } // optEadEnd: OPT EAD end, or STEM EAD end if on STEM OPT; null if you never had OPT. function capGapEligibility({ filedOn, optEadEnd = null, programEnd, changeOfStatus, capSubject }) { parse(filedOn); const result = (statusExtended, workExtended, reason) => ({ statusExtended, workExtended, reason, label: LABEL }); if (!changeOfStatus) return result(false, false, "consular processing is not cap-gap eligible"); if (!capSubject) return result(false, false, "cap-exempt petitions are not cap-gap eligible"); const f1GraceEnd = addDays(optEadEnd ? maxDate(optEadEnd, programEnd) : programEnd, 60); if (filedOn > f1GraceEnd) { return result(false, false, `filed after the F-1 60-day grace period ended on ${f1GraceEnd}`); } if (optEadEnd && filedOn <= optEadEnd) { return result(true, true, `filed on or before the EAD end date ${optEadEnd}: ` + "F-1 status and work authorization may be extended"); } return result(true, false, `filed while F-1 status was in effect (grace period runs to ${f1GraceEnd}) ` + "but not during a valid EAD: status may be extended, no work authorization"); } module.exports = { LABEL, FLAG, gracePeriodEnd, gracePeriodReport, capGapEnd, capGapEligibility };