Commit graph

7955 commits

Author SHA1 Message Date
KhalimCK
22db9c43f3
Merge pull request #1497 from Hestia-Homes/feature/small-roof-solar-min-array
Sub-Ladder PV Configurations: small roofs get the array their caps allow (ADR-0058)
2026-07-08 12:50:41 +01:00
KhalimCK
bd24a789b9
Merge pull request #1498 from Hestia-Homes/feature/bulk-document-download
Bulk Document Download: emailed ZIP of a property set's documents (ADR-0059/0060)
2026-07-08 12:49:46 +01:00
Khalim Conn-Kowlessar
90fe2914e0 Select documents by HubSpot project_code(s), unioned with hand-picked property_ids
The 'all properties' selection resolved against property.portfolio_id, but a
portfolio spans multiple HubSpot projects — so 'all' pulled the whole portfolio,
not the project the user was looking at. The project↔property grain lives on
hubspot_deal_data, not property.

task.inputs is now {project_codes?: str[], property_ids?: int[], portfolio_id?}:
the route resolves the distinct landlord_property_id set as the union of every
property in the named project_codes (from hubspot_deal_data) and the hand-picked
property_ids; portfolio_id is optional and only names the package. Drops the
portfolio-scoped select_all. Cap now applies to the resolved set size.

ADR-0060, CONTEXT.md and the request schema updated to match. New router tests
cover project-code resolution, the union+dedup, null landlord_property_id drop,
and the empty-project rejection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 11:42:17 +00:00
Daniel Roth
86e6b83e57
Merge pull request #1500 from Hestia-Homes/feature/abri-api-integration
Abri API integration: temporary project code variable for testing
2026-07-08 12:30:10 +01:00
Khalim Conn-Kowlessar
9ec7987e97 PR review: best-effort read path, true per-member streaming, race-safe 409, local email 🟩
Addresses PR #1498 review:
- A per-file read failure (missing/deleted object, or a bucket the role can't
  reach) is now a SkippedDocument(reason=unreadable), not a whole-run abort;
  an all-unreadable selection still fails (no empty package). Best-effort on
  the read path (ADR-0060).
- Each ZIP member is streamed to a temp file (S3DocumentDownloader.download,
  boto download_file) and added from disk, so a single multi-GB member never
  hits the heap — the 'never held whole in memory' claim is now true.
- The 409 double-submit guard is DB-arbitrated: the sub_task insert is
  conditional on the task having none, so a race creates only one.
- Local env gets a placeholder recipient email so the route is exercisable.
- PackageEntry carries landlord_property_id so a read failure can be reported.
- TODO noting the UploadedFile.s3_upload_timestamp typing fix.
Two new tests: per-file read failure skips-and-reports; address fallback flows
to the folder name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 11:08:46 +00:00
Khalim Conn-Kowlessar
0b52a28808 Read the property selection from task.inputs; route takes only task_id 🟩
Per PR review: the FE writes the selection config
({portfolio_id, property_ids?, select_all?}) into the FE-owned task.inputs and
passes only task_id, so a large hand-picked selection never travels in an HTTP
body. The route reads task.inputs, resolves to landlord_property_ids, caps, and
pins the recipe onto sub_task.inputs as before. Declares the FE-owned inputs
column on the TaskRow mirror so the backend can read it and the test schema
builds it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 10:41:14 +00:00
Daniel Roth
8e880eea6f Expose the Abri test project-code override to the scraper lambda 🟩
Wires TEST_ABRI_PROJECT_CODE_OVERRIDE from an optional deploy-time tfvar
through to the hubspot_deal_etl lambda env; unset keeps production behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:28:48 +00:00
Daniel Roth
002f782905 Fire Abri flows on a test project code via an override env var 🟩
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:25:45 +00:00
Daniel Roth
3ff99619ac Fire Abri flows on a test project code via an override env var 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:24:54 +00:00
Daniel Roth
9806f94c7f Merge branch 'main' into feature/abri-api-integration 2026-07-08 10:18:52 +00:00
Daniel Roth
b6e49de628
Merge pull request #1499 from Hestia-Homes/feature/abri-api-integration
Abri API: send request as XML
2026-07-08 11:18:36 +01:00
Daniel Roth
fe167f077f Declare the XML content type the Abri relay requires 🟩
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:08:06 +00:00
Daniel Roth
0d82f9e660 Declare the XML content type the Abri relay requires 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 10:07:04 +00:00
Khalim Conn-Kowlessar
e4f14ed883 Type the requester-email extraction cleanly 🟪
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 10:05:11 +00:00
Khalim Conn-Kowlessar
b6746cc24a Terraform + CI for bulk_document_download: exports bucket, 10GB /tmp, SES SMTP, wiring 🟩
New lambda_with_sqs consumer (timeout 900, memory 3008, ephemeral_storage 10240
for multi-GB ZIPs); dedicated retrofit-document-exports bucket (no lifecycle on
DATA_BUCKET); IAM to read source buckets + write/presign-read the exports
bucket; SES SMTP creds baked from Secrets Manager (no ses:* on the role). Adds
ephemeral_storage_size knob to the shared lambda modules (default 512, backward
compatible). Wires the queue url+arn into fast-api and orders the CI jobs
(ADR-0055).

