Commit graph

560 commits

Author SHA1 Message Date
Khalim Conn-Kowlessar
0890b59b56 Persist alternative-wall u_value / thickness / is_basement 🟩
SapAlternativeWall.u_value (heat_transmission.py:1616), .wall_thickness_mm
(:1645) and .is_basement (:1618) were read by the calculator but had no column,
so they dropped on save. Latent today (0 gov-API certs lodge them — live only on
the Elmhurst/site-notes path), but is_basement is the nastiest shape: it does not
null out, it FLIPS meaning (True -> None -> is_basement_wall reads False),
silently switching off the RdSAP §5.17 / Table 23 basement-wall U path.

Add alt_wall_{1,2}_{u_value,thickness_mm,is_basement} to EpcBuildingPartModel,
write in from_domain, reconstruct in _to_alt_wall, and drop the three
_UNPERSISTED_ALLOWLIST entries so the structural guard enforces them. is_basement
is nullable (None "not stated" != False "not a basement"; a NOT NULL DEFAULT
false re-enables the code-6 heuristic — same reasoning as #1661's wall_is_basement).

Companion assessment-model migration: six nullable columns on epc_building_part
(deploy gate). Round-trip + field-coverage guard green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:31:43 +00:00
Khalim Conn-Kowlessar
9d2ac59ed2 Store uprn as BIGINT so real 12-digit UPRNs persist 🟩
EpcPropertyModel.uprn was `Optional[int]`, which SQLModel maps to INTEGER, so a
real 11/12-digit UPRN (e.g. 10000452538) overflowed int32 and hard-failed the
insert with NumericValueOutOfRange — a failure on EVERY real-cert save, masked
because the round-trip fixtures use small fake UPRNs. The FE `property.uprn` is
already bigint; pin the column to BigInteger so ours matches (backend-only, no
migration). Verified: uprn 10000452538 saves + reloads.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 16:12:18 +00:00
Khalim Conn-Kowlessar
6d2982f826 Persist cylinder_heat_loss so it survives the DB round-trip 🟩
SapHeating.cylinder_heat_loss is read by the calculator (cert_to_inputs §4
cylinder loss) but had no column, so it was silently dropped on save: a dwelling
reloaded from Postgres lost its lodged cylinder loss and fell back to the Table
2b age-band default — worst case -15.53 SAP over the corpus round-trip (#1665).

Add heating_cylinder_heat_loss (nullable double precision) to EpcPropertyModel,
write it in from_domain and reconstruct it in the SapHeating compose path,
mirroring heating_cylinder_volume_measured_l. Remove the now-false
_UNPERSISTED_ALLOWLIST entry ("dormant — not read by the calculator") so the
ADR structural guard enforces it. Verified: worst cylinder cert 10091630692
round-trip -15.53 -> 0.0000.

Companion assessment-model migration (heating_cylinder_heat_loss column) must
land + be applied per-environment before the backend deploys (deploy gate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 15:51:31 +00:00
Daniel Roth
ae1da26558 Move the PasHub fetcher into the DDD layer structure 🟪
The service was the last one living wholly under backend/. It now follows
the same layering as abri and the other newer services:

  domain/pashub_fetcher/          core file classification, subfolders
  infrastructure/pashub_fetcher/  PasHub client, token getter, wire DTOs
  orchestration/                  PashubFetcherOrchestrator (was PashubService)
  applications/pashub_fetcher/    lambda handler, trigger request, dev tooling

core_files.py is split along the layer boundary: the domain module keeps the
filename/evidence-category classification rules and no longer imports
infrastructure.postgres, while the CoreFiles -> FileTypeEnum translation moves
to infrastructure/pashub_fetcher/core_file_types.py.

Tests move into the tests/ tree by layer. Note this puts them in the only
suite CI currently runs (unit_tests.yml is disabled), so these 73 tests now
execute on PRs for the first time; they were previously reachable only via
the legacy pytest.ini testpaths.

sharepoint_renamer's image now copies just domain/pashub_fetcher/ rather than
the whole service, since SharepointSubfolders is all it needed.

Behaviour is unchanged. tests/ goes 9927 -> 10000 passed (+73, exactly the
tests that moved in); the legacy suite keeps its same 17 pre-existing failures
and 11 errors. tests/test_lambda_packaging.py confirms both changed
Dockerfiles still copy their handler's full import closure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 13:35:54 +00:00
KhalimCK
69d16596c9
Merge pull request #1661 from Hestia-Homes/fix/persist-rafter-insulation-thickness
Persist the measured at-rafters roof insulation thickness
2026-07-22 09:31:16 +01:00
Khalim Conn-Kowlessar
42ed6f8e46 Persist the explicit basement-wall determination 🟩
RdSAP 10 §5.17 / Table 23. `wall_is_basement` is not a lodged U-value — it is a
disambiguation flag selecting WHICH RdSAP default applies, so unlike
wall/roof/floor_u_value it must survive the round-trip.

Code 6 is canonically SYSTEM-BUILT and the gov-API path infers "basement" from a
code-6 heuristic; the Elmhurst site-notes mapper sets wall_is_basement=False
precisely to defeat it. Dropping the field inverted the answer — a system-built
wall came back as a basement wall, dragging the whole ground floor onto the
Table 23 basement-floor U-value via `has_basement`.

Declared on no RdSAP schema, so it never arrives via the gov API; the earlier
corpus scan showing 0 occurrences measured the wrong population. The Elmhurst
site-notes path sets it and those EPCs are persisted
(scripts/wchg_elmhurst_ingest.py).

Nullable is load-bearing: None = "not stated, use the heuristic", False =
"explicitly system-built, do not".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 17:58:26 +00:00
Khalim Conn-Kowlessar
d867838330 Persist the measured at-rafters roof insulation thickness 🟩
Add `rafter_insulation_thickness` to `EpcBuildingPartModel` (JSONB, matching the
sibling thickness columns so Union[str, int] survives) and wire it through
`from_domain` / `_to_building_part`.

Property 749719 lodges "250mm" -> Table 16 col (2) U 0.23. Dropped, the roof
billed at U 2.30 — a 10x error worth -9.8 SAP (lodged 73, calculated 63.2) that
moved the dwelling a whole band, C -> D, and drew a 9.63-point loft-insulation
recommendation on a roof rated 4/5 by the assessor.

The sibling wall/roof/floor_u_value and wall_is_basement fields have the same
drop but are deliberately NOT persisted: they are full-SAP artefacts and we
re-model as RdSAP, which uses default U-values. Their allow-list entries are
re-justified rather than removed — the previous "not read by the calculator" was
stale and is what hid this drop.

Requires the Drizzle migration in Hestia-Homes/assessment-model to land FIRST;
without the column, EPC reads fail outright.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 17:53:17 +00:00
Daniel Roth
edd4ec00de Make the OpenHousing resource mandatory, dropping the default fallback
The deal's third_party_surveyor_identifier is now required by the log
and amend flows: the trigger contract rejects messages without it
(blank treated as missing, since a blank resource makes OpenHousing
silently drop the appointment), and LogJobRequest/AmendJobRequest carry
it as a required field. The ABRI_RELAY_DEFAULT_RESOURCE fallback and
its config/terraform/workflow wiring are removed as dead code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 13:13:28 +00:00
Daniel Roth
c8fe4585bf Send the deal's third-party surveyor as the OpenHousing resource
Carry third_party_surveyor_identifier on the Abri trigger message and
through dispatch into the log and amend flows, booking the job against
that resource. LogJobRequest gains the optional resource field
AmendJobRequest already had; the client falls back to the configured
default surveyor when the deal names no third-party resource, so
existing deals behave unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 13:00:47 +00:00
KhalimCK
660e42b920
Merge pull request #1526 from Hestia-Homes/feature/fabric-first-scenario
Fabric-first scenario constraint: two-phase optimisation (ADR-0061)
2026-07-10 12:48:06 +01:00
Jun-te Kim
121b3d991d
Merge pull request #1533 from Hestia-Homes/fix/epc-schema-allowlist
Unsupported EPC schema versions skip with a warning instead of crashing
2026-07-10 12:43:40 +01:00
Jun-te Kim
b540cf1ca2 Unsupported EPC schema versions are skipped with a warning, not a crash
An unrecognised schema_type used to raise ValueError and abort the whole
call; now from_api_response logs a warning and returns None so one
unmapped cert doesn't break a batch. Schema 15.0 already has a mapper
(PR #1531) so it's unaffected; only genuinely unmapped versions skip.
2026-07-10 11:17:07 +00:00
Daniel Roth
f8b9d82200 Always send a bookable resource on Abri amend_job
OpenHousing logs a job but silently drops the appointment when no bookable
resource is sent, so the survey date never lands. log_job already sends the
configured default_resource; amend_job omitted the parameter unless the
request carried an explicit surveyor. Default amend's resource to the
configured surveyor too, and fail loudly at config load on a blank
ABRI_RELAY_DEFAULT_RESOURCE so the misconfiguration can't ship an empty
resource again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:37:35 +00:00
Daniel Roth
7997d88375 minor updates following Abri feedback 2026-07-09 16:27:50 +00:00
Khalim Conn-Kowlessar
e75798b281 Scenario carries its Fabric First flag from the scenario table 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:44:31 +00:00
Khalim Conn-Kowlessar
caa0847b70 Scenario carries its Fabric First flag from the scenario table 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:44:31 +00:00
Khalim Conn-Kowlessar
91db972d48 Bulk download: readable email, deal-name folders, visible worker logs
First successful live run surfaced three issues:

1. Email looked rubbish (a giant raw presigned URL). Now sends a proper HTML
   email with a 'Download documents' button plus a plain-text fallback, and a
   summary (N documents across M properties, expiry). Email delivery is now
   best-effort: a transport failure no longer loses an already-built package
   (the link is still on sub_task.outputs), and the SMTP connect has a 30s
   timeout so an unreachable SES endpoint fails fast instead of hanging to the
   900s Lambda timeout.

2. Every folder was 'address unavailable (...)': the resolver read property.address,
   but these are HubSpot deals with no property row. It now uses the deal's
   dealname from hubspot_deal_data.

3. No logs / no idea why a run took ~9 minutes: the worker's INFO logs were
   dropped (Lambda root logger defaults to WARNING). The handler now raises the
   level, and the orchestrator logs per-phase timing and volume (gather+plan,
   packaged N files / X MB, upload, email, total) so the slow phase is visible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 15:02:38 +00: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
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
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
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
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
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
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
Daniel Roth
157f9fc4b4 Surface both canceljob failure shapes as verbatim rejections 🟩
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:34:35 +00:00
Daniel Roth
03f0f4d490 Abandon a job through the relay canceljob route 🟩
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:32:13 +00:00
Daniel Roth
c206a76b22 Abandon a job through the relay canceljob route 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:30:24 +00:00
KhalimCK
9ff4de0563
Merge pull request #1481 from Hestia-Homes/feature/modelling-trigger-run
Modelling Run Distributor: POST /v1/modelling/trigger-run
2026-07-07 14:36:45 +01:00
Khalim Conn-Kowlessar
07190fc332 Legacy property columns no longer resolve type or built form 🟩
ADR-0056 amended: override and EPC own type/form facts; a property with
neither is Unknown whatever the legacy columns say. Mirror columns removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 12:52:11 +00:00
Khalim Conn-Kowlessar
84fb9884d6 No filters resolves the whole portfolio minus deletions 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:32:10 +00:00
Jun-te Kim
bc1cca77db
Merge pull request #1356 from Hestia-Homes/feature/historic-epc-repository
Historic EPC repository: DDD port + resolver for address→UPRN
2026-07-07 10:56:33 +01:00
Daniel Roth
ed08a6adfe HubSpot clients share one scrubbed-retry call policy 🟪
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:02:22 +00:00
Daniel Roth
2ea9257036 A rate-limited HubSpot write is retried before succeeding 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:59:53 +00:00
Daniel Roth
049475ec96 A failed HubSpot write raises a scrubbed error carrying the orphaned job_no 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:58:36 +00:00
Daniel Roth
96f86aacac A logged job's job_no is written to HubSpot and then the deal database 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:54:05 +00:00
Daniel Roth
98674ae895 Log an OpenHousing job from a confirmed survey booking's deal fields 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:50:07 +00:00
Daniel Roth
06354a166e Phone-number selection policy lives in the domain layer 🟪
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:47:37 +00:00
Daniel Roth
8fd808ffd9 HubSpot deal-contacts transport lives under infrastructure like the other API clients 🟪
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:01:47 +00:00
Daniel Roth
3dd2dfe92c Empty tenancies, Abri rejections and ambiguous responses resolve like the other relay operations 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:32:13 +00:00
Daniel Roth
64d397e6b4 Tenant vulnerabilities are recorded on the tenant's own contact 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:31:00 +00:00
Daniel Roth
52f2286ee9 Runner-up number fills the contact's secondary phone number 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:29:38 +00:00
Daniel Roth
10636c1752 Runner-up number fills the contact's secondary phone number 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:28:56 +00:00
Daniel Roth
1752a6cf10 Contact carries the best-priority mobile and landline Abri lists 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:27:59 +00:00
Daniel Roth
76d0b4bac9 Abri tenancy signatory becomes an associated HubSpot deal contact 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:24:47 +00:00
Daniel Roth
78fcba45e5 Abri tenancy signatory becomes an associated HubSpot deal contact 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:22:52 +00:00