Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/qodev_apollo_api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
20 changes: 18 additions & 2 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}
)
Expand All @@ -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):
Expand Down
Loading