NOTE: terraform is drafted, not validated (no AWS/terraform in the dev env).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 10:03:50 +00:00
Khalim Conn-Kowlessar
3c00e0ef00 FastAPI trigger route: POST /v1/documents/bulk-download 🟩
Resolves the authenticated requester's email (ADR-0059), resolves + caps the
property selection, pins the recipe (landlord_property_ids, recipient_email,
package_name) onto one pre-created sub_task (raw SQL, dodging the mirror
double-registration), and enqueues one message to the worker. Refuses
double-submits (409) and oversized selections (400).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:59:02 +00:00
Daniel Roth
4a1fdd87a8 Merge branch 'main' into feature/abri-api-integration 2026-07-08 09:53:45 +00:00
Daniel Roth
8b7d32433e
Merge pull request #1492 from Hestia-Homes/feature/abri-api-integration
Abri API integration: abandon job
2026-07-08 10:53:21 +01:00
Khalim Conn-Kowlessar
6db6bbd98f ADR-0060: dedicated exports bucket + multi-GB streaming (drop DATA_BUCKET lifecycle) 🟪
Per review: packages go to a separate DOCUMENT_EXPORTS_BUCKET (not DATA_BUCKET),
no 7-day lifecycle imposed on the shared data bucket; packages can be several GB
so they stream via /tmp + multipart upload with raised ephemeral storage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:52:43 +00:00
Khalim Conn-Kowlessar
8c0b015923 bulk_document_download Lambda: attach-mode handler, trigger body, packaging 🟩
Reads the recipe from sub_task.inputs, assembles the Download Package via the
orchestrator (source docs from each row's bucket -> ZIP -> the dedicated
DOCUMENT_EXPORTS_BUCKET), returns the presigned URL + skip summary for
sub_task.outputs. Dockerfile/requirements mirror bulk_upload_finaliser.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:51:51 +00:00
Khalim Conn-Kowlessar
a36aa49c2b Stream the Download Package ZIP via /tmp and multipart upload 🟪
Multi-GB packages are no longer held in memory: each source file is read,
written into the on-disk ZIP, and released; the archive is streamed up from
disk. Behaviour unchanged (moto tests green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:51:26 +00:00
Khalim Conn-Kowlessar
879581bb32 Stream a local file to S3 with managed multipart upload 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:50:34 +00:00
Khalim Conn-Kowlessar
79c8890d07 Stream a local file to S3 with managed multipart upload 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:50:10 +00:00
Khalim Conn-Kowlessar
0387f4a897 Resolve a property's display address from its landlord_property_id 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:45:13 +00:00
Khalim Conn-Kowlessar
456d4dffe7 Resolve a property's display address from its landlord_property_id 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:44:36 +00:00
Khalim Conn-Kowlessar
79fa46d97c Read document bytes from an arbitrary source bucket 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:42:37 +00:00
Khalim Conn-Kowlessar
0a09b56425 Read document bytes from an arbitrary source bucket 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:42:05 +00:00
Khalim Conn-Kowlessar
77e7b0fda9 Package the good documents and report the skipped ones 🟩
Pins the best-effort contract (ADR-0060) delivered with the tracer — no red
phase; discriminating: a strict impl would fail the run or omit the report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:33:58 +00:00
Khalim Conn-Kowlessar
59ef8a12ab Treat a selection with no documents as a recorded failure 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:33:25 +00:00
Khalim Conn-Kowlessar
73b21e8d64 Treat a selection with no documents as a recorded failure 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:32:37 +00:00
Khalim Conn-Kowlessar
e7181b7d1f Build, upload and email a Download Package for a property set 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:31:49 +00:00
Khalim Conn-Kowlessar
115f506353 Build, upload and email a Download Package for a property set 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:30:38 +00:00
Khalim Conn-Kowlessar
5151fea9b2 STARTTLS and authenticate before sending the email 🟩
Pins the auth handshake delivered with the tracer green (no red phase);
discriminating: an unauthenticated/cleartext send would be rejected by SES.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:27:31 +00:00
Khalim Conn-Kowlessar
74732c6e5a Send a document-download email to the requesting user over SES SMTP 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:27:09 +00:00
Khalim Conn-Kowlessar
7466b78e54 Send a document-download email to the requesting user over SES SMTP 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:25:56 +00:00
Khalim Conn-Kowlessar
135dd9abda Fetch all uploaded files for a set of properties by landlord_property_id 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:23:47 +00:00
Khalim Conn-Kowlessar
910f7cc06c Fetch all uploaded files for a set of properties by landlord_property_id 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:23:12 +00:00
Khalim Conn-Kowlessar
da3e0cd4b4 Generate a presigned GET URL for an S3 object 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:18:54 +00:00
Khalim Conn-Kowlessar
ba13300ee1 Generate a presigned GET URL for an S3 object 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:18:10 +00:00
Khalim Conn-Kowlessar
e906c8fc98 Scope latest-per-Document-Type per property, each in its own folder 🟩
Pins behavior delivered with the tracer slice (the group key is
(landlord_property_id, document_type)) — no red phase; discriminating: a
global-by-type dedupe would drop one property's file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:15:44 +00:00
Khalim Conn-Kowlessar
b89b9fe944 Name the packaged file by Document Type with the original extension 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:07:25 +00:00
Khalim Conn-Kowlessar
63933db529 Name the packaged file by Document Type with the original extension 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:06:51 +00:00
Khalim Conn-Kowlessar
bd928828e3 Skip and report a document with no Document Type 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:06:26 +00:00
Khalim Conn-Kowlessar
34c48d4adc Skip and report a document with no Document Type 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:05:44 +00:00
Khalim Conn-Kowlessar
810e910961 Keep only the latest file of a Document Type in each property's folder 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:04:58 +00:00
Khalim Conn-Kowlessar
28b7b43372 Keep only the latest file of a Document Type in each property's folder 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:04:19 +00:00
Khalim Conn-Kowlessar
6d001d650e ADR-0059/0060: Bulk Document Download design — SES-SMTP email sender + capped best-effort Download Package 🟪
Grilled 2026-07-08. Locks: app-owned-task + attach-mode lane (ADR-0055);
FastAPI route resolves the requesting user (dbId -> UserModel.email) and pins
it + the resolved property_id set into tasks.inputs; one capped Download
Package = one sub_task; landlord_property_id matching (property_id a future
gap); address-named folders (hubspot-enriched), latest per Document Type,
null-type skipped+reported; best-effort (fail only on infra / wholly-empty);
URL to sub_task.outputs + email; DATA_BUCKET/bulk-downloads/ ~7-day lifecycle.
Backend-sent email via a repositories/email port + infrastructure/email SES-SMTP
adapter. Glossary: Bulk Document Download, Download Package, Document Type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 08:54:43 +00:00
Daniel Roth
84d0e1e0d5 Date an abandonment to the deal's own dates, not now() 🟩
date_abandoned now resolves to the third failed attempt's confirmed
survey date, falling back to its last submission date, rather than the
processing date. This dates the OpenHousing cancellation to when the
job actually lapsed even if the trigger message lags or redelivers.

- domain: abandonment_date() encodes the confirmed-survey -> last-
  submission fallback
- DealAbandonment carries both dates; abandon_job raises
  AbandonmentDateUnknownError when a deal has neither
- last_submission_date now flows through the trigger message and request
- the injected clock is dropped: nothing reads now() any more

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 08:51:39 +00:00
Khalim Conn-Kowlessar
fbedf4541b A small roof derives Sub-Ladder rungs below Google's smallest config 🟪
North-drop now runs once: the selection computes the usable (north-dropped)
ladder, feasibility filters it, and Sub-Ladder derivation consumes it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:10:37 +00:00
Khalim Conn-Kowlessar
5d0a7751e6 A small-roof house gets Sub-Ladder solar Options instead of nothing 🟩
End-to-end pin at the generator seam (recommend_solar) — passed on first
run since option construction is unchanged by design (ADR-0058).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:09:18 +00:00