from decimal import Decimal from pathlib import Path import pytest from payslip_ingest.parsers.p60 import P60ParserError, parse_p60 FIXTURES = Path(__file__).parent / "fixtures" def _load(name: str) -> str: return (FIXTURES / name).read_text(encoding="utf-8") def test_parses_meta_uk_p60_2024_25() -> None: result = parse_p60(_load("meta_uk_p60_2024_25.txt")) assert result.tax_year == "2024/25" assert result.employer == "Facebook UK Limited" assert result.employer_paye_ref == "120/FA12345" assert result.gross_pay == Decimal("232630.34") assert result.income_tax == Decimal("95820.11") assert result.national_insurance == Decimal("5172.40") assert result.student_loan == Decimal("0.00") assert result.tax_code == "1257L" def test_parse_p60_raises_on_non_p60_text() -> None: with pytest.raises(P60ParserError, match="does not look like a P60"): parse_p60("Payslip for March 2025\nGross: £1000\n") def test_parse_p60_raises_on_empty_text() -> None: with pytest.raises(P60ParserError): parse_p60("") def test_parse_p60_raises_without_tax_year_anchor() -> None: with pytest.raises(P60ParserError, match="Tax year"): parse_p60("P60\nSome other content without the required anchor\n") def test_parse_p60_handles_old_facebook_uk_ltd_spelling() -> None: """Pre-2022 P60s list the employer as `Facebook UK Ltd` (no `Limited`).""" text = _load("meta_uk_p60_2024_25.txt").replace("Facebook UK Limited", "Facebook UK Ltd") result = parse_p60(text) assert result.employer == "Facebook UK Ltd" def test_parse_p60_student_loan_missing_is_none() -> None: """P60s for years without student-loan deductions omit that line entirely.""" text = _load("meta_uk_p60_2024_25.txt") # Strip the Student Loan line (simulating a year pre-loan). stripped = "\n".join(line for line in text.splitlines() if "Student Loan" not in line) result = parse_p60(stripped) assert result.student_loan is None def test_parse_p60_tax_code_missing_is_none() -> None: """Some historical P60s may not print a `Final tax code` line.""" text = _load("meta_uk_p60_2024_25.txt").replace("Final tax code", "XXX") result = parse_p60(text) assert result.tax_code is None def test_parse_p60_sums_ni_across_letter_bands() -> None: """Employees who cross NI letter bands mid-year get one row per letter.""" text = _load("meta_uk_p60_2024_25.txt") # Append a second NI letter row — same shape as the A row in the fixture. extra = "C £6,396.00 £47,268.00 £1,000.00\n" augmented = text + "\n" + extra result = parse_p60(augmented) # 5172.40 (letter A, in fixture) + 1000.00 (letter C, appended) assert result.national_insurance == Decimal("6172.40")