diff --git a/domain/bulk_document_download/__init__.py b/domain/bulk_document_download/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/domain/bulk_document_download/package_plan.py b/domain/bulk_document_download/package_plan.py new file mode 100644 index 000000000..6d08f6171 --- /dev/null +++ b/domain/bulk_document_download/package_plan.py @@ -0,0 +1,66 @@ +"""The Download Package plan (ADR-0060). + +Pure logic: given the documents resolved for a set of properties, decide what +goes where in the ZIP — one folder per property, the latest file of each +Document Type — and which rows are skipped and reported. No I/O: the +orchestrator does the S3/DB work; this module only lays out the archive. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Optional, Sequence + + +@dataclass(frozen=True) +class ResolvedDocument: + """One `uploaded_files` row resolved for packaging: its property identity + (matched by `landlord_property_id`, ADR-0060), the display address enriched + from the hubspot deals data, its Document Type (`file_type`, nullable), the + S3 location to read it from, and when it was uploaded (for latest-per-type). + """ + + landlord_property_id: str + address: str + document_type: Optional[str] + s3_bucket: str + s3_key: str + uploaded_at: datetime + + +@dataclass(frozen=True) +class PackageEntry: + """One file to place in the Download Package: where it lands in the ZIP and + the S3 object to stream in from.""" + + zip_path: str + s3_bucket: str + s3_key: str + + +@dataclass(frozen=True) +class SkippedDocument: + """A resolved row left out of the package, with the reason (surfaced in the + run's `sub_task.outputs`, ADR-0060).""" + + landlord_property_id: str + s3_key: str + reason: str + + +@dataclass(frozen=True) +class DownloadPackagePlan: + """The laid-out archive: the files to include and the rows skipped.""" + + entries: tuple[PackageEntry, ...] + skipped: tuple[SkippedDocument, ...] + + +def plan_download_package( + documents: Sequence[ResolvedDocument], +) -> DownloadPackagePlan: + """Lay out a Download Package from resolved documents (ADR-0060): one folder + per property, the latest file of each Document Type, null-Document-Type rows + skipped and reported.""" + raise NotImplementedError diff --git a/tests/domain/bulk_document_download/__init__.py b/tests/domain/bulk_document_download/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/domain/bulk_document_download/test_package_plan.py b/tests/domain/bulk_document_download/test_package_plan.py new file mode 100644 index 000000000..2271aefd0 --- /dev/null +++ b/tests/domain/bulk_document_download/test_package_plan.py @@ -0,0 +1,48 @@ +"""The Download Package plan (ADR-0060) — pure layout of the ZIP from resolved +documents: one folder per property, latest file of each Document Type, +null-Document-Type rows skipped and reported.""" + +from datetime import datetime, timezone + +from domain.bulk_document_download.package_plan import ( + ResolvedDocument, + plan_download_package, +) + + +def _doc( + *, + landlord_property_id: str = "LP1", + address: str = "12 Oak Street", + document_type: str = "site_note", + s3_key: str = "a.pdf", + uploaded_at: datetime = datetime(2026, 1, 1, tzinfo=timezone.utc), +) -> ResolvedDocument: + return ResolvedDocument( + landlord_property_id=landlord_property_id, + address=address, + document_type=document_type, + s3_bucket="src-bucket", + s3_key=s3_key, + uploaded_at=uploaded_at, + ) + + +def test_keeps_only_the_latest_file_of_a_document_type_in_the_property_folder() -> None: + # Arrange — two site_note files for the same property, uploaded at + # different times; only the newest should be packaged. + older = _doc( + s3_key="old.pdf", uploaded_at=datetime(2026, 1, 1, tzinfo=timezone.utc) + ) + newer = _doc( + s3_key="new.pdf", uploaded_at=datetime(2026, 6, 1, tzinfo=timezone.utc) + ) + + # Act + plan = plan_download_package([older, newer]) + + # Assert — one entry, the newer file, foldered under the property. + assert len(plan.entries) == 1 + entry = plan.entries[0] + assert entry.s3_key == "new.pdf" + assert entry.zip_path.startswith("12 Oak Street (LP1)/")