From d29e0bec5d698edc559c16506a10f1dc1c188604 Mon Sep 17 00:00:00 2001 From: "Joseph T. French" Date: Mon, 5 Oct 2026 17:00:55 -0500 Subject: [PATCH] feat(ledger): resolve a reconciling item against the preview it was decided on resolve_reconciling_item takes expected_drift_detected_at, the preview's drift_detected_at; the server refuses the resolution when the item was flagged again since. Optional, so existing calls are unchanged. --- robosystems_client/clients/ledger_client.py | 12 +++++++ .../resolve_reconciling_item_request.py | 36 +++++++++++++++++++ tests/test_ledger_client.py | 15 ++++++++ 3 files changed, 63 insertions(+) diff --git a/robosystems_client/clients/ledger_client.py b/robosystems_client/clients/ledger_client.py index 525cf1a..781fceb 100644 --- a/robosystems_client/clients/ledger_client.py +++ b/robosystems_client/clients/ledger_client.py @@ -2306,6 +2306,7 @@ def resolve_reconciling_item( status: str | None = None, note: str | None = None, reference_event_id: str | None = None, + expected_drift_detected_at: str | datetime.datetime | None = None, ) -> ResolveReconcilingItemResponse: """Decide one reconciling item. @@ -2315,11 +2316,22 @@ def resolve_reconciling_item( preview reports. ``posting_date`` and ``status`` (``draft`` or ``posted``) apply to ``catch_up`` only; ``reference_event_id`` to ``acknowledge`` only. + + Pass ``expected_drift_detected_at`` as the ``drift_detected_at`` of the + preview the decision was made on: the resolution is refused if the item + was flagged again since, and must be previewed again. """ if isinstance(posting_date, str): posting_date = datetime.date.fromisoformat(posting_date) + if isinstance(expected_drift_detected_at, str): + expected_drift_detected_at = datetime.datetime.fromisoformat( + expected_drift_detected_at + ) body = ResolveReconcilingItemRequest( event_id=event_id, + expected_drift_detected_at=( + expected_drift_detected_at if expected_drift_detected_at is not None else UNSET + ), disposition=( ResolveReconcilingItemRequestDispositionType0(disposition) if disposition is not None diff --git a/robosystems_client/models/resolve_reconciling_item_request.py b/robosystems_client/models/resolve_reconciling_item_request.py index c98348d..a17ac7a 100644 --- a/robosystems_client/models/resolve_reconciling_item_request.py +++ b/robosystems_client/models/resolve_reconciling_item_request.py @@ -24,6 +24,9 @@ class ResolveReconcilingItemRequest: Attributes: event_id (str): Event id (evt_ prefixed) to resolve + expected_drift_detected_at (datetime.datetime | None | Unset): The `drift_detected_at` of the preview this + resolution was decided on. Pass it: the resolution is refused when the item has been flagged again since, with a + newer payload nobody has seen, and must be previewed again. disposition (None | ResolveReconcilingItemRequestDispositionType0 | Unset): How to dispose of the difference. Omit to take the default the preview reports: restate when every period the event touches is open, catch_up when any is closed. @@ -39,6 +42,7 @@ class ResolveReconcilingItemRequest: """ event_id: str + expected_drift_detected_at: datetime.datetime | None | Unset = UNSET disposition: None | ResolveReconcilingItemRequestDispositionType0 | Unset = UNSET posting_date: datetime.date | None | Unset = UNSET status: ResolveReconcilingItemRequestStatus | Unset = ( @@ -51,6 +55,14 @@ class ResolveReconcilingItemRequest: def to_dict(self) -> dict[str, Any]: event_id = self.event_id + expected_drift_detected_at: None | str | Unset + if isinstance(self.expected_drift_detected_at, Unset): + expected_drift_detected_at = UNSET + elif isinstance(self.expected_drift_detected_at, datetime.datetime): + expected_drift_detected_at = self.expected_drift_detected_at.isoformat() + else: + expected_drift_detected_at = self.expected_drift_detected_at + disposition: None | str | Unset if isinstance(self.disposition, Unset): disposition = UNSET @@ -90,6 +102,8 @@ def to_dict(self) -> dict[str, Any]: "event_id": event_id, } ) + if expected_drift_detected_at is not UNSET: + field_dict["expected_drift_detected_at"] = expected_drift_detected_at if disposition is not UNSET: field_dict["disposition"] = disposition if posting_date is not UNSET: @@ -108,6 +122,27 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: d = dict(src_dict) event_id = d.pop("event_id") + def _parse_expected_drift_detected_at( + data: object, + ) -> datetime.datetime | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + expected_drift_detected_at_type_0 = datetime.datetime.fromisoformat(data) + + return expected_drift_detected_at_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(datetime.datetime | None | Unset, data) + + expected_drift_detected_at = _parse_expected_drift_detected_at( + d.pop("expected_drift_detected_at", UNSET) + ) + def _parse_disposition( data: object, ) -> None | ResolveReconcilingItemRequestDispositionType0 | Unset: @@ -171,6 +206,7 @@ def _parse_reference_event_id(data: object) -> None | str | Unset: resolve_reconciling_item_request = cls( event_id=event_id, + expected_drift_detected_at=expected_drift_detected_at, disposition=disposition, posting_date=posting_date, status=status, diff --git a/tests/test_ledger_client.py b/tests/test_ledger_client.py index dccefa2..1ba6194 100644 --- a/tests/test_ledger_client.py +++ b/tests/test_ledger_client.py @@ -550,6 +550,21 @@ def test_resolve_reconciling_item_sends_the_chosen_treatment( "note": "Vendor credit arrived late", } + @patch("robosystems_client.clients.ledger_client.op_resolve_reconciling_item") + def test_resolve_reconciling_item_sends_the_previewed_stamp( + self, mock_op, mock_config, graph_id + ): + envelope = _envelope("resolve-reconciling-item", {"event_id": "evt_1"}) + mock_op.return_value = _mock_response(envelope) + client = LedgerClient(mock_config) + client.resolve_reconciling_item( + graph_id, "evt_1", expected_drift_detected_at="2026-08-21T04:00:00+00:00" + ) + assert mock_op.call_args.kwargs["body"].to_dict() == { + "event_id": "evt_1", + "expected_drift_detected_at": "2026-08-21T04:00:00+00:00", + } + def test_resolve_reconciling_item_refuses_an_unknown_treatment( self, mock_config, graph_id ):