"""Parse an Elmhurst *recommendation* Summary PDF into an EpcPropertyData. The Modelling cascade pins use Elmhurst's own before/after measure re-lodgements as deterministic test vectors: each measure folder under `sap worksheets/Recommendations Elmhurst Files/` holds a `before` Summary (the baseline cert) and an `after` Summary (the same cert re-lodged with the measure applied). Applying the matching Recommendation Generator's overlay to the parsed `before` must reproduce the calculator's score on the parsed `after` at delta 0 — proving the overlay is the exact field change Elmhurst made. This routes the Summary PDF through the same extractor + mapper chain the worksheet e2e fixtures use (`_elmhurst_worksheet_001431.build_epc`), NOT the Textract `parse_site_notes_pdf` path — that path has an unrelated window extraction bug on cert 001431. The before/after Summaries are mirrored into `tests/domain/modelling/fixtures/` so the pins do not depend on the unstaged workspace. """ from __future__ import annotations import re import subprocess from pathlib import Path from typing import Final from backend.documents_parser.elmhurst_extractor import ElmhurstSiteNotesExtractor from datatypes.epc.domain.epc_property_data import EpcPropertyData from datatypes.epc.domain.mapper import EpcPropertyDataMapper _FIXTURES_DIR: Final[Path] = Path(__file__).resolve().parent / "fixtures" def _summary_pdf_to_textract_style_pages(pdf_path: Path) -> list[str]: """Convert a Summary PDF into the per-page text format the `ElmhurstSiteNotesExtractor` expects (label\\nvalue sequences). Mirror of the helper in `_elmhurst_worksheet_001431.py`: `pdftotext -layout` preserves the spatial label/value pairing on each line; we split on 2+ spaces to surface the tokens, then rejoin newline-delimited. """ info: str = subprocess.run( ["pdfinfo", str(pdf_path)], capture_output=True, text=True, check=True, ).stdout match = re.search(r"Pages:\s+(\d+)", info) if match is None: raise RuntimeError(f"Could not parse page count from {pdf_path}") page_count = int(match.group(1)) pages: list[str] = [] for i in range(1, page_count + 1): layout: str = subprocess.run( [ "pdftotext", "-layout", "-f", str(i), "-l", str(i), str(pdf_path), "-", ], capture_output=True, text=True, check=True, ).stdout tokens: list[str] = [] for line in layout.splitlines(): if not line.strip(): tokens.append("") continue parts = [p for p in re.split(r"\s{2,}", line.strip()) if p] tokens.extend(parts) pages.append("\n".join(tokens)) return pages def parse_recommendation_summary(fixture_name: str) -> EpcPropertyData: """Parse a before/after recommendation Summary fixture (by file name in `tests/domain/modelling/fixtures/`) into an EpcPropertyData.""" pdf_path: Path = _FIXTURES_DIR / fixture_name pages: list[str] = _summary_pdf_to_textract_style_pages(pdf_path) site_notes = ElmhurstSiteNotesExtractor(pages).extract() return EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)