diff --git a/src/api/endpoints/proposals/agencies/approve/__init__.py b/src/api/endpoints/proposals/agencies/by_id/__init__.py similarity index 100% rename from src/api/endpoints/proposals/agencies/approve/__init__.py rename to src/api/endpoints/proposals/agencies/by_id/__init__.py diff --git a/src/api/endpoints/proposals/agencies/get/__init__.py b/src/api/endpoints/proposals/agencies/by_id/approve/__init__.py similarity index 100% rename from src/api/endpoints/proposals/agencies/get/__init__.py rename to src/api/endpoints/proposals/agencies/by_id/approve/__init__.py diff --git a/src/api/endpoints/proposals/agencies/approve/query.py b/src/api/endpoints/proposals/agencies/by_id/approve/query.py similarity index 98% rename from src/api/endpoints/proposals/agencies/approve/query.py rename to src/api/endpoints/proposals/agencies/by_id/approve/query.py index 3c08954e..07dd21ff 100644 --- a/src/api/endpoints/proposals/agencies/approve/query.py +++ b/src/api/endpoints/proposals/agencies/by_id/approve/query.py @@ -3,7 +3,7 @@ from sqlalchemy.exc import NoResultFound from sqlalchemy.ext.asyncio import AsyncSession -from src.api.endpoints.proposals.agencies.approve.response import ProposalAgencyApproveResponse +from src.api.endpoints.proposals.agencies.by_id.approve.response import ProposalAgencyApproveResponse from src.db.models.impl.agency.enums import JurisdictionType, AgencyType from src.db.models.impl.agency.sqlalchemy import Agency from src.db.models.impl.link.agency_location.sqlalchemy import LinkAgencyLocation diff --git a/src/api/endpoints/proposals/agencies/approve/response.py b/src/api/endpoints/proposals/agencies/by_id/approve/response.py similarity index 100% rename from src/api/endpoints/proposals/agencies/approve/response.py rename to src/api/endpoints/proposals/agencies/by_id/approve/response.py diff --git a/src/api/endpoints/proposals/agencies/reject/__init__.py b/src/api/endpoints/proposals/agencies/by_id/locations/__init__.py similarity index 100% rename from src/api/endpoints/proposals/agencies/reject/__init__.py rename to src/api/endpoints/proposals/agencies/by_id/locations/__init__.py diff --git a/src/api/endpoints/proposals/agencies/by_id/locations/delete/__init__.py b/src/api/endpoints/proposals/agencies/by_id/locations/delete/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/api/endpoints/proposals/agencies/by_id/locations/delete/query.py b/src/api/endpoints/proposals/agencies/by_id/locations/delete/query.py new file mode 100644 index 00000000..1ce236cb --- /dev/null +++ b/src/api/endpoints/proposals/agencies/by_id/locations/delete/query.py @@ -0,0 +1,30 @@ +from sqlalchemy import delete +from sqlalchemy.ext.asyncio import AsyncSession + +from src.db.models.impl.link.agency_location.sqlalchemy import LinkAgencyLocation +from src.db.models.impl.proposals.agency_.link__location import ProposalLinkAgencyLocation +from src.db.queries.base.builder import QueryBuilderBase + + +class DeleteProposalAgencyLocationQueryBuilder(QueryBuilderBase): + + def __init__( + self, + agency_id: int, + location_id: int, + ): + super().__init__() + self.agency_id = agency_id + self.location_id = location_id + + async def run(self, session: AsyncSession) -> None: + statement = ( + delete(ProposalLinkAgencyLocation) + .where( + (ProposalLinkAgencyLocation.proposal_agency_id == self.agency_id) + & (ProposalLinkAgencyLocation.location_id == self.location_id) + ) + ) + + await session.execute(statement) + diff --git a/src/api/endpoints/proposals/agencies/by_id/locations/get/__init__.py b/src/api/endpoints/proposals/agencies/by_id/locations/get/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/api/endpoints/proposals/agencies/by_id/locations/get/query.py b/src/api/endpoints/proposals/agencies/by_id/locations/get/query.py new file mode 100644 index 00000000..bc45f8ba --- /dev/null +++ b/src/api/endpoints/proposals/agencies/by_id/locations/get/query.py @@ -0,0 +1,41 @@ +from typing import Sequence + +from sqlalchemy import select, RowMapping +from sqlalchemy.ext.asyncio import AsyncSession + +from src.api.endpoints.agencies.by_id.locations.get.response import AgencyGetLocationsResponse +from src.api.endpoints.proposals.agencies.by_id.locations.get.response import ProposalAgencyGetLocationsOuterResponse +from src.db.models.impl.link.agency_location.sqlalchemy import LinkAgencyLocation +from src.db.models.impl.proposals.agency_.link__location import ProposalLinkAgencyLocation +from src.db.models.views.location_expanded import LocationExpandedView +from src.db.queries.base.builder import QueryBuilderBase + + +class GetProposalAgencyLocationsQueryBuilder(QueryBuilderBase): + + def __init__( + self, + agency_id: int, + ): + super().__init__() + self.agency_id = agency_id + + async def run(self, session: AsyncSession) -> ProposalAgencyGetLocationsOuterResponse: + query = ( + select( + ProposalLinkAgencyLocation.location_id, + LocationExpandedView.full_display_name + ) + .where( + ProposalLinkAgencyLocation.proposal_agency_id == self.agency_id + ) + .join( + LocationExpandedView, + LocationExpandedView.id == ProposalLinkAgencyLocation.location_id + ) + ) + + result: Sequence[RowMapping] = await self.sh.mappings(session, query=query) + return ProposalAgencyGetLocationsOuterResponse( + results=[AgencyGetLocationsResponse(**row) for row in result] + ) \ No newline at end of file diff --git a/src/api/endpoints/proposals/agencies/by_id/locations/get/response.py b/src/api/endpoints/proposals/agencies/by_id/locations/get/response.py new file mode 100644 index 00000000..f6175e6d --- /dev/null +++ b/src/api/endpoints/proposals/agencies/by_id/locations/get/response.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + +from src.api.endpoints.agencies.by_id.locations.get.response import AgencyGetLocationsResponse + + +class ProposalAgencyGetLocationsOuterResponse(BaseModel): + results: list[AgencyGetLocationsResponse] \ No newline at end of file diff --git a/src/api/endpoints/proposals/agencies/by_id/locations/post/__init__.py b/src/api/endpoints/proposals/agencies/by_id/locations/post/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/api/endpoints/proposals/agencies/by_id/locations/post/query.py b/src/api/endpoints/proposals/agencies/by_id/locations/post/query.py new file mode 100644 index 00000000..439482e5 --- /dev/null +++ b/src/api/endpoints/proposals/agencies/by_id/locations/post/query.py @@ -0,0 +1,23 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from src.db.models.impl.proposals.agency_.link__location import ProposalLinkAgencyLocation +from src.db.queries.base.builder import QueryBuilderBase + + +class AddProposalAgencyLocationQueryBuilder(QueryBuilderBase): + + def __init__( + self, + agency_id: int, + location_id: int + ): + super().__init__() + self.agency_id = agency_id + self.location_id = location_id + + async def run(self, session: AsyncSession) -> None: + lal = ProposalLinkAgencyLocation( + proposal_agency_id=self.agency_id, + location_id=self.location_id, + ) + session.add(lal) \ No newline at end of file diff --git a/src/api/endpoints/proposals/agencies/by_id/put/__init__.py b/src/api/endpoints/proposals/agencies/by_id/put/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/api/endpoints/proposals/agencies/by_id/put/query.py b/src/api/endpoints/proposals/agencies/by_id/put/query.py new file mode 100644 index 00000000..996cd909 --- /dev/null +++ b/src/api/endpoints/proposals/agencies/by_id/put/query.py @@ -0,0 +1,45 @@ +from fastapi import HTTPException +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from src.api.endpoints.proposals.agencies.by_id.put.request import ProposalAgencyPutRequest +from src.db.models.impl.proposals.agency_.core import ProposalAgency +from src.db.queries.base.builder import QueryBuilderBase + + +class UpdateProposalAgencyQueryBuilder(QueryBuilderBase): + + def __init__( + self, + agency_id: int, + request: ProposalAgencyPutRequest, + ): + super().__init__() + self.agency_id = agency_id + self.request = request + + async def run(self, session: AsyncSession) -> None: + + query = ( + select( + ProposalAgency + ) + .where( + ProposalAgency.id == self.agency_id + ) + ) + + agency: ProposalAgency | None = await self.sh.one_or_none(session, query=query) + if not agency: + raise HTTPException(status_code=400, detail="Proposed Agency not found") + + if self.request.name is not None: + agency.name = self.request.name + if self.request.type is not None: + agency.agency_type = self.request.type + if self.request.jurisdiction_type is not None: + agency.jurisdiction_type = self.request.jurisdiction_type + + + + diff --git a/src/api/endpoints/proposals/agencies/by_id/put/request.py b/src/api/endpoints/proposals/agencies/by_id/put/request.py new file mode 100644 index 00000000..4f49f17e --- /dev/null +++ b/src/api/endpoints/proposals/agencies/by_id/put/request.py @@ -0,0 +1,10 @@ +from src.api.shared.models.request_base import RequestBase +from src.db.models.impl.agency.enums import AgencyType, JurisdictionType + + +class ProposalAgencyPutRequest(RequestBase): + name: str | None = None + type: AgencyType | None = None + jurisdiction_type: JurisdictionType | None = None + + diff --git a/src/api/endpoints/proposals/agencies/by_id/reject/__init__.py b/src/api/endpoints/proposals/agencies/by_id/reject/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/api/endpoints/proposals/agencies/reject/query.py b/src/api/endpoints/proposals/agencies/by_id/reject/query.py similarity index 93% rename from src/api/endpoints/proposals/agencies/reject/query.py rename to src/api/endpoints/proposals/agencies/by_id/reject/query.py index 0635a58d..e7038b4f 100644 --- a/src/api/endpoints/proposals/agencies/reject/query.py +++ b/src/api/endpoints/proposals/agencies/by_id/reject/query.py @@ -2,8 +2,8 @@ from sqlalchemy import select, RowMapping, update from sqlalchemy.ext.asyncio import AsyncSession -from src.api.endpoints.proposals.agencies.reject.request import ProposalAgencyRejectRequestModel -from src.api.endpoints.proposals.agencies.reject.response import ProposalAgencyRejectResponse +from src.api.endpoints.proposals.agencies.by_id.reject.request import ProposalAgencyRejectRequestModel +from src.api.endpoints.proposals.agencies.by_id.reject.response import ProposalAgencyRejectResponse from src.db.models.impl.proposals.agency_.core import ProposalAgency from src.db.models.impl.proposals.agency_.decision_info import ProposalAgencyDecisionInfo from src.db.models.impl.proposals.enums import ProposalStatus diff --git a/src/api/endpoints/proposals/agencies/reject/request.py b/src/api/endpoints/proposals/agencies/by_id/reject/request.py similarity index 100% rename from src/api/endpoints/proposals/agencies/reject/request.py rename to src/api/endpoints/proposals/agencies/by_id/reject/request.py diff --git a/src/api/endpoints/proposals/agencies/reject/response.py b/src/api/endpoints/proposals/agencies/by_id/reject/response.py similarity index 100% rename from src/api/endpoints/proposals/agencies/reject/response.py rename to src/api/endpoints/proposals/agencies/by_id/reject/response.py diff --git a/src/api/endpoints/proposals/agencies/root/__init__.py b/src/api/endpoints/proposals/agencies/root/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/api/endpoints/proposals/agencies/root/get/__init__.py b/src/api/endpoints/proposals/agencies/root/get/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/api/endpoints/proposals/agencies/get/query.py b/src/api/endpoints/proposals/agencies/root/get/query.py similarity index 94% rename from src/api/endpoints/proposals/agencies/get/query.py rename to src/api/endpoints/proposals/agencies/root/get/query.py index dde61c90..6f4df84d 100644 --- a/src/api/endpoints/proposals/agencies/get/query.py +++ b/src/api/endpoints/proposals/agencies/root/get/query.py @@ -5,7 +5,7 @@ from sqlalchemy.orm import joinedload from src.api.endpoints.agencies.by_id.locations.get.response import AgencyGetLocationsResponse -from src.api.endpoints.proposals.agencies.get.response import ProposalAgencyGetOuterResponse, ProposalAgencyGetResponse +from src.api.endpoints.proposals.agencies.root.get.response import ProposalAgencyGetOuterResponse, ProposalAgencyGetResponse from src.db.models.impl.proposals.agency_.core import ProposalAgency from src.db.models.impl.proposals.enums import ProposalStatus from src.db.queries.base.builder import QueryBuilderBase diff --git a/src/api/endpoints/proposals/agencies/get/response.py b/src/api/endpoints/proposals/agencies/root/get/response.py similarity index 100% rename from src/api/endpoints/proposals/agencies/get/response.py rename to src/api/endpoints/proposals/agencies/root/get/response.py diff --git a/src/api/endpoints/proposals/routes.py b/src/api/endpoints/proposals/routes.py index 8371c604..147e0501 100644 --- a/src/api/endpoints/proposals/routes.py +++ b/src/api/endpoints/proposals/routes.py @@ -1,13 +1,21 @@ from fastapi import APIRouter, Depends, Path from src.api.dependencies import get_async_core -from src.api.endpoints.proposals.agencies.approve.query import ProposalAgencyApproveQueryBuilder -from src.api.endpoints.proposals.agencies.approve.response import ProposalAgencyApproveResponse -from src.api.endpoints.proposals.agencies.get.query import ProposalAgencyGetQueryBuilder -from src.api.endpoints.proposals.agencies.get.response import ProposalAgencyGetOuterResponse -from src.api.endpoints.proposals.agencies.reject.query import ProposalAgencyRejectQueryBuilder -from src.api.endpoints.proposals.agencies.reject.request import ProposalAgencyRejectRequestModel -from src.api.endpoints.proposals.agencies.reject.response import ProposalAgencyRejectResponse +from src.api.endpoints.agencies.by_id.locations.get.response import AgencyGetLocationsResponse +from src.api.endpoints.proposals.agencies.by_id.approve.query import ProposalAgencyApproveQueryBuilder +from src.api.endpoints.proposals.agencies.by_id.approve.response import ProposalAgencyApproveResponse +from src.api.endpoints.proposals.agencies.by_id.locations.delete.query import DeleteProposalAgencyLocationQueryBuilder +from src.api.endpoints.proposals.agencies.by_id.locations.get.query import GetProposalAgencyLocationsQueryBuilder +from src.api.endpoints.proposals.agencies.by_id.locations.get.response import ProposalAgencyGetLocationsOuterResponse +from src.api.endpoints.proposals.agencies.by_id.locations.post.query import AddProposalAgencyLocationQueryBuilder +from src.api.endpoints.proposals.agencies.by_id.put.query import UpdateProposalAgencyQueryBuilder +from src.api.endpoints.proposals.agencies.by_id.put.request import ProposalAgencyPutRequest +from src.api.endpoints.proposals.agencies.root.get.query import ProposalAgencyGetQueryBuilder +from src.api.endpoints.proposals.agencies.root.get.response import ProposalAgencyGetOuterResponse +from src.api.endpoints.proposals.agencies.by_id.reject.query import ProposalAgencyRejectQueryBuilder +from src.api.endpoints.proposals.agencies.by_id.reject.request import ProposalAgencyRejectRequestModel +from src.api.endpoints.proposals.agencies.by_id.reject.response import ProposalAgencyRejectResponse +from src.api.shared.models.message_response import MessageResponse from src.core.core import AsyncCore from src.security.dtos.access_info import AccessInfo from src.security.manager import get_access_info @@ -53,4 +61,58 @@ async def reject_proposed_agency( deciding_user_id=access_info.user_id, request_model=request, ) - ) \ No newline at end of file + ) + +@proposal_router.get("/agencies/{proposed_agency_id}/locations") +async def get_agency_locations( + proposed_agency_id: int = Path( + description="Agency ID to get locations for" + ), + async_core: AsyncCore = Depends(get_async_core), +) -> ProposalAgencyGetLocationsOuterResponse: + return await async_core.adb_client.run_query_builder( + GetProposalAgencyLocationsQueryBuilder(agency_id=proposed_agency_id) + ) + +@proposal_router.post("/agencies/{proposed_agency_id}/locations/{location_id}") +async def add_location_to_agency( + proposed_agency_id: int = Path( + description="Agency ID to add location to" + ), + location_id: int = Path( + description="Location ID to add" + ), + async_core: AsyncCore = Depends(get_async_core), +) -> MessageResponse: + await async_core.adb_client.run_query_builder( + AddProposalAgencyLocationQueryBuilder(agency_id=proposed_agency_id, location_id=location_id) + ) + return MessageResponse(message="Location added to agency.") + +@proposal_router.delete("/agencies/{proposed_agency_id}/locations/{location_id}") +async def remove_location_from_agency( + proposed_agency_id: int = Path( + description="Agency ID to remove location from" + ), + location_id: int = Path( + description="Location ID to remove" + ), + async_core: AsyncCore = Depends(get_async_core), +) -> MessageResponse: + await async_core.adb_client.run_query_builder( + DeleteProposalAgencyLocationQueryBuilder(agency_id=proposed_agency_id, location_id=location_id) + ) + return MessageResponse(message="Location removed from agency.") + +@proposal_router.put("/agencies/{proposed_agency_id}") +async def update_agency( + request: ProposalAgencyPutRequest, + proposed_agency_id: int = Path( + description="Agency ID to update" + ), + async_core: AsyncCore = Depends(get_async_core), +) -> MessageResponse: + await async_core.adb_client.run_query_builder( + UpdateProposalAgencyQueryBuilder(agency_id=proposed_agency_id, request=request) + ) + return MessageResponse(message="Proposed agency updated.") diff --git a/tests/automated/integration/api/proposals/test_agencies.py b/tests/automated/integration/api/proposals/test_agencies.py index 31037f12..d1a2d2ab 100644 --- a/tests/automated/integration/api/proposals/test_agencies.py +++ b/tests/automated/integration/api/proposals/test_agencies.py @@ -1,12 +1,15 @@ import pytest -from src.api.endpoints.proposals.agencies.approve.response import ProposalAgencyApproveResponse -from src.api.endpoints.proposals.agencies.get.response import ProposalAgencyGetOuterResponse -from src.api.endpoints.proposals.agencies.reject.request import ProposalAgencyRejectRequestModel -from src.api.endpoints.proposals.agencies.reject.response import ProposalAgencyRejectResponse +from src.api.endpoints.proposals.agencies.by_id.approve.response import ProposalAgencyApproveResponse +from src.api.endpoints.proposals.agencies.by_id.locations.get.response import ProposalAgencyGetLocationsOuterResponse +from src.api.endpoints.proposals.agencies.by_id.put.request import ProposalAgencyPutRequest +from src.api.endpoints.proposals.agencies.root.get.response import ProposalAgencyGetOuterResponse +from src.api.endpoints.proposals.agencies.by_id.reject.request import ProposalAgencyRejectRequestModel +from src.api.endpoints.proposals.agencies.by_id.reject.response import ProposalAgencyRejectResponse from src.api.endpoints.submit.agency.enums import AgencyProposalRequestStatus from src.api.endpoints.submit.agency.request import SubmitAgencyRequestModel from src.api.endpoints.submit.agency.response import SubmitAgencyProposalResponse +from src.api.shared.models.message_response import MessageResponse from src.db.client.async_ import AsyncDatabaseClient from src.db.models.impl.agency.enums import AgencyType, JurisdictionType from src.db.models.impl.agency.sqlalchemy import Agency @@ -16,13 +19,15 @@ from tests.helpers.api_test_helper import APITestHelper from tests.helpers.data_creator.models.creation_info.county import CountyCreationInfo from tests.helpers.data_creator.models.creation_info.locality import LocalityCreationInfo +from tests.helpers.data_creator.models.creation_info.us_state import USStateCreationInfo @pytest.mark.asyncio async def test_agencies( api_test_helper: APITestHelper, pittsburgh_locality: LocalityCreationInfo, - allegheny_county: CountyCreationInfo + allegheny_county: CountyCreationInfo, + pennsylvania: USStateCreationInfo ): request = SubmitAgencyRequestModel( name="test_agency", @@ -71,6 +76,78 @@ async def test_agencies( assert [loc.location_id for loc in proposal.locations] == request.location_ids assert proposal.created_at is not None + # Edit Endpoint + edit_response: MessageResponse = rv.put_v3( + f"/proposal/agencies/{proposal_id}", + expected_model=MessageResponse, + json=ProposalAgencyPutRequest( + name='Modified Agency', + type=AgencyType.AGGREGATED, + jurisdiction_type=JurisdictionType.COUNTY, + ).model_dump(mode="json") + ) + assert edit_response.message == "Proposed agency updated." + + # Confirm agency proposal is updated + get_response_1p5: ProposalAgencyGetOuterResponse = rv.get_v3( + "/proposal/agencies", + expected_model=ProposalAgencyGetOuterResponse + ) + # Confirm agency is in response + assert len(get_response_1p5.results) == 1 + proposal = get_response_1p5.results[0] + assert proposal.id == proposal_id + assert proposal.name == 'Modified Agency' + assert proposal.proposing_user_id == MOCK_USER_ID + assert proposal.agency_type == AgencyType.AGGREGATED + assert proposal.jurisdiction_type == JurisdictionType.COUNTY + assert [loc.location_id for loc in proposal.locations] == request.location_ids + assert proposal.created_at is not None + + + # Get locations for endpoint + get_locations_response: ProposalAgencyGetLocationsOuterResponse = rv.get_v3( + f"/proposal/agencies/{proposal_id}/locations", + expected_model=ProposalAgencyGetLocationsOuterResponse + ) + assert len(get_locations_response.results) == 2 + # Check Location IDs match + assert {loc.location_id for loc in get_locations_response.results} == { + allegheny_county.location_id, + pittsburgh_locality.location_id + } + + # Add location to endpoint + add_locations_response: MessageResponse = rv.post_v3( + f"/proposal/agencies/{proposal_id}/locations/{pennsylvania.location_id}" + ) + # Check that location is added + get_locations_response: ProposalAgencyGetLocationsOuterResponse = rv.get_v3( + f"/proposal/agencies/{proposal_id}/locations", + expected_model=ProposalAgencyGetLocationsOuterResponse + ) + assert len(get_locations_response.results) == 3 + assert {loc.location_id for loc in get_locations_response.results} == { + allegheny_county.location_id, + pittsburgh_locality.location_id, + pennsylvania.location_id + } + + # Remove Location from endpoint + remove_location_response: MessageResponse = rv.delete_v3( + f"/proposal/agencies/{proposal_id}/locations/{pennsylvania.location_id}" + ) + # Check that location is removed + get_locations_response: ProposalAgencyGetLocationsOuterResponse = rv.get_v3( + f"/proposal/agencies/{proposal_id}/locations", + expected_model=ProposalAgencyGetLocationsOuterResponse + ) + assert len(get_locations_response.results) == 2 + assert {loc.location_id for loc in get_locations_response.results} == { + allegheny_county.location_id, + pittsburgh_locality.location_id, + } + # Call APPROVE endpoint approve_response: ProposalAgencyApproveResponse = rv.post_v3( f"/proposal/agencies/{proposal_id}/approve", @@ -86,9 +163,9 @@ async def test_agencies( assert len(agencies) == 1 agency = agencies[0] assert agency.id == agency_id - assert agency.name == request.name - assert agency.agency_type == request.agency_type - assert agency.jurisdiction_type == request.jurisdiction_type + assert agency.name == "Modified Agency" + assert agency.agency_type == AgencyType.AGGREGATED + assert agency.jurisdiction_type == JurisdictionType.COUNTY links: list[LinkAgencyLocation] = await adb_client.get_all(LinkAgencyLocation) assert len(links) == 2 @@ -107,7 +184,15 @@ async def test_agencies( submit_response_accepted_duplicate: SubmitAgencyProposalResponse = rv.post_v3( "/submit/agency", expected_model=SubmitAgencyProposalResponse, - json=request.model_dump(mode="json") + json=SubmitAgencyRequestModel( + name='Modified Agency', + agency_type=AgencyType.AGGREGATED, + jurisdiction_type=JurisdictionType.COUNTY, + location_ids=[ + allegheny_county.location_id, + pittsburgh_locality.location_id + ] + ).model_dump(mode="json") ) assert submit_response_accepted_duplicate.status == AgencyProposalRequestStatus.ACCEPTED_DUPLICATE assert submit_response_accepted_duplicate.proposal_id is None