Merge pull request #1370 from Hestia-Homes/feature/ventilation-audit-changes

Ventilation audit changes: directly populate Total Effective Area for trickle vents from form data, rather than calculating it
This commit is contained in:
Daniel Roth 2026-07-01 09:20:54 +01:00 committed by GitHub
commit a8821c9cdb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 161 additions and 15 deletions

View file

@ -0,0 +1,11 @@
version: "3.9"
services:
audit-generator-lambda:
build:
context: ../../../
dockerfile: applications/audit_generator/handler/Dockerfile
ports:
- "9001:8080"
env_file:
- ../../../.env

View file

@ -0,0 +1,29 @@
#!/usr/bin/env python3
import json
import requests
HOST = "localhost"
PORT = "9001"
LAMBDA_URL = f"http://{HOST}:{PORT}/2015-03-31/functions/function/invocations"
payload = {
"Records": [
{
"messageId": "test-message-id",
"body": json.dumps(
{
"task_id": "00000000-0000-0000-0000-000000000001",
"sub_task_id": "00000000-0000-0000-0000-000000000002",
"hubspot_deal_id": "500311510234",
}
),
}
]
}
response = requests.post(LAMBDA_URL, json=payload)
print("Status code:", response.status_code)
print("Response:")
print(response.text)

View file

@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Run AuditGeneratorOrchestrator directly, bypassing @subtask_handler.
Loads credentials from the repo-root .env file so no DB task/subtask rows
are needed.
"""
import os
import sys
from pathlib import Path
from typing import Any
import boto3
from dotenv import load_dotenv
REPO_ROOT = Path(__file__).resolve().parents[3]
load_dotenv(REPO_ROOT / ".env")
sys.path.insert(0, str(REPO_ROOT))
from utilities.logger import setup_logger
setup_logger()
from infrastructure.postgres.config import PostgresConfig
from infrastructure.postgres.engine import make_engine, make_session
from infrastructure.s3.s3_client import S3Client
from orchestration.audit_generator_orchestrator import AuditGeneratorOrchestrator
from orchestration.audit_generator_unit_of_work import AuditGeneratorUnitOfWork
HUBSPOT_DEAL_ID = "505712829658"
boto3_client: Any = boto3.client # type: ignore[attr-defined]
s3_client = S3Client(
boto_s3_client=boto3_client("s3"),
# bucket=os.environ["S3_BUCKET_NAME"],
bucket="retrofit-energy-assessments-dev",
)
engine = make_engine(PostgresConfig.from_env(os.environ))
def session_factory() -> Any:
return make_session(engine)
def uow_factory() -> AuditGeneratorUnitOfWork:
return AuditGeneratorUnitOfWork(session_factory)
print(f"Running AuditGeneratorOrchestrator for deal: {HUBSPOT_DEAL_ID!r}")
AuditGeneratorOrchestrator(
hubspot_deal_id=HUBSPOT_DEAL_ID,
s3_client=s3_client,
uow_factory=uow_factory,
).run()
print("Done")

View file

@ -99,8 +99,17 @@ def _map_window_ventilation(
opening_type=_try_get_str_value("Opening Type"),
num_openings=_try_get_int_value("Number of Openings (In Same Window)"),
pct_openable=_try_get_int_value("% of Window Openable"),
trickle_vent_area_mm2=_try_get_int_value(
"Trickle Vent Effective Area (mm2) (No Code Then Width x Height)"
trickle_vent_area_mm2=next(
(
v
for label in [
"Total Trickle Vent Effective Area (mm2) (No Code Then Width x Height)",
"Individual Trickle Vent Effective Area (mm2) (No Code Then Width x Height)",
"Trickle Vent Effective Area (mm2) (No Code Then Width x Height)",
]
if (v := _try_get_int_value(label)) is not None
),
None,
),
num_trickle_vents=_try_get_int_value("No. of Trickle Vents"),
)

View file

@ -11,7 +11,7 @@ from domain.magicplan.models import Door, Plan, Room, Window
_DATA_START_ROW = 6
_MAX_ROWS = 50
_DOOR_AREA_COL = "U"
_DOOR_AREA_COL = "T"
_DOOR_AREA_CF_RANGE = f"{_DOOR_AREA_COL}{_DATA_START_ROW}:{_DOOR_AREA_COL}{_DATA_START_ROW + _MAX_ROWS - 1}"
_Y_THRESHOLD = 7600
_DOOR_AREA_HEADER = CellRichText(
@ -94,9 +94,9 @@ def populate_sheet(sheet: Any, plan: Plan) -> None:
for i, (room_name, door) in enumerate(doors):
row = _DATA_START_ROW + i
vent = door.ventilation
_write_cell(sheet, row, "R", room_name)
_write_cell(sheet, row, "S", door.width_mm)
_write_cell(sheet, row, "T", vent.undercut_mm if vent else 0)
# U = formula =S*T — do not write
_write_cell(sheet, row, "Q", room_name)
_write_cell(sheet, row, "R", door.width_mm)
_write_cell(sheet, row, "S", vent.undercut_mm if vent else 0)
# T = formula =R*S — do not write
_apply_column_y_formatting(sheet)

View file

@ -8,6 +8,7 @@ import domain.magicplan.api.response as api
from domain.magicplan.api.response import MagicPlanPlan, Symbol, Vec3, WallItem
from domain.magicplan.mapper import (
_map_window, # pyright: ignore[reportPrivateUsage]
_map_window_ventilation, # pyright: ignore[reportPrivateUsage]
map_address,
map_plan,
)
@ -229,3 +230,42 @@ def test_map_address_city_absent_is_omitted() -> None:
def test_map_address_none_returns_none() -> None:
assert map_address(None) is None
def _make_survey_field(label: str, value: str) -> api.SurveyField:
return api.SurveyField(
id="test",
type=5,
type_as_string="float",
is_required=False,
label=label,
value=api.FieldValue(index=-1, has_value=True, is_array=False, value=value),
)
def test_trickle_vent_area_read_from_legacy_field_label() -> None:
# Arrange — old MagicPlan label, not yet renamed in the API response
fields = [_make_survey_field(
"Trickle Vent Effective Area (mm2) (No Code Then Width x Height)", "1700"
)]
# Act
vent = _map_window_ventilation(fields)
# Assert
assert vent is not None
assert vent.trickle_vent_area_mm2 == 1700
def test_trickle_vent_area_read_from_individual_field_label() -> None:
# Arrange — "Individual" variant used in some MagicPlan templates
fields = [_make_survey_field(
"Individual Trickle Vent Effective Area (mm2) (No Code Then Width x Height)", "900"
)]
# Act
vent = _map_window_ventilation(fields)
# Assert
assert vent is not None
assert vent.trickle_vent_area_mm2 == 900

View file

@ -148,7 +148,7 @@ def test_writes_trickle_vent_data_to_columns_N_and_O() -> None:
assert sheet["O6"].value == 2
def test_writes_door_room_name_to_column_R() -> None:
def test_writes_door_room_name_to_column_Q() -> None:
# Arrange
plan = _make_plan(num_rooms=1, num_windows_per_room=0, num_doors_per_room=1)
sheet = _blank_sheet()
@ -157,10 +157,10 @@ def test_writes_door_room_name_to_column_R() -> None:
populate_sheet(sheet, plan)
# Assert
assert sheet["R6"].value == "Room 0"
assert sheet["Q6"].value == "Room 0"
def test_writes_door_dimensions_to_columns_S_and_T() -> None:
def test_writes_door_dimensions_to_columns_R_and_S() -> None:
# Arrange
plan = _make_plan(num_rooms=1, num_windows_per_room=0, num_doors_per_room=1)
sheet = _blank_sheet()
@ -169,8 +169,8 @@ def test_writes_door_dimensions_to_columns_S_and_T() -> None:
populate_sheet(sheet, plan)
# Assert
assert sheet["S6"].value == 800.0
assert sheet["T6"].value == 10.0
assert sheet["R6"].value == 800.0
assert sheet["S6"].value == 10.0
def test_writes_zeros_when_window_has_no_ventilation() -> None:
@ -200,7 +200,7 @@ def test_writes_zeros_when_door_has_no_ventilation() -> None:
populate_sheet(sheet, plan)
# Assert
assert sheet["T6"].value == 0
assert sheet["S6"].value == 0
_TEMPLATE_PATH = (
@ -220,4 +220,4 @@ def test_real_template_survives_18_doors() -> None:
populate_sheet(sheet, plan)
assert sheet["R23"].value == "Room 17"
assert sheet["Q23"].value == "Room 17"

File diff suppressed because one or more lines are too long