From a672c0dea0dbfb54a35d4d5deeefea7303c93193 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 13:51:46 +0000 Subject: [PATCH 01/10] add localhandler for testing and update requirements --- backend/magic_plan/handler/requirements.txt | 4 +++ .../local_handler/docker-compose.yml | 11 ++++++++ .../local_handler/invoke_local_lambda.py | 28 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 backend/magic_plan/local_handler/docker-compose.yml create mode 100644 backend/magic_plan/local_handler/invoke_local_lambda.py diff --git a/backend/magic_plan/handler/requirements.txt b/backend/magic_plan/handler/requirements.txt index cfacf455..29123caa 100644 --- a/backend/magic_plan/handler/requirements.txt +++ b/backend/magic_plan/handler/requirements.txt @@ -5,3 +5,7 @@ sqlmodel psycopg2-binary==2.9.10 pydantic-settings==2.6.0 boto3==1.35.44 + +pytz==2024.2 +pandas==2.2.2 +numpy==2.1.2 diff --git a/backend/magic_plan/local_handler/docker-compose.yml b/backend/magic_plan/local_handler/docker-compose.yml new file mode 100644 index 00000000..5a42d259 --- /dev/null +++ b/backend/magic_plan/local_handler/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.9" + +services: + ecmk-fetcher-lambda: + build: + context: ../../../ + dockerfile: backend/magic_plan/handler/Dockerfile + ports: + - "9000:8080" + env_file: + - ../../../.env \ No newline at end of file diff --git a/backend/magic_plan/local_handler/invoke_local_lambda.py b/backend/magic_plan/local_handler/invoke_local_lambda.py new file mode 100644 index 00000000..7bb65806 --- /dev/null +++ b/backend/magic_plan/local_handler/invoke_local_lambda.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +import json +import requests + +HOST = "localhost" +PORT = "9000" + +LAMBDA_URL = f"http://{HOST}:{PORT}/2015-03-31/functions/function/invocations" + +payload = { + "Records": [ + { + "body": json.dumps( + # { + # "address": "2 Laburnum Way, Rombley, BR2 8BZ | Retrofit Assessment", + # "hubspot_deal_id": "500262906061", + # } + {"address": "33 Wallaby Way, Sydney", "hubspot_deal_id": "123456789"} + ) + } + ] +} + +response = requests.post(LAMBDA_URL, json=payload) + +print("Status code:", response.status_code) +print("Response:") +print(response.text) From da4f5f44c0ba871f05cf7da3cc2f070a5398b496 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 13:58:16 +0000 Subject: [PATCH 02/10] =?UTF-8?q?Set=20API=20key=20as=20session=20header?= =?UTF-8?q?=20on=20MagicPlanClient=20construction=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/magic_plan/tests/test_magic_plan_client.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/magic_plan/tests/test_magic_plan_client.py b/backend/magic_plan/tests/test_magic_plan_client.py index c96b9cdf..cb2385b1 100644 --- a/backend/magic_plan/tests/test_magic_plan_client.py +++ b/backend/magic_plan/tests/test_magic_plan_client.py @@ -20,6 +20,7 @@ def _load_fixture(name: str) -> dict[str, Any]: def _make_client(mock_session: MagicMock) -> MagicPlanClient: + mock_session.headers = {} with patch( "backend.magic_plan.magic_plan_client.requests.Session", return_value=mock_session, @@ -44,7 +45,14 @@ def test_customer_header_set_on_session(mock_session: MagicMock) -> None: # Act _make_client(mock_session) # Assert - mock_session.headers.update.assert_called_once_with({"customer": CUSTOMER_ID}) + assert mock_session.headers["customer"] == CUSTOMER_ID + + +def test_api_key_header_set_on_session(mock_session: MagicMock) -> None: + # Act + _make_client(mock_session) + # Assert + assert mock_session.headers["key"] == API_KEY # --- get_plans --- From d59bf2d7cbf35299128099345a4334d1dc372c94 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 13:59:33 +0000 Subject: [PATCH 03/10] =?UTF-8?q?Set=20API=20key=20as=20session=20header?= =?UTF-8?q?=20on=20MagicPlanClient=20construction=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/magic_plan/magic_plan_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/magic_plan/magic_plan_client.py b/backend/magic_plan/magic_plan_client.py index 06905e6a..4029c436 100644 --- a/backend/magic_plan/magic_plan_client.py +++ b/backend/magic_plan/magic_plan_client.py @@ -9,7 +9,7 @@ class MagicPlanClient: def __init__(self, customer_id: str, api_key: str) -> None: self._api_key = api_key self._session = requests.Session() - self._session.headers.update({"customer": customer_id}) + self._session.headers.update({"customer": customer_id, "key": api_key}) def get_plans(self) -> PlansListResponse: r = self._session.get(f"{_BASE_URL}/plans", params={"key": self._api_key}) From ffcff33dd4434efe98bd2ca04d683b726eb453ba Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 14:00:07 +0000 Subject: [PATCH 04/10] =?UTF-8?q?get=5Fplans()=20sends=20no=20API=20key=20?= =?UTF-8?q?query=20parameter=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/magic_plan/tests/test_magic_plan_client.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/magic_plan/tests/test_magic_plan_client.py b/backend/magic_plan/tests/test_magic_plan_client.py index cb2385b1..8b0b3f71 100644 --- a/backend/magic_plan/tests/test_magic_plan_client.py +++ b/backend/magic_plan/tests/test_magic_plan_client.py @@ -70,9 +70,7 @@ def test_get_plans_calls_correct_url( # Act client.get_plans() # Assert - mock_session.get.assert_called_once_with( - f"{BASE_URL}/plans", params={"key": API_KEY} - ) + mock_session.get.assert_called_once_with(f"{BASE_URL}/plans") def test_get_plans_calls_raise_for_status( From 20b32bcda03679d7d0c28f82d53621e4fb092af2 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 14:01:35 +0000 Subject: [PATCH 05/10] =?UTF-8?q?get=5Fplans()=20sends=20no=20API=20key=20?= =?UTF-8?q?query=20parameter=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/magic_plan/magic_plan_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/magic_plan/magic_plan_client.py b/backend/magic_plan/magic_plan_client.py index 4029c436..bed3dc9c 100644 --- a/backend/magic_plan/magic_plan_client.py +++ b/backend/magic_plan/magic_plan_client.py @@ -12,7 +12,7 @@ class MagicPlanClient: self._session.headers.update({"customer": customer_id, "key": api_key}) def get_plans(self) -> PlansListResponse: - r = self._session.get(f"{_BASE_URL}/plans", params={"key": self._api_key}) + r = self._session.get(f"{_BASE_URL}/plans") r.raise_for_status() return PlansListResponse.model_validate(r.json()["data"]) From 7752039dbdb5244d12a974e2ce3a17728d25293d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 14:01:40 +0000 Subject: [PATCH 06/10] =?UTF-8?q?=5Ffetch=5Fplan()=20sends=20no=20API=20ke?= =?UTF-8?q?y=20query=20parameter=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/magic_plan/tests/test_magic_plan_client.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/backend/magic_plan/tests/test_magic_plan_client.py b/backend/magic_plan/tests/test_magic_plan_client.py index 8b0b3f71..a0827bee 100644 --- a/backend/magic_plan/tests/test_magic_plan_client.py +++ b/backend/magic_plan/tests/test_magic_plan_client.py @@ -132,9 +132,7 @@ def test_get_plan_calls_correct_url( # Act client.get_plan(plan_id) # Assert - mock_session.get.assert_called_once_with( - f"{BASE_URL}/plans/{plan_id}", params={"key": API_KEY} - ) + mock_session.get.assert_called_once_with(f"{BASE_URL}/plans/{plan_id}") def test_get_plan_calls_raise_for_status( @@ -204,9 +202,7 @@ def test_get_plan_raw_calls_correct_url( # Act client.get_plan_raw(plan_id) # Assert - mock_session.get.assert_called_once_with( - f"{BASE_URL}/plans/{plan_id}", params={"key": API_KEY} - ) + mock_session.get.assert_called_once_with(f"{BASE_URL}/plans/{plan_id}") def test_get_plan_raw_calls_raise_for_status( From eb381a778c2dc8c2ff2341ff14a4376463e4b5d8 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 14:02:17 +0000 Subject: [PATCH 07/10] =?UTF-8?q?=5Ffetch=5Fplan()=20sends=20no=20API=20ke?= =?UTF-8?q?y=20query=20parameter=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/magic_plan/magic_plan_client.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/magic_plan/magic_plan_client.py b/backend/magic_plan/magic_plan_client.py index bed3dc9c..f9ae030f 100644 --- a/backend/magic_plan/magic_plan_client.py +++ b/backend/magic_plan/magic_plan_client.py @@ -23,8 +23,6 @@ class MagicPlanClient: return self._fetch_plan(plan_id).content def _fetch_plan(self, plan_id: str) -> requests.Response: - r = self._session.get( - f"{_BASE_URL}/plans/{plan_id}", params={"key": self._api_key} - ) + r = self._session.get(f"{_BASE_URL}/plans/{plan_id}") r.raise_for_status() return r From 3df726937e43c3c2758266144418740ae7636238 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 14:03:07 +0000 Subject: [PATCH 08/10] =?UTF-8?q?Remove=20unused=20=5Fapi=5Fkey=20instance?= =?UTF-8?q?=20variable=20now=20auth=20is=20fully=20header-based=20?= =?UTF-8?q?=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/magic_plan/magic_plan_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/magic_plan/magic_plan_client.py b/backend/magic_plan/magic_plan_client.py index f9ae030f..2880bf43 100644 --- a/backend/magic_plan/magic_plan_client.py +++ b/backend/magic_plan/magic_plan_client.py @@ -7,7 +7,6 @@ _BASE_URL = "https://cloud.magicplan.app/api/v2" class MagicPlanClient: def __init__(self, customer_id: str, api_key: str) -> None: - self._api_key = api_key self._session = requests.Session() self._session.headers.update({"customer": customer_id, "key": api_key}) From 04df924146e336688bd4110111625b3738e3ac21 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 14:13:12 +0000 Subject: [PATCH 09/10] fix local invoker --- backend/magic_plan/handler.py | 5 +++-- backend/magic_plan/local_handler/invoke_local_lambda.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/magic_plan/handler.py b/backend/magic_plan/handler.py index f2c03b90..5fd90b7a 100644 --- a/backend/magic_plan/handler.py +++ b/backend/magic_plan/handler.py @@ -20,7 +20,9 @@ def handler(body: dict[str, Any], context: Any) -> str: api_key=settings.MAGICPLAN_API_KEY, ) # TODO: read s3_bucket from env var so staging/prod use the correct bucket - plan: Plan = MagicPlanService(client, s3_bucket="retrofit-energy-assessments-dev").run(payload) + plan: Plan = MagicPlanService( + client, s3_bucket="retrofit-energy-assessments-dev" + ).run(payload) logger.info("Saved MagicPlan plan uid=%s", plan.uid) return plan.uid @@ -30,7 +32,6 @@ if __name__ == "__main__": "Records": [ { "body": '{"address": "2 Laburnum Way Bromley BR2 8BZ", "hubspot_deal_id": "local-test-deal"}', - "messageId": "local-test", } ] } diff --git a/backend/magic_plan/local_handler/invoke_local_lambda.py b/backend/magic_plan/local_handler/invoke_local_lambda.py index 7bb65806..146951fe 100644 --- a/backend/magic_plan/local_handler/invoke_local_lambda.py +++ b/backend/magic_plan/local_handler/invoke_local_lambda.py @@ -10,13 +10,14 @@ LAMBDA_URL = f"http://{HOST}:{PORT}/2015-03-31/functions/function/invocations" payload = { "Records": [ { + "messageId": "test-message-id", "body": json.dumps( # { # "address": "2 Laburnum Way, Rombley, BR2 8BZ | Retrofit Assessment", # "hubspot_deal_id": "500262906061", # } {"address": "33 Wallaby Way, Sydney", "hubspot_deal_id": "123456789"} - ) + ), } ] } From 75d03139341444ab5694793634f014c903fa6e3a Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 14:14:37 +0000 Subject: [PATCH 10/10] fix broken magicplan handler tests --- backend/magic_plan/tests/test_handler.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/backend/magic_plan/tests/test_handler.py b/backend/magic_plan/tests/test_handler.py index 366f3ded..b0365f5b 100644 --- a/backend/magic_plan/tests/test_handler.py +++ b/backend/magic_plan/tests/test_handler.py @@ -54,7 +54,7 @@ def test_handler_raises_on_missing_address(mock_plan: MagicMock) -> None: def test_handler_constructs_client_from_settings(mock_service: MagicMock) -> None: # Arrange - body = {"address": ADDRESS} + body = {"address": ADDRESS, "hubspot_deal_id": "deal-123"} with patch("backend.magic_plan.handler.get_settings", return_value=_make_settings(customer_id="cust-xyz", api_key="key-xyz")), \ patch("backend.magic_plan.handler.MagicPlanClient") as MockClient, \ patch("backend.magic_plan.handler.MagicPlanService", return_value=mock_service): @@ -69,31 +69,37 @@ def test_handler_constructs_client_from_settings(mock_service: MagicMock) -> Non def test_handler_calls_service_run_with_address(mock_service: MagicMock) -> None: # Arrange - body = {"address": ADDRESS} + body = {"address": ADDRESS, "hubspot_deal_id": "deal-123"} with patch("backend.magic_plan.handler.get_settings", return_value=_make_settings()), \ patch("backend.magic_plan.handler.MagicPlanClient"), \ patch("backend.magic_plan.handler.MagicPlanService", return_value=mock_service): # Act _call_handler(body) # Assert - mock_service.run.assert_called_once_with(ADDRESS, None) + mock_service.run.assert_called_once() + request = mock_service.run.call_args.args[0] + assert request.address == ADDRESS + assert request.uprn is None def test_handler_passes_uprn_to_service(mock_service: MagicMock) -> None: # Arrange - body = {"address": ADDRESS, "uprn": "100023336956"} + body = {"address": ADDRESS, "uprn": "100023336956", "hubspot_deal_id": "deal-123"} with patch("backend.magic_plan.handler.get_settings", return_value=_make_settings()), \ patch("backend.magic_plan.handler.MagicPlanClient"), \ patch("backend.magic_plan.handler.MagicPlanService", return_value=mock_service): # Act _call_handler(body) # Assert - mock_service.run.assert_called_once_with(ADDRESS, "100023336956") + mock_service.run.assert_called_once() + request = mock_service.run.call_args.args[0] + assert request.address == ADDRESS + assert request.uprn == "100023336956" def test_handler_returns_plan_uid(mock_service: MagicMock) -> None: # Arrange - body = {"address": ADDRESS} + body = {"address": ADDRESS, "hubspot_deal_id": "deal-123"} with patch("backend.magic_plan.handler.get_settings", return_value=_make_settings()), \ patch("backend.magic_plan.handler.MagicPlanClient"), \ patch("backend.magic_plan.handler.MagicPlanService", return_value=mock_service):