From beaf21fdcc4786dcf0b500dd37605e102eaa543f Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 14:32:37 +0000 Subject: [PATCH 1/7] correct magic plan url paths --- backend/magic_plan/magic_plan_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/magic_plan/magic_plan_client.py b/backend/magic_plan/magic_plan_client.py index 2880bf43..34c40695 100644 --- a/backend/magic_plan/magic_plan_client.py +++ b/backend/magic_plan/magic_plan_client.py @@ -11,7 +11,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") + r = self._session.get(f"{_BASE_URL}/workgroups/plans") r.raise_for_status() return PlansListResponse.model_validate(r.json()["data"]) @@ -22,6 +22,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}") + r = self._session.get(f"{_BASE_URL}/plans/get/{plan_id}") r.raise_for_status() return r From 8727a78f8bb0719061cc075ba49dc264ea90f9cd Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 14:33:58 +0000 Subject: [PATCH 2/7] =?UTF-8?q?correct=20magic=20plan=20url=20paths=20?= =?UTF-8?q?=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/magic_plan/tests/test_magic_plan_client.py | 6 +++--- 1 file changed, 3 insertions(+), 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 a0827bee..27d4ebad 100644 --- a/backend/magic_plan/tests/test_magic_plan_client.py +++ b/backend/magic_plan/tests/test_magic_plan_client.py @@ -70,7 +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") + mock_session.get.assert_called_once_with(f"{BASE_URL}/workgroups/plans") def test_get_plans_calls_raise_for_status( @@ -132,7 +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}") + mock_session.get.assert_called_once_with(f"{BASE_URL}/plans/get/{plan_id}") def test_get_plan_calls_raise_for_status( @@ -202,7 +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}") + mock_session.get.assert_called_once_with(f"{BASE_URL}/plans/get/{plan_id}") def test_get_plan_raw_calls_raise_for_status( From 62acc3ce98cb717a4c65711d3028e526443bbdfc Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 14:45:09 +0000 Subject: [PATCH 3/7] =?UTF-8?q?Paginate=20get=5Fplans=20to=20return=20flat?= =?UTF-8?q?=20list[PlanSummary]=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/magic_plan/magic_plan_client.py | 8 +++----- backend/magic_plan/tests/test_magic_plan_client.py | 13 ++++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/backend/magic_plan/magic_plan_client.py b/backend/magic_plan/magic_plan_client.py index 34c40695..ee52ffb0 100644 --- a/backend/magic_plan/magic_plan_client.py +++ b/backend/magic_plan/magic_plan_client.py @@ -1,6 +1,6 @@ import requests -from datatypes.magicplan.api.response import MagicPlanPlan, PlansListResponse +from datatypes.magicplan.api.response import MagicPlanPlan, PlanSummary, PlansListResponse _BASE_URL = "https://cloud.magicplan.app/api/v2" @@ -10,10 +10,8 @@ class MagicPlanClient: self._session = requests.Session() self._session.headers.update({"customer": customer_id, "key": api_key}) - def get_plans(self) -> PlansListResponse: - r = self._session.get(f"{_BASE_URL}/workgroups/plans") - r.raise_for_status() - return PlansListResponse.model_validate(r.json()["data"]) + def get_plans(self) -> list[PlanSummary]: + raise NotImplementedError def get_plan(self, plan_id: str) -> MagicPlanPlan: return MagicPlanPlan.model_validate(self._fetch_plan(plan_id).json()["data"]) diff --git a/backend/magic_plan/tests/test_magic_plan_client.py b/backend/magic_plan/tests/test_magic_plan_client.py index 27d4ebad..bf078517 100644 --- a/backend/magic_plan/tests/test_magic_plan_client.py +++ b/backend/magic_plan/tests/test_magic_plan_client.py @@ -7,7 +7,7 @@ import pytest import requests from backend.magic_plan.magic_plan_client import MagicPlanClient -from datatypes.magicplan.api.response import MagicPlanPlan, PlansListResponse +from datatypes.magicplan.api.response import MagicPlanPlan, PlanSummary FIXTURE_DIR = Path(__file__).parents[2] / "magic_plan" BASE_URL = "https://cloud.magicplan.app/api/v2" @@ -70,7 +70,9 @@ def test_get_plans_calls_correct_url( # Act client.get_plans() # Assert - mock_session.get.assert_called_once_with(f"{BASE_URL}/workgroups/plans") + mock_session.get.assert_called_once_with( + f"{BASE_URL}/workgroups/plans", params={"page": 1} + ) def test_get_plans_calls_raise_for_status( @@ -88,7 +90,7 @@ def test_get_plans_calls_raise_for_status( mock_session.get.return_value.raise_for_status.assert_called_once() -def test_get_plans_returns_plans_list_response( +def test_get_plans_returns_list_of_plan_summaries( client: MagicPlanClient, mock_session: MagicMock ) -> None: # Arrange @@ -100,8 +102,9 @@ def test_get_plans_returns_plans_list_response( # Act result = client.get_plans() # Assert - assert isinstance(result, PlansListResponse) - assert len(result.plans) == 1 + assert isinstance(result, list) + assert len(result) == 1 + assert isinstance(result[0], PlanSummary) def test_get_plans_propagates_http_error( From f83ddd05a8a6a8bace716e0e449c95bf040b1527 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 14:46:00 +0000 Subject: [PATCH 4/7] =?UTF-8?q?Paginate=20get=5Fplans=20to=20return=20flat?= =?UTF-8?q?=20list[PlanSummary]=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, 3 insertions(+), 1 deletion(-) diff --git a/backend/magic_plan/magic_plan_client.py b/backend/magic_plan/magic_plan_client.py index ee52ffb0..de2fe4f6 100644 --- a/backend/magic_plan/magic_plan_client.py +++ b/backend/magic_plan/magic_plan_client.py @@ -11,7 +11,9 @@ class MagicPlanClient: self._session.headers.update({"customer": customer_id, "key": api_key}) def get_plans(self) -> list[PlanSummary]: - raise NotImplementedError + r = self._session.get(f"{_BASE_URL}/workgroups/plans", params={"page": 1}) + r.raise_for_status() + return PlansListResponse.model_validate(r.json()["data"]).plans def get_plan(self, plan_id: str) -> MagicPlanPlan: return MagicPlanPlan.model_validate(self._fetch_plan(plan_id).json()["data"]) From 6dfca082f8e9619c9a9c31dfd1c3524dc03be530 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 14:52:31 +0000 Subject: [PATCH 5/7] =?UTF-8?q?Fetch=20all=20pages=20in=20get=5Fplans=20pa?= =?UTF-8?q?gination=20loop=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/test_magic_plan_client.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/backend/magic_plan/tests/test_magic_plan_client.py b/backend/magic_plan/tests/test_magic_plan_client.py index bf078517..211a5d4d 100644 --- a/backend/magic_plan/tests/test_magic_plan_client.py +++ b/backend/magic_plan/tests/test_magic_plan_client.py @@ -119,6 +119,34 @@ def test_get_plans_propagates_http_error( client.get_plans() +def test_get_plans_multi_page_fetches_all_pages( + client: MagicPlanClient, mock_session: MagicMock +) -> None: + # Arrange + page1_plan = _load_fixture("magicplan_api_plans_response_example.json")["data"][ + "plans" + ][0] + page2_plan = {**page1_plan, "id": "page-2-plan-id"} + page1_response = MagicMock() + page1_response.json.return_value = { + "data": {"paging": {"page": 1, "next_page": True, "count": 2}, "plans": [page1_plan]} + } + page2_response = MagicMock() + page2_response.json.return_value = { + "data": {"paging": {"page": 2, "next_page": False, "count": 2}, "plans": [page2_plan]} + } + mock_session.get.side_effect = [page1_response, page2_response] + # Act + result = client.get_plans() + # Assert + assert mock_session.get.call_count == 2 + mock_session.get.assert_any_call(f"{BASE_URL}/workgroups/plans", params={"page": 1}) + mock_session.get.assert_any_call(f"{BASE_URL}/workgroups/plans", params={"page": 2}) + assert len(result) == 2 + assert result[0].id == page1_plan["id"] + assert result[1].id == "page-2-plan-id" + + # --- get_plan --- From 0d324f99b29dfe1453d891f799512413e3776484 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 14:52:46 +0000 Subject: [PATCH 6/7] =?UTF-8?q?Fetch=20all=20pages=20in=20get=5Fplans=20pa?= =?UTF-8?q?gination=20loop=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 | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/backend/magic_plan/magic_plan_client.py b/backend/magic_plan/magic_plan_client.py index de2fe4f6..bf50a6f8 100644 --- a/backend/magic_plan/magic_plan_client.py +++ b/backend/magic_plan/magic_plan_client.py @@ -11,9 +11,17 @@ class MagicPlanClient: self._session.headers.update({"customer": customer_id, "key": api_key}) def get_plans(self) -> list[PlanSummary]: - r = self._session.get(f"{_BASE_URL}/workgroups/plans", params={"page": 1}) - r.raise_for_status() - return PlansListResponse.model_validate(r.json()["data"]).plans + all_plans: list[PlanSummary] = [] + page = 1 + while True: + r = self._session.get(f"{_BASE_URL}/workgroups/plans", params={"page": page}) + r.raise_for_status() + response = PlansListResponse.model_validate(r.json()["data"]) + all_plans.extend(response.plans) + if not response.paging.next_page: + break + page += 1 + return all_plans def get_plan(self, plan_id: str) -> MagicPlanPlan: return MagicPlanPlan.model_validate(self._fetch_plan(plan_id).json()["data"]) From 5f77fbf4e45194a6fe18486e2cf199896333b0fa Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 12 May 2026 14:54:14 +0000 Subject: [PATCH 7/7] =?UTF-8?q?Fetch=20all=20pages=20in=20get=5Fplans=20pa?= =?UTF-8?q?gination=20loop=20=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/magic_plan/magic_plan_service.py | 12 +++--------- .../magic_plan/tests/test_magic_plan_service.py | 14 +++++++------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/backend/magic_plan/magic_plan_service.py b/backend/magic_plan/magic_plan_service.py index fb0a7610..22e19ddf 100644 --- a/backend/magic_plan/magic_plan_service.py +++ b/backend/magic_plan/magic_plan_service.py @@ -3,11 +3,7 @@ import json from datetime import datetime, timezone from typing import Optional -from datatypes.magicplan.api.response import ( - MagicPlanPlan, - PlanSummary, - PlansListResponse, -) +from datatypes.magicplan.api.response import MagicPlanPlan, PlanSummary from datatypes.magicplan.domain.mapper import map_plan from datatypes.magicplan.domain.models import Plan @@ -39,10 +35,8 @@ class MagicPlanService: if uprn is not None: logger.info("MagicPlanService.run uprn=%s", uprn) - plans_response: PlansListResponse = self._client.get_plans() - matched: Optional[PlanSummary] = find_matching_plan( - plans_response.plans, address - ) + plans: list[PlanSummary] = self._client.get_plans() + matched: Optional[PlanSummary] = find_matching_plan(plans, address) if matched is None: raise ValueError(f"No MagicPlan found for address: {address!r}") diff --git a/backend/magic_plan/tests/test_magic_plan_service.py b/backend/magic_plan/tests/test_magic_plan_service.py index f6954824..158cf4d6 100644 --- a/backend/magic_plan/tests/test_magic_plan_service.py +++ b/backend/magic_plan/tests/test_magic_plan_service.py @@ -91,7 +91,7 @@ def test_run_fetches_plan_with_matched_id( domain_plan: Plan, ) -> None: # Arrange - mock_client.get_plans.return_value.plans = [plan_summary] + mock_client.get_plans.return_value = [plan_summary] mock_client.get_plan.return_value = api_magic_plan service = _make_service(mock_client) with patch( @@ -114,7 +114,7 @@ def test_run_returns_mapped_plan( domain_plan: Plan, ) -> None: # Arrange - mock_client.get_plans.return_value.plans = [plan_summary] + mock_client.get_plans.return_value = [plan_summary] mock_client.get_plan.return_value = api_magic_plan service = _make_service(mock_client) with patch( @@ -137,7 +137,7 @@ def test_run_calls_save_plan_with_mapped_plan( plan_summary: PlanSummary, ) -> None: # Arrange - mock_client.get_plans.return_value.plans = [plan_summary] + mock_client.get_plans.return_value = [plan_summary] mock_client.get_plan.return_value = api_magic_plan service = _make_service(mock_client) with patch( @@ -161,7 +161,7 @@ def test_run_accepts_uprn_without_error( plan_summary: PlanSummary, ) -> None: # Arrange - mock_client.get_plans.return_value.plans = [plan_summary] + mock_client.get_plans.return_value = [plan_summary] mock_client.get_plan.return_value = api_magic_plan service = _make_service(mock_client) with patch( @@ -184,7 +184,7 @@ def test_run_uploads_to_s3_with_uprn_key( plan_summary: PlanSummary, ) -> None: # Arrange - mock_client.get_plans.return_value.plans = [plan_summary] + mock_client.get_plans.return_value = [plan_summary] request = _make_request(uprn="100023336956") service = MagicPlanService(client=mock_client, s3_bucket=S3_BUCKET) with patch( @@ -211,7 +211,7 @@ def test_run_uploads_to_s3_with_deal_id_key_when_uprn_absent( plan_summary: PlanSummary, ) -> None: # Arrange - mock_client.get_plans.return_value.plans = [plan_summary] + mock_client.get_plans.return_value = [plan_summary] mock_client.get_plan.return_value = api_magic_plan request = _make_request(hubspot_deal_id="deal-456", uprn=None) service = MagicPlanService(client=mock_client, s3_bucket=S3_BUCKET) @@ -242,7 +242,7 @@ def test_run_creates_uploaded_file_record( plan_summary: PlanSummary, ) -> None: # Arrange - mock_client.get_plans.return_value.plans = [plan_summary] + mock_client.get_plans.return_value = [plan_summary] mock_client.get_plan.return_value = api_magic_plan request = _make_request(hubspot_deal_id="deal-789", uprn="100023336956") service = MagicPlanService(client=mock_client, s3_bucket=S3_BUCKET)