Model/repositories/uploaded_file/uploaded_file_postgres_repository.py
Khalim Conn-Kowlessar 26857f8df7 Bulk download: match uploaded_files via hubspot_deal_id bridge + add worker observability
A live test returned 'no documents could be packaged' for 91 correctly-resolved
properties. Root cause: no upload source populates uploaded_files.landlord_property_id
(pashub/magic-plan/audit set hubspot_deal_id; ECMK sets hubspot_listing_id), so
matching on landlord_property_id found zero rows.

Match fix: the worker now joins uploaded_files -> hubspot_deal_data (on deal_id) ->
landlord_property_id, taking the property identity from the bridge. The repository
returns a small PropertyDocument read-model instead of the infra ORM row, so the
orchestrator no longer names infrastructure.postgres.* (resolves the leak dancafc
flagged) and the s3_upload_timestamp cast is gone. Coverage is limited to
deal-id-linked sources; listing_id/uprn-only files are a noted follow-up.

Observability: the empty-selection failure now carries stage counts
(selected/matched/planned/skipped) in both the message (-> the worker WARNING log)
and details (-> sub_task.outputs), and the run logs those counts. The next failure
says 'matched 0 documents' instead of failing opaquely.

ADR-0060 + CONTEXT.md updated (matching decision + considered options).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 13:58:46 +00:00

66 lines
2.7 KiB
Python

from __future__ import annotations
from typing import Optional, Sequence
from sqlalchemy import select, text
from sqlmodel import Session, col
from domain.bulk_document_download.property_document import PropertyDocument
from infrastructure.postgres.uploaded_file_table import FileTypeEnum, UploadedFile
class UploadedFilePostgresRepository:
def __init__(self, session: Session) -> None:
self._session = session
def get_latest_by_hubspot_deal_id(
self, hubspot_deal_id: str, file_type: FileTypeEnum
) -> Optional[UploadedFile]:
stmt = (
select(UploadedFile)
.where(col(UploadedFile.hubspot_deal_id) == hubspot_deal_id)
.where(col(UploadedFile.file_type) == file_type.value)
.order_by(col(UploadedFile.s3_upload_timestamp).desc())
.limit(1)
)
return self._session.execute(stmt).scalars().one_or_none() # pyright: ignore[reportDeprecated]
def by_landlord_property_ids(
self, landlord_property_ids: Sequence[str]
) -> list[PropertyDocument]:
"""Every uploaded file held for the given properties (ADR-0060), across
all Document Types, as a small domain read-model.
Files are matched by **`hubspot_deal_id`**, bridged to the property
through `hubspot_deal_data` — `uploaded_files.landlord_property_id` is
unpopulated in practice (no upload source sets it), so the property
identity comes from the deal bridge, not the file row. Coverage is
therefore limited to deal-id-linked sources (pashub, magic plan, audit
generator); files keyed only by `hubspot_listing_id` or `uprn` are not
reached — see the ADR-0060 matching decision.
Latest-per-Document-Type selection and null-type skipping are the
Download Package plan's job, not the query's."""
rows = self._session.connection().execute(
text(
"SELECT h.landlord_property_id, u.file_type,"
" u.s3_file_bucket, u.s3_file_key, u.s3_upload_timestamp"
" FROM uploaded_files u"
" JOIN hubspot_deal_data h ON h.deal_id = u.hubspot_deal_id"
" WHERE h.landlord_property_id = ANY(:lpids)"
),
{"lpids": list(landlord_property_ids)},
)
return [
PropertyDocument(
landlord_property_id=row[0],
document_type=row[1],
s3_bucket=row[2],
s3_key=row[3],
uploaded_at=row[4],
)
for row in rows.all()
]
def insert(self, uploaded_file: UploadedFile) -> None:
self._session.add(uploaded_file)