diff --git a/repositories/historic_epc/historic_epc_resolver.py b/repositories/historic_epc/historic_epc_resolver.py new file mode 100644 index 000000000..ee6262be4 --- /dev/null +++ b/repositories/historic_epc/historic_epc_resolver.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import Optional + +from datatypes.epc.domain.historic_epc_matching import ( + HistoricEpcMatches, + rank_historic_epc, +) +from repositories.historic_epc.historic_epc_repository import ( + HistoricEpcRepository, + PostcodeNotFound, +) + + +class HistoricEpcResolver: + """Resolves an address to a historic-EPC match by composing the repository + (fetch a postcode's records) with the matcher (score them against the + address). This is where ``address2uprn`` plugs onto the old-EPC backup. + """ + + def __init__(self, repo: HistoricEpcRepository) -> None: + self._repo = repo + + def match(self, user_address: str, postcode: str) -> HistoricEpcMatches: + """All of the postcode's historic EPCs scored against ``user_address``.""" + raise NotImplementedError + + def resolve_uprn( + self, user_address: str, postcode: str + ) -> Optional[tuple[str, str, float]]: + """``(uprn, matched_address, lexiscore)`` for an unambiguous rank-1 + match, else None (no data / ambiguous tie / zero score).""" + raise NotImplementedError diff --git a/tests/repositories/historic_epc/test_historic_epc_resolver.py b/tests/repositories/historic_epc/test_historic_epc_resolver.py new file mode 100644 index 000000000..a22c14b8d --- /dev/null +++ b/tests/repositories/historic_epc/test_historic_epc_resolver.py @@ -0,0 +1,53 @@ +"""HistoricEpcResolver composes the repository with the address matcher. + +Exercised against a fake in-memory repository (a dict of postcode -> records), +so the resolver's composition is tested with no S3 and no network — the matcher +and the repo each have their own tests. +""" + +from __future__ import annotations + +import dataclasses + +from datatypes.epc.domain.historic_epc import HistoricEpc +from datatypes.epc.domain.historic_epc_matching import HistoricEpcMatches +from repositories.historic_epc.historic_epc_repository import HistoricEpcRepository +from repositories.historic_epc.historic_epc_resolver import HistoricEpcResolver + + +def _hist(address: str, uprn: str) -> HistoricEpc: + fields = {f.name: "" for f in dataclasses.fields(HistoricEpc)} + fields["address"] = address + fields["uprn"] = uprn + return HistoricEpc(**fields) + + +class _FakeRepo(HistoricEpcRepository): + def __init__(self, by_postcode: dict[str, list[HistoricEpc]]) -> None: + self._by_postcode = by_postcode + + def get_for_postcode(self, postcode: str) -> list[HistoricEpc]: + key = postcode.upper().replace(" ", "") + return self._by_postcode.get(key, []) + + +def test_match_composes_repo_and_matcher_into_scored_matches(): + # Arrange + repo = _FakeRepo( + { + "AB338AL": [ + _hist("48 GORDON ROAD", "200"), + _hist("47 GORDON ROAD", "100"), + ] + } + ) + resolver = HistoricEpcResolver(repo) + + # Act + result = resolver.match("47 Gordon Road", "AB33 8AL") + + # Assert + assert isinstance(result, HistoricEpcMatches) + assert result.postcode == "AB338AL" + assert len(result.matches) == 2 + assert result.top().record.address == "47 GORDON ROAD"