diff --git a/src/qodev_apollo_api/client.py b/src/qodev_apollo_api/client.py index e862cde..bb895c3 100644 --- a/src/qodev_apollo_api/client.py +++ b/src/qodev_apollo_api/client.py @@ -545,7 +545,26 @@ async def update_opportunity_roles( Returns: The updated Deal. """ - data = {"opportunity_id": opportunity_id, "roles": roles} + # Apollo's endpoint expects each entry's role type *nested* under a ``role`` + # array — sending ``opportunity_contact_role_type_id`` flat on the entry (with + # no ``role`` key) makes the server call ``.map`` on nil and 422 with + # "undefined method 'map' for nil". Reshape the flat RoleAssignment entries + # into the wire format the server actually accepts. + wire_roles: list[dict] = [] + for entry in roles: + # RoleAssignment types is_primary as a bool; default to False when omitted. + # Avoid bool(...) coercion, which would turn a stray truthy non-bool (e.g. + # the string "false") into True. + is_primary = entry.get("is_primary", False) + role_obj: dict[str, Any] = {"is_primary": is_primary} + role_type_id = entry.get("opportunity_contact_role_type_id") + if role_type_id: + role_obj["opportunity_contact_role_type_id"] = role_type_id + wire_roles.append( + {"contact_id": entry["contact_id"], "is_primary": is_primary, "role": [role_obj]} + ) + + data = {"opportunity_id": opportunity_id, "roles": wire_roles} result = await self._post("/opportunities/update_roles", data) return Deal.model_validate(result.get("opportunity", result)) diff --git a/tests/test_client.py b/tests/test_client.py index 6406a78..8ffefa3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -643,7 +643,11 @@ async def test_list_opportunity_contact_role_types(client: ApolloClient): async def test_update_opportunity_roles(client: ApolloClient): - """Test POST /opportunities/update_roles returns the updated Deal with the roles body.""" + """The flat RoleAssignment entries are reshaped into Apollo's nested ``role`` wire format. + + Regression: sending ``opportunity_contact_role_type_id`` flat on the entry (no + ``role`` key) makes Apollo 422 with "undefined method 'map' for nil". + """ client._client.request.return_value = _make_response( {"opportunity": {"id": "d1", "name": "Big Deal"}} ) @@ -659,7 +663,19 @@ async def test_update_opportunity_roles(client: ApolloClient): call_args = client._client.request.call_args assert call_args[0] == ("POST", "/opportunities/update_roles") - assert call_args[1]["json"] == {"opportunity_id": "d1", "roles": roles} + assert call_args[1]["json"] == { + "opportunity_id": "d1", + "roles": [ + { + "contact_id": "c1", + "is_primary": True, + "role": [{"is_primary": True, "opportunity_contact_role_type_id": "rt1"}], + }, + # No role type → the nested role object carries only is_primary (never a + # flat/absent role type, which is what triggered the nil.map crash). + {"contact_id": "c2", "is_primary": False, "role": [{"is_primary": False}]}, + ], + } async def test_list_custom_fields(client: ApolloClient):