From b60072d3f430083a7ca368b265c3a09a07002727 Mon Sep 17 00:00:00 2001 From: Tarek Lokal Date: Sat, 11 Jul 2026 16:18:16 +0200 Subject: [PATCH] fix(spp_dci_server): accept DCI envelope on registry alias search (#217) Disability/crvs/farmer sync/search stubs now accept DCIEnvelope requests and return per-item on-search envelopes, fixing multi-item batch handling. Co-authored-by: Cursor --- spp_dci_server/routers/registry_aliases.py | 172 +++++++++--------- spp_dci_server/tests/test_callback_routers.py | 29 ++- 2 files changed, 113 insertions(+), 88 deletions(-) diff --git a/spp_dci_server/routers/registry_aliases.py b/spp_dci_server/routers/registry_aliases.py index f0e21fc9..9c3507ae 100644 --- a/spp_dci_server/routers/registry_aliases.py +++ b/spp_dci_server/routers/registry_aliases.py @@ -11,37 +11,100 @@ """ import logging -from datetime import datetime +import uuid +from datetime import UTC, datetime from typing import Annotated from odoo.api import Environment from odoo.addons.fastapi.dependencies import odoo_env +from odoo.addons.spp_dci.schemas import DCICallbackHeader, DCIEnvelope from odoo.addons.spp_dci.schemas.constants import MsgHeaderStatusReasonCode from odoo.addons.spp_dci.schemas.search import ( SearchRequest, SearchResponse, SearchResponseItem, ) +from odoo.addons.spp_dci.services import get_sender_id -from fastapi import APIRouter, Depends, status +from fastapi import APIRouter, Depends, HTTPException, status from ..middleware.signature import verify_bearer_token _logger = logging.getLogger(__name__) + +def _build_stub_search_envelope( + request_envelope: DCIEnvelope, + env: Environment, + not_implemented_message: str, +) -> DCIEnvelope: + """Return a DCI on-search envelope with per-item rjct stubs.""" + try: + search_request = SearchRequest.model_validate(request_envelope.message) + except Exception as e: + _logger.error("Invalid SearchRequest message: %s", str(e)) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid search request message: {str(e)}", + ) from e + + response_items = [] + for req_item in search_request.search_request: + response_items.append( + SearchResponseItem( + reference_id=req_item.reference_id, + timestamp=datetime.now(UTC), + status="rjct", + status_reason_code=MsgHeaderStatusReasonCode.ACTION_NOT_SUPPORTED.value, + status_reason_message=not_implemented_message, + data=None, + pagination=None, + locale=req_item.locale, + ) + ) + + search_response = SearchResponse( + transaction_id=search_request.transaction_id, + correlation_id=str(uuid.uuid4()), + search_response=response_items, + ) + + our_sender_id = get_sender_id(env) or "openspp" + total_count = len(response_items) + + callback_header = DCICallbackHeader( + version=request_envelope.header.version, + message_id=str(uuid.uuid4()), + message_ts=datetime.now(UTC), + action=f"on-{request_envelope.header.action}", + sender_id=our_sender_id, + receiver_id=request_envelope.header.sender_id, + total_count=total_count, + status="rjct", + status_reason_code=MsgHeaderStatusReasonCode.ACTION_NOT_SUPPORTED.value, + status_reason_message=not_implemented_message, + completed_count=0, + ) + + return DCIEnvelope( + signature="", + header=callback_header, + message=search_response.model_dump(mode="json", exclude_none=True), + ) + + # Disability Registry router disability_router = APIRouter(tags=["Disability Registry"], prefix="/disability/registry") @disability_router.post( "/sync/search", - response_model=SearchResponse, + response_model=DCIEnvelope, response_model_exclude_none=True, - status_code=status.HTTP_501_NOT_IMPLEMENTED, ) async def disability_sync_search( - request: SearchRequest, + request_envelope: DCIEnvelope, env: Annotated[Environment, Depends(odoo_env)], _bearer_token: Annotated[str, Depends(verify_bearer_token)], ): @@ -54,29 +117,10 @@ async def disability_sync_search( For now, returns "not implemented" response for SPDCI compliance testing. """ _logger.warning("Disability Registry search endpoint called but not yet implemented") - - # Build response items with "not implemented" status - response_items = [] - for req_item in request.search_request: - response_items.append( - SearchResponseItem( - reference_id=req_item.reference_id, - timestamp=datetime.utcnow(), - status="rjct", - status_reason_code=MsgHeaderStatusReasonCode.ACTION_NOT_SUPPORTED.value, - status_reason_message=( - "Disability Registry not yet implemented. Will be available in spp_dci_server_disability module." - ), - data=None, - pagination=None, - locale=req_item.locale, - ) - ) - - return SearchResponse( - transaction_id=request.transaction_id, - correlation_id=None, - search_response=response_items, + return _build_stub_search_envelope( + request_envelope, + env, + "Disability Registry not yet implemented. Will be available in spp_dci_server_disability module.", ) @@ -91,7 +135,7 @@ async def disability_sync_notify( """ SPDCI-compliant Disability Registry notification endpoint. - Path: /disability/registry/sync/notify + Path: /disability/registry/sync/search This endpoint will be implemented when the Disability Registry module is added. """ @@ -99,7 +143,7 @@ async def disability_sync_notify( return { "status": "rjct", "message": "Disability Registry not yet implemented", - "timestamp": datetime.utcnow().isoformat(), + "timestamp": datetime.now(UTC).isoformat(), } @@ -109,12 +153,11 @@ async def disability_sync_notify( @crvs_router.post( "/sync/search", - response_model=SearchResponse, + response_model=DCIEnvelope, response_model_exclude_none=True, - status_code=status.HTTP_501_NOT_IMPLEMENTED, ) async def crvs_sync_search( - request: SearchRequest, + request_envelope: DCIEnvelope, env: Annotated[Environment, Depends(odoo_env)], _bearer_token: Annotated[str, Depends(verify_bearer_token)], ): @@ -127,29 +170,10 @@ async def crvs_sync_search( For now, returns "not implemented" response for SPDCI compliance testing. """ _logger.warning("Civil Registry search endpoint called but not yet implemented") - - # Build response items with "not implemented" status - response_items = [] - for req_item in request.search_request: - response_items.append( - SearchResponseItem( - reference_id=req_item.reference_id, - timestamp=datetime.utcnow(), - status="rjct", - status_reason_code=MsgHeaderStatusReasonCode.ACTION_NOT_SUPPORTED.value, - status_reason_message=( - "Civil Registry not yet implemented. Will be available in spp_dci_server_crvs module." - ), - data=None, - pagination=None, - locale=req_item.locale, - ) - ) - - return SearchResponse( - transaction_id=request.transaction_id, - correlation_id=None, - search_response=response_items, + return _build_stub_search_envelope( + request_envelope, + env, + "Civil Registry not yet implemented. Will be available in spp_dci_server_crvs module.", ) @@ -172,7 +196,7 @@ async def crvs_sync_notify( return { "status": "rjct", "message": "Civil Registry not yet implemented", - "timestamp": datetime.utcnow().isoformat(), + "timestamp": datetime.now(UTC).isoformat(), } @@ -182,12 +206,11 @@ async def crvs_sync_notify( @farmer_router.post( "/sync/search", - response_model=SearchResponse, + response_model=DCIEnvelope, response_model_exclude_none=True, - status_code=status.HTTP_501_NOT_IMPLEMENTED, ) async def farmer_sync_search( - request: SearchRequest, + request_envelope: DCIEnvelope, env: Annotated[Environment, Depends(odoo_env)], _bearer_token: Annotated[str, Depends(verify_bearer_token)], ): @@ -200,29 +223,10 @@ async def farmer_sync_search( For now, returns "not implemented" response for SPDCI compliance testing. """ _logger.warning("Farmer Registry search endpoint called but not yet implemented") - - # Build response items with "not implemented" status - response_items = [] - for req_item in request.search_request: - response_items.append( - SearchResponseItem( - reference_id=req_item.reference_id, - timestamp=datetime.utcnow(), - status="rjct", - status_reason_code=MsgHeaderStatusReasonCode.ACTION_NOT_SUPPORTED.value, - status_reason_message=( - "Farmer Registry not yet implemented. Will be available in spp_dci_server_farmer module." - ), - data=None, - pagination=None, - locale=req_item.locale, - ) - ) - - return SearchResponse( - transaction_id=request.transaction_id, - correlation_id=None, - search_response=response_items, + return _build_stub_search_envelope( + request_envelope, + env, + "Farmer Registry not yet implemented. Will be available in spp_dci_server_farmer module.", ) @@ -245,5 +249,5 @@ async def farmer_sync_notify( return { "status": "rjct", "message": "Farmer Registry not yet implemented", - "timestamp": datetime.utcnow().isoformat(), + "timestamp": datetime.now(UTC).isoformat(), } diff --git a/spp_dci_server/tests/test_callback_routers.py b/spp_dci_server/tests/test_callback_routers.py index 85445b2c..0791e487 100644 --- a/spp_dci_server/tests/test_callback_routers.py +++ b/spp_dci_server/tests/test_callback_routers.py @@ -15,11 +15,14 @@ from odoo.tests import tagged from odoo.addons.spp_dci.schemas.constants import MsgHeaderStatusReasonCode +from odoo.addons.spp_dci.schemas.envelope import DCIMessageHeader from odoo.addons.spp_dci.schemas.search import ( SearchCriteria, SearchRequest, SearchRequestItem, + SearchResponse, ) +from odoo.addons.spp_dci.schemas import DCIEnvelope from .common import DCIServerCommon @@ -211,8 +214,24 @@ def _build_search_request(self, n_items=1): ) return SearchRequest(transaction_id="txn-alias", search_request=items) + def _build_search_envelope(self, n_items=1): + search_request = self._build_search_request(n_items=n_items) + header = DCIMessageHeader( + message_id="msg-alias-test", + message_ts=datetime.now(UTC), + action="search", + sender_id="registry-witness", + receiver_id="openspp", + total_count=n_items, + ) + return DCIEnvelope( + signature="", + header=header, + message=search_request.model_dump(mode="json"), + ) + def test_search_stubs_return_per_item_rjct(self): - request = self._build_search_request(n_items=2) + request = self._build_search_envelope(n_items=2) # Each alias mentions its corresponding spp_dci_server_* module in # the rejection message so operators can find the missing addon. expected_module = { @@ -223,9 +242,11 @@ def test_search_stubs_return_per_item_rjct(self): for registry, endpoint in self.search_endpoints.items(): with self.subTest(registry=registry): response = _run(endpoint(request, self.env, _bearer_token="t")) - self.assertEqual(response.transaction_id, "txn-alias") - self.assertEqual(len(response.search_response), 2) - for item in response.search_response: + self.assertIsInstance(response, DCIEnvelope) + search_response = SearchResponse.model_validate(response.message) + self.assertEqual(search_response.transaction_id, "txn-alias") + self.assertEqual(len(search_response.search_response), 2) + for item in search_response.search_response: self.assertEqual(item.status, "rjct") self.assertEqual( item.status_reason_code,