"""F-1 to H-1B transition date estimates: cap-gap end, cap-gap eligibility, and the H-1B up-to-60-day grace period end. Estimates only. USCIS decides; the grace period is discretionary. Sources: 8 CFR 214.1(l)(2), 8 CFR 214.2(f)(5)(vi), USCIS cap-gap page. """ import argparse from datetime import date, timedelta LABEL = ("Estimate from the dates you entered. Not legal advice. " "USCIS decides; the grace period is discretionary.") 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.") def grace_period_end(last_day_of_employment, validity_end): """Last day of the up-to-60-day grace period. Day 1 is the day after last_day_of_employment, so day 60 is last_day_of_employment + 60 days. Capped at the I-797/I-94 validity end. """ if validity_end < last_day_of_employment: raise ValueError("validity_end is before last_day_of_employment; check your dates") return min(last_day_of_employment + timedelta(days=60), validity_end) def grace_period_report(last_day_of_employment, validity_end, last_day_paid=None): """grace_period_end plus the label. If last_day_paid differs from the last day worked, both dates are shown side by side with the FLAG. Neither is picked.""" report = { "by_last_day_worked": grace_period_end(last_day_of_employment, validity_end), "label": LABEL, } if last_day_paid and last_day_paid != last_day_of_employment: # min(): pay running past the validity end still stops at the validity end. report["by_last_day_paid"] = grace_period_end(min(last_day_paid, validity_end), validity_end) report["flag"] = FLAG return report def cap_gap_end(fiscal_year, approved_validity_start=None): """Outer cap-gap date: April 1 of the fiscal year, or the approved petition's validity start date if that is earlier. FY N runs Oct 1 (N-1) to Sep 30 N, so FY2027 -> 2027-04-01.""" april_1 = date(fiscal_year, 4, 1) return min(april_1, approved_validity_start) if approved_validity_start else april_1 def cap_gap_eligibility(filed_on, opt_ead_end, program_end, change_of_status, cap_subject): """Rough cap-gap check from dates. opt_ead_end: the OPT EAD end date, or the STEM EAD end date if on STEM OPT. Pass None if you never had OPT. program_end: program end date on the I-20. Does not check that the petition was nonfrivolous, based on a selected registration, filed in the filing period, or that you maintained status. """ def result(status, work, reason): return {"status_extended": status, "work_extended": work, "reason": reason, "label": LABEL} if not change_of_status: return result(False, False, "consular processing is not cap-gap eligible") if not cap_subject: return result(False, False, "cap-exempt petitions are not cap-gap eligible") f1_grace_end = max(d for d in (opt_ead_end, program_end) if d) + timedelta(days=60) if filed_on > f1_grace_end: return result(False, False, f"filed after the F-1 60-day grace period ended on {f1_grace_end}") if opt_ead_end and filed_on <= opt_ead_end: return result(True, True, f"filed on or before the EAD end date {opt_ead_end}: " "F-1 status and work authorization may be extended") return result(True, False, f"filed while F-1 status was in effect (grace period runs to {f1_grace_end}) " "but not during a valid EAD: status may be extended, no work authorization") def iso_or_none(text): """CLI helper: 'none' (any case) -> None, otherwise a YYYY-MM-DD date.""" return None if text.strip().lower() == "none" else date.fromisoformat(text) def _main(): iso = date.fromisoformat p = argparse.ArgumentParser(description=LABEL) sub = p.add_subparsers(dest="cmd", required=True) g = sub.add_parser("grace", help="H-1B grace period end") g.add_argument("last_day_worked", type=iso) g.add_argument("validity_end", type=iso, help="I-797 / I-94 end date") g.add_argument("--paid", type=iso, help="last day paid, if different") c = sub.add_parser("capgap-end", help="cap-gap outer date") c.add_argument("fiscal_year", type=int) c.add_argument("validity_start", type=iso, nargs="?", help="approved petition start date") e = sub.add_parser("eligibility", help="rough cap-gap eligibility check") e.add_argument("filed_on", type=iso) e.add_argument("opt_ead_end", type=iso_or_none, help="OPT or STEM EAD end date, or 'none' if you never had OPT") e.add_argument("program_end", type=iso) e.add_argument("--consular", action="store_true", help="petition requests consular processing") e.add_argument("--cap-exempt", action="store_true", help="employer is cap-exempt") a = p.parse_args() if a.cmd == "grace": for k, v in grace_period_report(a.last_day_worked, a.validity_end, a.paid).items(): print(f"{k}: {v}") elif a.cmd == "capgap-end": print(f"cap_gap_end: {cap_gap_end(a.fiscal_year, a.validity_start)}") print(f"label: {LABEL}") else: r = cap_gap_eligibility(a.filed_on, a.opt_ead_end, a.program_end, change_of_status=not a.consular, cap_subject=not a.cap_exempt) for k, v in r.items(): print(f"{k}: {v}") if __name__ == "__main__": _main()