fix: add Commands (receiver) to CPO version details for all OCPI versions#22
Conversation
…sions Add ModuleID.commands with InterfaceRole.receiver to the CPO ENDPOINTS_LIST for OCPI 2.1.1, 2.2.1, and 2.3.0. Without this, Payter and other eMSP/PTP partners could not discover the Commands endpoint via GET version details, blocking START_SESSION/STOP_SESSION flows. Also add credentials SENDER and payments SENDER roles to CPO endpoints for 2.2.1 and 2.3.0, as recommended by Payter to support proactive credential updates and payment data push. To support multiple interface roles per module (e.g. credentials with both SENDER and RECEIVER), convert ENDPOINTS_LIST from dict[ModuleID, Endpoint] to list[Endpoint] in all six endpoint files (CPO + eMSP for each version). Update main.py to iterate the list instead of using dict.get(). All existing tests pass. Co-authored-by: Cursor <cursoragent@cursor.com>
alfonsosastre
left a comment
There was a problem hiding this comment.
Code Review
The fix is correct and well-scoped. The root cause (missing commands RECEIVER entry for CPO) directly matches the Payter error, and the dict → list refactor is the right structural change to support multiple interface roles per module. The main.py two-pass logic (collect modules with routers, then iterate all endpoints filtering by that set) handles the multi-role case cleanly.
Verified
elu_ocpi/main.pyalready passesModuleID.commandsin themoduleslist (line 280), so the endpoint will be advertised after this library is updated.- All 6 endpoint files (CPO + eMSP × 3 versions) are updated consistently.
- OCPI spec compliance looks correct: Commands RECEIVER for CPO (CPO receives
START_SESSION/STOP_SESSIONfrom eMSP), Credentials SENDER for CPO (proactive token push), Payments SENDER for CPO (DirectPayment data push to eMSPs).
Issues
Moderate — missing test for the core behaviour
The test plan items are manual checkboxes. There is no automated test that verifies GET /ocpi/{version}/details actually returns commands with role=RECEIVER for a CPO app. The existing test_get_versions_v_2_2_1 test passes modules=[], so it exercises none of the new filtering logic; the assertion len(response.json()["data"]) == 2 is checking keys in VersionDetail (version + endpoints), not the number of endpoints.
Suggest adding a test like:
def test_version_details_includes_commands_for_cpo():
app = get_application(
version_numbers=[VersionNumber.v_2_2_1],
roles=[enums.RoleEnum.cpo],
modules=[enums.ModuleID.commands],
crud=MockCrud,
authenticator=ClientAuthenticator,
)
client = TestClient(app)
response = client.get("/ocpi/2.2.1/details", headers=AUTH_HEADERS)
assert response.status_code == 200
endpoints = response.json()["data"]["endpoints"]
commands_endpoints = [e for e in endpoints if e["identifier"] == "commands"]
assert len(commands_endpoints) == 1
assert commands_endpoints[0]["role"] == "RECEIVER"This would also serve as a regression guard for the Payter bug.
Minor — CREDENTIALS_SENDER omitted for OCPI 2.1.1, silently
credentials SENDER is added for 2.2.1 and 2.3.0 but not 2.1.1. This is correct because the 2.1.1 Endpoint schema has no role field (InterfaceRole is a 2.2.1+ concept), but there is nothing in the code to signal this to future maintainers. A short comment in v_2_1_1/cpo.py would help:
# OCPI 2.1.1 Endpoint schema has no InterfaceRole field;
# credentials SENDER role is implicit and not separately advertised.Minor — no type annotation on ENDPOINTS_LIST
The variable changed from dict[ModuleID, Endpoint] to list[Endpoint]. Adding an explicit annotation makes the change self-documenting and helps mypy catch any future accidental reversion:
ENDPOINTS_LIST: list[Endpoint] = [...]Summary
Approve with suggestions — the fix is correct and unblocks Payter's START_SESSION flow. The moderate item (automated test) is worth doing before merging so the fix is regression-guarded; the minor items are optional cleanup.
- Add list[Endpoint] type annotation to ENDPOINTS_LIST in all 6 endpoint files (CPO + eMSP × 3 versions) so mypy catches accidental reversion - Add comment in v_2_1_1/cpo.py explaining why credentials SENDER is omitted (InterfaceRole is a 2.2.1+ concept) - Add regression tests for commands RECEIVER discovery in version details for all three OCPI versions; add credentials SENDER/RECEIVER tests for 2.2.1 and 2.3.0; add payments SENDER/RECEIVER test for 2.3.0 - Fix tests/test_modules/mocks/async_client.py to use list iteration instead of dict key lookup after ENDPOINTS_LIST dict→list migration All 457 tests pass. Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: handle OCPI version negotiation in push_object
push_object now accepts both a versions URL and a version details URL
as endpoints_url. When a versions list is returned (data is a list),
it automatically picks the best mutual version and fetches the details
URL to discover endpoints. This follows the proper OCPI spec flow:
1. GET /versions → list of supported versions with details URLs
2. Pick best mutual version
3. GET /{version}/details → list of endpoints
4. Push to the correct module endpoint
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: ensure trailing slash in client_url for push requests
The endpoint URL from version details doesn't include a trailing
slash, causing the country_code to be concatenated directly
(e.g. locationsDE/ELU/... instead of locations/DE/ELU/...).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: add Commands (receiver) to CPO version details for all OCPI versions (#22)
* feat: add Commands (receiver) to CPO version details for all OCPI versions
Add ModuleID.commands with InterfaceRole.receiver to the CPO ENDPOINTS_LIST
for OCPI 2.1.1, 2.2.1, and 2.3.0. Without this, Payter and other eMSP/PTP
partners could not discover the Commands endpoint via GET version details,
blocking START_SESSION/STOP_SESSION flows.
Also add credentials SENDER and payments SENDER roles to CPO endpoints for
2.2.1 and 2.3.0, as recommended by Payter to support proactive credential
updates and payment data push.
To support multiple interface roles per module (e.g. credentials with both
SENDER and RECEIVER), convert ENDPOINTS_LIST from dict[ModuleID, Endpoint]
to list[Endpoint] in all six endpoint files (CPO + eMSP for each version).
Update main.py to iterate the list instead of using dict.get().
All existing tests pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: address review suggestions for commands CPO endpoint PR
- Add list[Endpoint] type annotation to ENDPOINTS_LIST in all 6 endpoint
files (CPO + eMSP × 3 versions) so mypy catches accidental reversion
- Add comment in v_2_1_1/cpo.py explaining why credentials SENDER is
omitted (InterfaceRole is a 2.2.1+ concept)
- Add regression tests for commands RECEIVER discovery in version details
for all three OCPI versions; add credentials SENDER/RECEIVER tests for
2.2.1 and 2.3.0; add payments SENDER/RECEIVER test for 2.3.0
- Fix tests/test_modules/mocks/async_client.py to use list iteration
instead of dict key lookup after ENDPOINTS_LIST dict→list migration
All 457 tests pass.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Alfonso Sastre <alfonso@elumobility.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: harden push version negotiation and add missing test coverage
- Guard missing "url" key in _pick_version_details_url so malformed
version entries are skipped instead of raising KeyError
- Clarify docstring: fallback may select a version newer than requested
- Add response.raise_for_status() after both HTTP fetches in push_object
so 4xx/5xx responses raise httpx.HTTPStatusError instead of silently
failing with a KeyError on response.json()["data"]
- Add raise_for_status() to MockResponse in async_client.py (required
after wiring raise_for_status into push_object); also add httpx/
MagicMock imports and replace bare next() with next(..., None) +
assertion for a clearer failure message
- Add 11 new tests covering: _pick_version_details_url (exact match,
fallback, no mutual version, empty list, missing url/version keys),
push_object version negotiation two-GET flow, no-mutual-version
ValueError, and HTTP error propagation on both GET requests
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs: document eMSP credentials SENDER gap and VERSION_PREFERENCE maintenance note
- Add comment in v_2_2_1/emsp.py and v_2_3_0/emsp.py explaining why credentials
is RECEIVER-only for eMSP and when to add SENDER if needed
- Add comment on _VERSION_PREFERENCE in push.py noting it must be updated
when new OCPI versions are added
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: apply ruff formatting to push.py (I001 import sort + line length)
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: apply ruff formatting to test files
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Alfonso Sastre <alfonso@elumobility.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
ModuleID.commandswithInterfaceRole.receiverto CPOENDPOINTS_LISTfor OCPI 2.1.1, 2.2.1, and 2.3.0 so that Payter and other eMSP/PTP partners can discover the Commands endpoint viaGET /ocpi/{version}/detailscredentials SENDERrole to CPO endpoints for 2.2.1 and 2.3.0 as recommended by Payterpayments SENDERrole to CPO endpoints for 2.3.0 as recommended by PayterENDPOINTS_LISTfromdict[ModuleID, Endpoint]tolist[Endpoint]in all 6 endpoint files (CPO + eMSP × 3 versions) to support multiple interface roles per module; updatesmain.pyto iterate the list instead of calling.get(module)Context
Payter reported:
The command handler in
elu_ocpi/crud.pywas fully implemented but not discoverable becauseModuleID.commandswas missing from the CPOENDPOINTS_LISTin the ocpi-python library.Test plan
pytest tests/— 43 tests passing)GET /ocpi/2.2.1/detailsincludes acommandsentry with roleRECEIVERGET /ocpi/2.2.1/detailsincludescredentialswith bothSENDERandRECEIVERSTART_SESSIONviaPOST /ocpi/cpo/2.2.1/commands/START_SESSIONMade with Cursor