"""API-level test for `POST /api/plan/sync-with-wish`. Exercises the router + Pydantic wire models (not just the service directly, which `tests/unit/test_planning_service_sync_with_wish.py` already covers) — this catches the class of bug where the router/model layer drops or mis-names a field on the way to `PlanningService.sync_with_wish`, or where the "exactly one of wish_create/wish_update" request validation is wired wrong. """ from uuid import uuid4 from httpx import AsyncClient from tests.conftest import SeededIds class TestSyncWithWishRequestValidation: async def test_neither_wish_create_nor_wish_update_is_422( self, authed_client: AsyncClient, seeded_ids: SeededIds ) -> None: response = await authed_client.post( "/api/plan/sync-with-wish", json={"workspace_id": str(seeded_ids.workspace_id)} ) assert response.status_code == 422 async def test_both_wish_create_and_wish_update_is_422( self, authed_client: AsyncClient, seeded_ids: SeededIds ) -> None: response = await authed_client.post( "/api/plan/sync-with-wish", json={ "workspace_id": str(seeded_ids.workspace_id), "wish_create": { "name": "X", "window_start_month": 6, "window_start_day": 1, "window_end_month": 6, "window_end_day": 7, "target_weeks": 1, "first_year": 2027, }, "wish_update": {"leave_wish_id": str(uuid4()), "patch": {}}, }, ) assert response.status_code == 422 class TestSyncWithWishCreatesAndFunds: async def test_wish_create_round_trips_and_days_are_persisted( self, authed_client: AsyncClient, seeded_ids: SeededIds ) -> None: response = await authed_client.post( "/api/plan/sync-with-wish", json={ "workspace_id": str(seeded_ids.workspace_id), "wish_create": { "child_id": str(seeded_ids.child_id), "name": "Sommar", "window_start_month": 6, "window_start_day": 1, "window_end_month": 6, "window_end_day": 7, "target_weeks": 1, "target_days": 7, "first_year": 2027, }, }, ) assert response.status_code == 200, response.text body = response.json() assert body["wish"]["name"] == "Sommar" assert body["sync"]["apply_result"]["accepted"] is True wish_id = body["wish"]["id"] wishes = await authed_client.get(f"/api/leave-wishes/{seeded_ids.workspace_id}") assert any(w["id"] == wish_id for w in wishes.json()) days = await authed_client.get( "/api/planned-days", params={"child_id": str(seeded_ids.child_id)} ) assert any(d["wish_id"] == wish_id for d in days.json()) async def test_day_actions_attach_an_ad_hoc_pinned_day_to_the_new_wish( self, authed_client: AsyncClient, seeded_ids: SeededIds ) -> None: # An ad-hoc, hand-pinned day (restructure step 3b: `POST # /api/day-assertions`, which mirrors into `planned_leave_day` # immediately) — the "day_actions" below need that mirrored row's # id, not the assertion's own. created_assertion = await authed_client.post( "/api/day-assertions", json={ "workspace_id": str(seeded_ids.workspace_id), "child_id": str(seeded_ids.child_id), "parent_personal_data_uuid": str(seeded_ids.personal_data_uuid), "date": "2027-08-02", "day_type": "vacation", }, ) assert created_assertion.status_code == 201, created_assertion.text planned = await authed_client.get( "/api/planned-days", params={"child_id": str(seeded_ids.child_id)} ) day_id = next(d["id"] for d in planned.json() if d["date"] == "2027-08-02") response = await authed_client.post( "/api/plan/sync-with-wish", json={ "workspace_id": str(seeded_ids.workspace_id), "wish_create": { "child_id": str(seeded_ids.child_id), "name": "Promoted", "window_start_month": 8, "window_start_day": 2, "window_end_month": 8, "window_end_day": 2, "target_weeks": 1, "target_days": 1, "first_year": 2027, "window_mode": "fixed", }, "day_actions": [{"planned_day_id": day_id, "action": "attach"}], }, ) assert response.status_code == 200, response.text body = response.json() assert body["sync"]["apply_result"]["accepted"] is True day_after = await authed_client.get(f"/api/planned-days/{day_id}") assert day_after.json()["wish_id"] == body["wish"]["id"] assert day_after.json()["is_pinned"] is True