Skip to content

fix: Commands CPO version details + push version negotiation#23

Merged
alfonsosastre merged 7 commits intomainfrom
fix/push-version-negotiation
Feb 25, 2026
Merged

fix: Commands CPO version details + push version negotiation#23
alfonsosastre merged 7 commits intomainfrom
fix/push-version-negotiation

Conversation

@alfonsosastre
Copy link
Contributor

Summary

This PR consolidates 4 fixes/features onto main:

  1. fix: handle OCPI version negotiation in push_object — ensures push requests negotiate the correct OCPI version with the remote party
  2. fix: ensure trailing slash in client_url for push requests — prevents malformed URLs during push
  3. feat: add Commands (receiver) to CPO version details for all OCPI versions — adds ModuleID.commands with InterfaceRole.receiver to CPO ENDPOINTS_LIST for OCPI 2.1.1, 2.2.1, and 2.3.0; also adds credentials SENDER (2.2.1, 2.3.0) and payments SENDER (2.3.0) as recommended by Payter; converts ENDPOINTS_LIST from dict to list to support multiple interface roles per module
  4. fix: address review suggestions — type annotations on ENDPOINTS_LIST, explanatory comment in v2.1.1, regression tests for Commands/credentials discovery, async_client.py mock fix

Context

Payter was unable to start charge sessions because the Commands module was not advertised in the CPO version details. Error on their side:

"No commands Module found for platform ELU Mobility CREATED"

The START_SESSION/STOP_SESSION handler was already implemented in elu_ocpi/crud.py — it just wasn't discoverable.

Test plan

  • All 457 tests pass
  • GET /ocpi/2.2.1/details returns commands with role RECEIVER
  • GET /ocpi/2.2.1/details returns credentials with both SENDER and RECEIVER
  • Payter can successfully issue START_SESSION

Made with Cursor

Alfonso Sastre and others added 7 commits February 24, 2026 16:44
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>
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>
…ions (#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>
- 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>
…ntenance 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>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@alfonsosastre alfonsosastre merged commit 235c502 into main Feb 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant