diff --git a/robosystems_client/api/robo_ledger_ledger_events/learn_classification_defaults.py b/robosystems_client/api/robo_ledger_ledger_events/learn_classification_defaults.py new file mode 100644 index 0000000..83a0d1f --- /dev/null +++ b/robosystems_client/api/robo_ledger_ledger_events/learn_classification_defaults.py @@ -0,0 +1,282 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.error_response import ErrorResponse +from ...models.learn_classification_defaults_request import ( + LearnClassificationDefaultsRequest, +) +from ...models.operation_envelope_learn_classification_defaults_response import ( + OperationEnvelopeLearnClassificationDefaultsResponse, +) +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + graph_id: str, + *, + body: LearnClassificationDefaultsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + if not isinstance(idempotency_key, Unset): + headers["Idempotency-Key"] = idempotency_key + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/extensions/roboledger/{graph_id}/operations/learn-classification-defaults".format( + graph_id=quote(str(graph_id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> ErrorResponse | OperationEnvelopeLearnClassificationDefaultsResponse | None: + if response.status_code == 200: + response_200 = OperationEnvelopeLearnClassificationDefaultsResponse.from_dict( + response.json() + ) + + return response_200 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + return response_400 + + if response.status_code == 401: + response_401 = ErrorResponse.from_dict(response.json()) + + return response_401 + + if response.status_code == 403: + response_403 = ErrorResponse.from_dict(response.json()) + + return response_403 + + if response.status_code == 404: + response_404 = ErrorResponse.from_dict(response.json()) + + return response_404 + + if response.status_code == 409: + response_409 = ErrorResponse.from_dict(response.json()) + + return response_409 + + if response.status_code == 422: + response_422 = ErrorResponse.from_dict(response.json()) + + return response_422 + + if response.status_code == 429: + response_429 = ErrorResponse.from_dict(response.json()) + + return response_429 + + if response.status_code == 500: + response_500 = ErrorResponse.from_dict(response.json()) + + return response_500 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[ErrorResponse | OperationEnvelopeLearnClassificationDefaultsResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + graph_id: str, + *, + client: AuthenticatedClient, + body: LearnClassificationDefaultsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeLearnClassificationDefaultsResponse]: + """Learn Classification Defaults + + Seed each counterparty's default account from the bank lines already committed to the books: the + account most of its lines went to becomes its default, with the lines that agreed counted as + confirmations. A counterparty that already has a default keeps it. Its still-open lines are then + suggested from the default. Run it once on books that were classified before defaults were learned; + after that, every commit teaches the default by itself. `dry_run` reports what would be learned + without writing it. + + **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours + return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. + + Args: + graph_id (str): + idempotency_key (None | str | Unset): + body (LearnClassificationDefaultsRequest): Seed counterparty defaults from the bank lines + already committed. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | OperationEnvelopeLearnClassificationDefaultsResponse] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + body=body, + idempotency_key=idempotency_key, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + graph_id: str, + *, + client: AuthenticatedClient, + body: LearnClassificationDefaultsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeLearnClassificationDefaultsResponse | None: + """Learn Classification Defaults + + Seed each counterparty's default account from the bank lines already committed to the books: the + account most of its lines went to becomes its default, with the lines that agreed counted as + confirmations. A counterparty that already has a default keeps it. Its still-open lines are then + suggested from the default. Run it once on books that were classified before defaults were learned; + after that, every commit teaches the default by itself. `dry_run` reports what would be learned + without writing it. + + **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours + return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. + + Args: + graph_id (str): + idempotency_key (None | str | Unset): + body (LearnClassificationDefaultsRequest): Seed counterparty defaults from the bank lines + already committed. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | OperationEnvelopeLearnClassificationDefaultsResponse + """ + + return sync_detailed( + graph_id=graph_id, + client=client, + body=body, + idempotency_key=idempotency_key, + ).parsed + + +async def asyncio_detailed( + graph_id: str, + *, + client: AuthenticatedClient, + body: LearnClassificationDefaultsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> Response[ErrorResponse | OperationEnvelopeLearnClassificationDefaultsResponse]: + """Learn Classification Defaults + + Seed each counterparty's default account from the bank lines already committed to the books: the + account most of its lines went to becomes its default, with the lines that agreed counted as + confirmations. A counterparty that already has a default keeps it. Its still-open lines are then + suggested from the default. Run it once on books that were classified before defaults were learned; + after that, every commit teaches the default by itself. `dry_run` reports what would be learned + without writing it. + + **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours + return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. + + Args: + graph_id (str): + idempotency_key (None | str | Unset): + body (LearnClassificationDefaultsRequest): Seed counterparty defaults from the bank lines + already committed. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | OperationEnvelopeLearnClassificationDefaultsResponse] + """ + + kwargs = _get_kwargs( + graph_id=graph_id, + body=body, + idempotency_key=idempotency_key, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + graph_id: str, + *, + client: AuthenticatedClient, + body: LearnClassificationDefaultsRequest, + idempotency_key: None | str | Unset = UNSET, +) -> ErrorResponse | OperationEnvelopeLearnClassificationDefaultsResponse | None: + """Learn Classification Defaults + + Seed each counterparty's default account from the bank lines already committed to the books: the + account most of its lines went to becomes its default, with the lines that agreed counted as + confirmations. A counterparty that already has a default keeps it. Its still-open lines are then + suggested from the default. Run it once on books that were classified before defaults were learned; + after that, every commit teaches the default by itself. `dry_run` reports what would be learned + without writing it. + + **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours + return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. + + Args: + graph_id (str): + idempotency_key (None | str | Unset): + body (LearnClassificationDefaultsRequest): Seed counterparty defaults from the bank lines + already committed. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | OperationEnvelopeLearnClassificationDefaultsResponse + """ + + return ( + await asyncio_detailed( + graph_id=graph_id, + client=client, + body=body, + idempotency_key=idempotency_key, + ) + ).parsed diff --git a/robosystems_client/api/robo_ledger_ledger_events/update_agent.py b/robosystems_client/api/robo_ledger_ledger_events/update_agent.py index 4f7f2ea..c13ef69 100644 --- a/robosystems_client/api/robo_ledger_ledger_events/update_agent.py +++ b/robosystems_client/api/robo_ledger_ledger_events/update_agent.py @@ -115,6 +115,10 @@ def sync_detailed( Patch counterparty fields. Only supplied fields are updated. Set is_active=false to deactivate (agents are never deleted — they are reference data referenced by events and transactions). + `classification_element_id` sets the account this counterparty's bank lines are suggested (an empty + string clears it), and `classification_mode='always_ask'` stops suggesting one for a counterparty + whose lines go to different accounts; its still-open lines are re-suggested at once. A default is + also learned as lines are committed, so set one by hand only to correct it. **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. @@ -158,6 +162,10 @@ def sync( Patch counterparty fields. Only supplied fields are updated. Set is_active=false to deactivate (agents are never deleted — they are reference data referenced by events and transactions). + `classification_element_id` sets the account this counterparty's bank lines are suggested (an empty + string clears it), and `classification_mode='always_ask'` stops suggesting one for a counterparty + whose lines go to different accounts; its still-open lines are re-suggested at once. A default is + also learned as lines are committed, so set one by hand only to correct it. **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. @@ -196,6 +204,10 @@ async def asyncio_detailed( Patch counterparty fields. Only supplied fields are updated. Set is_active=false to deactivate (agents are never deleted — they are reference data referenced by events and transactions). + `classification_element_id` sets the account this counterparty's bank lines are suggested (an empty + string clears it), and `classification_mode='always_ask'` stops suggesting one for a counterparty + whose lines go to different accounts; its still-open lines are re-suggested at once. A default is + also learned as lines are committed, so set one by hand only to correct it. **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. @@ -237,6 +249,10 @@ async def asyncio( Patch counterparty fields. Only supplied fields are updated. Set is_active=false to deactivate (agents are never deleted — they are reference data referenced by events and transactions). + `classification_element_id` sets the account this counterparty's bank lines are suggested (an empty + string clears it), and `classification_mode='always_ask'` stops suggesting one for a counterparty + whose lines go to different accounts; its still-open lines are re-suggested at once. A default is + also learned as lines are committed, so set one by hand only to correct it. **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. diff --git a/robosystems_client/api/robo_ledger_ledger_events/update_event_block.py b/robosystems_client/api/robo_ledger_ledger_events/update_event_block.py index 14e8440..82df4b1 100644 --- a/robosystems_client/api/robo_ledger_ledger_events/update_event_block.py +++ b/robosystems_client/api/robo_ledger_ledger_events/update_event_block.py @@ -121,7 +121,12 @@ def sync_detailed( captured metadata to produce the GL rows, unless it already wrote them when the event was created; a bank-feed line with no account chosen and no matching rule is refused. Errors from the handler (validation, element resolution, closed period, unbalanced lines) surface as 422 here so the inbox - UI can display the failure reason without retry. + UI can display the failure reason without retry. A bank line's flow is a second classification + beside its account, overriding the account's default flow on the cash flow and equity statements: + patch `classified_flow_qname` (or `flow_qname` on each `classified_allocations` part) with an rs- + gaap flow concept such as `rs-gaap:ProceedsFromIssuanceOfLongTermDebt`. On a line that has already + posted, this re-tags its lines in place, in a closed month too, since no balance moves; an empty + string clears it. **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. @@ -172,7 +177,12 @@ def sync( captured metadata to produce the GL rows, unless it already wrote them when the event was created; a bank-feed line with no account chosen and no matching rule is refused. Errors from the handler (validation, element resolution, closed period, unbalanced lines) surface as 422 here so the inbox - UI can display the failure reason without retry. + UI can display the failure reason without retry. A bank line's flow is a second classification + beside its account, overriding the account's default flow on the cash flow and equity statements: + patch `classified_flow_qname` (or `flow_qname` on each `classified_allocations` part) with an rs- + gaap flow concept such as `rs-gaap:ProceedsFromIssuanceOfLongTermDebt`. On a line that has already + posted, this re-tags its lines in place, in a closed month too, since no balance moves; an empty + string clears it. **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. @@ -218,7 +228,12 @@ async def asyncio_detailed( captured metadata to produce the GL rows, unless it already wrote them when the event was created; a bank-feed line with no account chosen and no matching rule is refused. Errors from the handler (validation, element resolution, closed period, unbalanced lines) surface as 422 here so the inbox - UI can display the failure reason without retry. + UI can display the failure reason without retry. A bank line's flow is a second classification + beside its account, overriding the account's default flow on the cash flow and equity statements: + patch `classified_flow_qname` (or `flow_qname` on each `classified_allocations` part) with an rs- + gaap flow concept such as `rs-gaap:ProceedsFromIssuanceOfLongTermDebt`. On a line that has already + posted, this re-tags its lines in place, in a closed month too, since no balance moves; an empty + string clears it. **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. @@ -267,7 +282,12 @@ async def asyncio( captured metadata to produce the GL rows, unless it already wrote them when the event was created; a bank-feed line with no account chosen and no matching rule is refused. Errors from the handler (validation, element resolution, closed period, unbalanced lines) surface as 422 here so the inbox - UI can display the failure reason without retry. + UI can display the failure reason without retry. A bank line's flow is a second classification + beside its account, overriding the account's default flow on the cash flow and equity statements: + patch `classified_flow_qname` (or `flow_qname` on each `classified_allocations` part) with an rs- + gaap flow concept such as `rs-gaap:ProceedsFromIssuanceOfLongTermDebt`. On a line that has already + posted, this re-tags its lines in place, in a closed month too, since no balance moves; an empty + string clears it. **Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict. diff --git a/robosystems_client/clients/ledger_client.py b/robosystems_client/clients/ledger_client.py index 184a936..dabd5d7 100644 --- a/robosystems_client/clients/ledger_client.py +++ b/robosystems_client/clients/ledger_client.py @@ -87,6 +87,9 @@ from ..api.robo_ledger_ledger_events.preview_event_block import ( sync_detailed as op_preview_event_block, ) +from ..api.robo_ledger_ledger_events.learn_classification_defaults import ( + sync_detailed as op_learn_classification_defaults, +) from ..api.robo_ledger_ledger_events.update_agent import ( sync_detailed as op_update_agent, ) @@ -544,6 +547,12 @@ ResolveReconcilingItemRequestStatus, ) from ..models.resolve_reconciling_item_response import ResolveReconcilingItemResponse +from ..models.learn_classification_defaults_request import ( + LearnClassificationDefaultsRequest, +) +from ..models.learn_classification_defaults_response import ( + LearnClassificationDefaultsResponse, +) from ..models.set_reconciliation_policy_request import SetReconciliationPolicyRequest from ..models.set_reconciliation_policy_request_statement_cycle_type_0 import ( SetReconciliationPolicyRequestStatementCycleType0, @@ -2131,7 +2140,10 @@ def update_agent( body: dict[str, Any], ) -> LedgerAgentResponse: """Update an agent. ``metadata_patch`` is a partial merge into existing - metadata; all other fields replace. + metadata; all other fields replace. ``classification_element_id`` sets + the account its bank lines are suggested (an empty string clears it) and + ``classification_mode="always_ask"`` stops suggesting one; its open lines + are re-suggested at once. """ request = UpdateAgentRequest.from_dict(body) response = op_update_agent( @@ -2140,6 +2152,23 @@ def update_agent( envelope = self._call_op("Update agent", response) return self._typed_result("Update agent", envelope, LedgerAgentResponse) + def learn_classification_defaults( + self, graph_id: str, *, dry_run: bool = False + ) -> LearnClassificationDefaultsResponse: + """Give each counterparty with committed bank lines and no default the + account most of its lines went to, and re-suggest its open lines. Run + once on books classified before defaults were learned; ``dry_run`` + reports without writing.""" + response = op_learn_classification_defaults( + graph_id=graph_id, + body=LearnClassificationDefaultsRequest(dry_run=dry_run), + client=self._get_client(), + ) + envelope = self._call_op("Learn classification defaults", response) + return self._typed_result( + "Learn classification defaults", envelope, LearnClassificationDefaultsResponse + ) + # ── Event handlers (DSL handler registry) ──────────────────────────── def create_event_handler( diff --git a/robosystems_client/graphql/generated/__init__.py b/robosystems_client/graphql/generated/__init__.py index 86024ed..955b892 100644 --- a/robosystems_client/graphql/generated/__init__.py +++ b/robosystems_client/graphql/generated/__init__.py @@ -63,7 +63,11 @@ GetLedgerAccountTreeAccountTreeRootsChildrenChildren, GetLedgerAccountTreeAccountTreeRootsChildrenChildrenChildren, ) -from .get_ledger_agent import GetLedgerAgent, GetLedgerAgentAgent +from .get_ledger_agent import ( + GetLedgerAgent, + GetLedgerAgentAgent, + GetLedgerAgentAgentClassification, +) from .get_ledger_closing_book_structures import ( GetLedgerClosingBookStructures, GetLedgerClosingBookStructuresClosingBookStructures, @@ -233,7 +237,11 @@ ListLedgerAccountsAccountsAccounts, ListLedgerAccountsAccountsPagination, ) -from .list_ledger_agents import ListLedgerAgents, ListLedgerAgentsAgents +from .list_ledger_agents import ( + ListLedgerAgents, + ListLedgerAgentsAgents, + ListLedgerAgentsAgentsClassification, +) from .list_ledger_bank_accounts import ( ListLedgerBankAccounts, ListLedgerBankAccountsBankAccounts, @@ -481,6 +489,7 @@ "GetLedgerAccountTreeAccountTreeRootsChildrenChildrenChildren", "GetLedgerAgent", "GetLedgerAgentAgent", + "GetLedgerAgentAgentClassification", "GetLedgerClosingBookStructures", "GetLedgerClosingBookStructuresClosingBookStructures", "GetLedgerClosingBookStructuresClosingBookStructuresCategories", @@ -639,6 +648,7 @@ "ListLedgerAccountsAccountsPagination", "ListLedgerAgents", "ListLedgerAgentsAgents", + "ListLedgerAgentsAgentsClassification", "ListLedgerBankAccounts", "ListLedgerBankAccountsBankAccounts", "ListLedgerBankAccountsBankAccountsAccounts", diff --git a/robosystems_client/graphql/generated/get_ledger_agent.py b/robosystems_client/graphql/generated/get_ledger_agent.py index 9e2a29c..eb4743a 100644 --- a/robosystems_client/graphql/generated/get_ledger_agent.py +++ b/robosystems_client/graphql/generated/get_ledger_agent.py @@ -28,6 +28,19 @@ class GetLedgerAgentAgent(BaseModel): created_at: Optional[str] = Field(alias="createdAt") updated_at: Optional[str] = Field(alias="updatedAt") created_by: Optional[str] = Field(alias="createdBy") + classification: Optional["GetLedgerAgentAgentClassification"] + + +class GetLedgerAgentAgentClassification(BaseModel): + element_id: str = Field(alias="elementId") + account_name: Optional[str] = Field(alias="accountName") + mode: str + confirmations: int + overrides: int + set_by: Optional[str] = Field(alias="setBy") + set_at: Optional[str] = Field(alias="setAt") + learned_from: Optional[str] = Field(alias="learnedFrom") GetLedgerAgent.model_rebuild() +GetLedgerAgentAgent.model_rebuild() diff --git a/robosystems_client/graphql/generated/list_ledger_agents.py b/robosystems_client/graphql/generated/list_ledger_agents.py index f00c929..4abd83e 100644 --- a/robosystems_client/graphql/generated/list_ledger_agents.py +++ b/robosystems_client/graphql/generated/list_ledger_agents.py @@ -28,6 +28,19 @@ class ListLedgerAgentsAgents(BaseModel): created_at: Optional[str] = Field(alias="createdAt") updated_at: Optional[str] = Field(alias="updatedAt") created_by: Optional[str] = Field(alias="createdBy") + classification: Optional["ListLedgerAgentsAgentsClassification"] + + +class ListLedgerAgentsAgentsClassification(BaseModel): + element_id: str = Field(alias="elementId") + account_name: Optional[str] = Field(alias="accountName") + mode: str + confirmations: int + overrides: int + set_by: Optional[str] = Field(alias="setBy") + set_at: Optional[str] = Field(alias="setAt") + learned_from: Optional[str] = Field(alias="learnedFrom") ListLedgerAgents.model_rebuild() +ListLedgerAgentsAgents.model_rebuild() diff --git a/robosystems_client/graphql/generated/operations.py b/robosystems_client/graphql/generated/operations.py index 24a259d..280d6af 100644 --- a/robosystems_client/graphql/generated/operations.py +++ b/robosystems_client/graphql/generated/operations.py @@ -541,6 +541,16 @@ createdAt updatedAt createdBy + classification { + elementId + accountName + mode + confirmations + overrides + setBy + setAt + learnedFrom + } } } """ @@ -1404,6 +1414,16 @@ createdAt updatedAt createdBy + classification { + elementId + accountName + mode + confirmations + overrides + setBy + setAt + learnedFrom + } } } """ diff --git a/robosystems_client/graphql/operations/ledger/GetLedgerAgent.graphql b/robosystems_client/graphql/operations/ledger/GetLedgerAgent.graphql index 74d687a..b5a8517 100644 --- a/robosystems_client/graphql/operations/ledger/GetLedgerAgent.graphql +++ b/robosystems_client/graphql/operations/ledger/GetLedgerAgent.graphql @@ -3,5 +3,8 @@ query GetLedgerAgent($id: String!) { id agentType name legalName taxId registrationNumber duns lei email phone address source externalId isActive is1099Recipient createdAt updatedAt createdBy + classification { + elementId accountName mode confirmations overrides setBy setAt learnedFrom + } } } diff --git a/robosystems_client/graphql/operations/ledger/ListLedgerAgents.graphql b/robosystems_client/graphql/operations/ledger/ListLedgerAgents.graphql index ec2b855..1962cb6 100644 --- a/robosystems_client/graphql/operations/ledger/ListLedgerAgents.graphql +++ b/robosystems_client/graphql/operations/ledger/ListLedgerAgents.graphql @@ -15,5 +15,8 @@ query ListLedgerAgents( id agentType name legalName taxId registrationNumber duns lei email phone address source externalId isActive is1099Recipient createdAt updatedAt createdBy + classification { + elementId accountName mode confirmations overrides setBy setAt learnedFrom + } } } diff --git a/robosystems_client/graphql/schema.graphql b/robosystems_client/graphql/schema.graphql index 9f5e326..231acb9 100644 --- a/robosystems_client/graphql/schema.graphql +++ b/robosystems_client/graphql/schema.graphql @@ -1538,6 +1538,7 @@ type Agent { externalId: String isActive: Boolean! is1099Recipient: Boolean! + classification: AgentClassification createdAt: DateTime updatedAt: DateTime createdBy: String @@ -1545,6 +1546,36 @@ type Agent { openPayable: OpenBalanceByAgent } +""" +A counterparty's default account, learned from committed lines or set by +hand. +""" +type AgentClassification { + """The default chart account.""" + elementId: String! + + """The account's name.""" + accountName: String + + """`suggest`: offered on each new line. `always_ask`: never offered.""" + mode: String! + + """Committed lines that went to this account.""" + confirmations: Int! + + """Committed lines that went elsewhere while it was the default.""" + overrides: Int! + + """Who set or last moved it.""" + setBy: String + + """When it was set or last moved.""" + setAt: String + + """The committed line it was learned from, when it was.""" + learnedFrom: String +} + """ Per-agent open balance row. diff --git a/robosystems_client/models/__init__.py b/robosystems_client/models/__init__.py index 06bab51..5aa610b 100644 --- a/robosystems_client/models/__init__.py +++ b/robosystems_client/models/__init__.py @@ -3,6 +3,7 @@ from .account_info import AccountInfo from .add_graph_member_request import AddGraphMemberRequest from .add_publish_list_members_operation import AddPublishListMembersOperation +from .agent_classification import AgentClassification from .analytical_statement_fact_row import AnalyticalStatementFactRow from .api_key_info import APIKeyInfo from .api_keys_response import APIKeysResponse @@ -376,6 +377,8 @@ ) from .journal_entry_line_item_response import JournalEntryLineItemResponse from .journal_entry_response import JournalEntryResponse +from .learn_classification_defaults_request import LearnClassificationDefaultsRequest +from .learn_classification_defaults_response import LearnClassificationDefaultsResponse from .ledger_agent_response import LedgerAgentResponse from .ledger_agent_response_address_type_0 import LedgerAgentResponseAddressType0 from .ledger_entity_response import LedgerEntityResponse @@ -620,6 +623,12 @@ from .operation_envelope_journal_entry_response_status import ( OperationEnvelopeJournalEntryResponseStatus, ) +from .operation_envelope_learn_classification_defaults_response import ( + OperationEnvelopeLearnClassificationDefaultsResponse, +) +from .operation_envelope_learn_classification_defaults_response_status import ( + OperationEnvelopeLearnClassificationDefaultsResponseStatus, +) from .operation_envelope_ledger_agent_response import ( OperationEnvelopeLedgerAgentResponse, ) @@ -1063,6 +1072,9 @@ from .upcoming_invoice import UpcomingInvoice from .update_agent_request import UpdateAgentRequest from .update_agent_request_address_type_0 import UpdateAgentRequestAddressType0 +from .update_agent_request_classification_mode_type_0 import ( + UpdateAgentRequestClassificationModeType0, +) from .update_agent_request_metadata_patch import UpdateAgentRequestMetadataPatch from .update_api_key_request import UpdateAPIKeyRequest from .update_entity_request import UpdateEntityRequest @@ -1140,6 +1152,7 @@ "AccountInfo", "AddGraphMemberRequest", "AddPublishListMembersOperation", + "AgentClassification", "AnalyticalStatementFactRow", "APIKeyInfo", "APIKeysResponse", @@ -1437,6 +1450,8 @@ "JournalEntryLineItemInputMetadataType0", "JournalEntryLineItemResponse", "JournalEntryResponse", + "LearnClassificationDefaultsRequest", + "LearnClassificationDefaultsResponse", "LedgerAgentResponse", "LedgerAgentResponseAddressType0", "LedgerEntityResponse", @@ -1557,6 +1572,8 @@ "OperationEnvelopeInitializeLedgerResponseStatus", "OperationEnvelopeJournalEntryResponse", "OperationEnvelopeJournalEntryResponseStatus", + "OperationEnvelopeLearnClassificationDefaultsResponse", + "OperationEnvelopeLearnClassificationDefaultsResponseStatus", "OperationEnvelopeLedgerAgentResponse", "OperationEnvelopeLedgerAgentResponseStatus", "OperationEnvelopeLedgerEntityResponse", @@ -1850,6 +1867,7 @@ "UpcomingInvoice", "UpdateAgentRequest", "UpdateAgentRequestAddressType0", + "UpdateAgentRequestClassificationModeType0", "UpdateAgentRequestMetadataPatch", "UpdateAPIKeyRequest", "UpdateEntityRequest", diff --git a/robosystems_client/models/agent_classification.py b/robosystems_client/models/agent_classification.py new file mode 100644 index 0000000..cbd9eab --- /dev/null +++ b/robosystems_client/models/agent_classification.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="AgentClassification") + + +@_attrs_define +class AgentClassification: + """A counterparty's default account, learned from committed lines or set by + hand. + + Attributes: + element_id (str): The default chart account. + mode (str): `suggest`: offered on each new line. `always_ask`: never offered. + confirmations (int): Committed lines that went to this account. + overrides (int): Committed lines that went elsewhere while it was the default. + account_name (None | str | Unset): The account's name. + set_by (None | str | Unset): Who set or last moved it. + set_at (None | str | Unset): When it was set or last moved. + learned_from (None | str | Unset): The committed line it was learned from, when it was. + """ + + element_id: str + mode: str + confirmations: int + overrides: int + account_name: None | str | Unset = UNSET + set_by: None | str | Unset = UNSET + set_at: None | str | Unset = UNSET + learned_from: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + element_id = self.element_id + + mode = self.mode + + confirmations = self.confirmations + + overrides = self.overrides + + account_name: None | str | Unset + if isinstance(self.account_name, Unset): + account_name = UNSET + else: + account_name = self.account_name + + set_by: None | str | Unset + if isinstance(self.set_by, Unset): + set_by = UNSET + else: + set_by = self.set_by + + set_at: None | str | Unset + if isinstance(self.set_at, Unset): + set_at = UNSET + else: + set_at = self.set_at + + learned_from: None | str | Unset + if isinstance(self.learned_from, Unset): + learned_from = UNSET + else: + learned_from = self.learned_from + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "element_id": element_id, + "mode": mode, + "confirmations": confirmations, + "overrides": overrides, + } + ) + if account_name is not UNSET: + field_dict["account_name"] = account_name + if set_by is not UNSET: + field_dict["set_by"] = set_by + if set_at is not UNSET: + field_dict["set_at"] = set_at + if learned_from is not UNSET: + field_dict["learned_from"] = learned_from + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + element_id = d.pop("element_id") + + mode = d.pop("mode") + + confirmations = d.pop("confirmations") + + overrides = d.pop("overrides") + + def _parse_account_name(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + account_name = _parse_account_name(d.pop("account_name", UNSET)) + + def _parse_set_by(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + set_by = _parse_set_by(d.pop("set_by", UNSET)) + + def _parse_set_at(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + set_at = _parse_set_at(d.pop("set_at", UNSET)) + + def _parse_learned_from(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + learned_from = _parse_learned_from(d.pop("learned_from", UNSET)) + + agent_classification = cls( + element_id=element_id, + mode=mode, + confirmations=confirmations, + overrides=overrides, + account_name=account_name, + set_by=set_by, + set_at=set_at, + learned_from=learned_from, + ) + + agent_classification.additional_properties = d + return agent_classification + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/learn_classification_defaults_request.py b/robosystems_client/models/learn_classification_defaults_request.py new file mode 100644 index 0000000..7f0815f --- /dev/null +++ b/robosystems_client/models/learn_classification_defaults_request.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="LearnClassificationDefaultsRequest") + + +@_attrs_define +class LearnClassificationDefaultsRequest: + """Seed counterparty defaults from the bank lines already committed. + + Attributes: + dry_run (bool | Unset): Report what would be learned without writing it. Default: False. + """ + + dry_run: bool | Unset = False + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + dry_run = self.dry_run + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if dry_run is not UNSET: + field_dict["dry_run"] = dry_run + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + dry_run = d.pop("dry_run", UNSET) + + learn_classification_defaults_request = cls( + dry_run=dry_run, + ) + + learn_classification_defaults_request.additional_properties = d + return learn_classification_defaults_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/learn_classification_defaults_response.py b/robosystems_client/models/learn_classification_defaults_response.py new file mode 100644 index 0000000..182d0cd --- /dev/null +++ b/robosystems_client/models/learn_classification_defaults_response.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="LearnClassificationDefaultsResponse") + + +@_attrs_define +class LearnClassificationDefaultsResponse: + """What seeding the defaults learned. + + Attributes: + agents_learned (int): Counterparties given a default from their committed lines. + agents_kept (int): Counterparties that already had a default, left as they were. + lines_read (int): Committed lines classified to a single account that were read. + open_lines_resuggested (int): Still-open lines whose suggestion changed as a result. + dry_run (bool): + """ + + agents_learned: int + agents_kept: int + lines_read: int + open_lines_resuggested: int + dry_run: bool + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + agents_learned = self.agents_learned + + agents_kept = self.agents_kept + + lines_read = self.lines_read + + open_lines_resuggested = self.open_lines_resuggested + + dry_run = self.dry_run + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "agents_learned": agents_learned, + "agents_kept": agents_kept, + "lines_read": lines_read, + "open_lines_resuggested": open_lines_resuggested, + "dry_run": dry_run, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + agents_learned = d.pop("agents_learned") + + agents_kept = d.pop("agents_kept") + + lines_read = d.pop("lines_read") + + open_lines_resuggested = d.pop("open_lines_resuggested") + + dry_run = d.pop("dry_run") + + learn_classification_defaults_response = cls( + agents_learned=agents_learned, + agents_kept=agents_kept, + lines_read=lines_read, + open_lines_resuggested=open_lines_resuggested, + dry_run=dry_run, + ) + + learn_classification_defaults_response.additional_properties = d + return learn_classification_defaults_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/ledger_agent_response.py b/robosystems_client/models/ledger_agent_response.py index ae669af..fec4466 100644 --- a/robosystems_client/models/ledger_agent_response.py +++ b/robosystems_client/models/ledger_agent_response.py @@ -10,6 +10,7 @@ from ..types import UNSET, Unset if TYPE_CHECKING: + from ..models.agent_classification import AgentClassification from ..models.ledger_agent_response_address_type_0 import ( LedgerAgentResponseAddressType0, ) @@ -37,6 +38,7 @@ class LedgerAgentResponse: phone (None | str | Unset): address (LedgerAgentResponseAddressType0 | None | Unset): external_id (None | str | Unset): + classification (AgentClassification | None | Unset): The default account its bank lines are suggested. created_at (datetime.datetime | None | Unset): updated_at (datetime.datetime | None | Unset): created_by (None | str | Unset): @@ -57,12 +59,14 @@ class LedgerAgentResponse: phone: None | str | Unset = UNSET address: LedgerAgentResponseAddressType0 | None | Unset = UNSET external_id: None | str | Unset = UNSET + classification: AgentClassification | None | Unset = UNSET created_at: datetime.datetime | None | Unset = UNSET updated_at: datetime.datetime | None | Unset = UNSET created_by: None | str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: + from ..models.agent_classification import AgentClassification from ..models.ledger_agent_response_address_type_0 import ( LedgerAgentResponseAddressType0, ) @@ -135,6 +139,14 @@ def to_dict(self) -> dict[str, Any]: else: external_id = self.external_id + classification: dict[str, Any] | None | Unset + if isinstance(self.classification, Unset): + classification = UNSET + elif isinstance(self.classification, AgentClassification): + classification = self.classification.to_dict() + else: + classification = self.classification + created_at: None | str | Unset if isinstance(self.created_at, Unset): created_at = UNSET @@ -187,6 +199,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["address"] = address if external_id is not UNSET: field_dict["external_id"] = external_id + if classification is not UNSET: + field_dict["classification"] = classification if created_at is not UNSET: field_dict["created_at"] = created_at if updated_at is not UNSET: @@ -198,6 +212,7 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.agent_classification import AgentClassification from ..models.ledger_agent_response_address_type_0 import ( LedgerAgentResponseAddressType0, ) @@ -306,6 +321,23 @@ def _parse_external_id(data: object) -> None | str | Unset: external_id = _parse_external_id(d.pop("external_id", UNSET)) + def _parse_classification(data: object) -> AgentClassification | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + classification_type_0 = AgentClassification.from_dict(data) + + return classification_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(AgentClassification | None | Unset, data) + + classification = _parse_classification(d.pop("classification", UNSET)) + def _parse_created_at(data: object) -> datetime.datetime | None | Unset: if data is None: return data @@ -365,6 +397,7 @@ def _parse_created_by(data: object) -> None | str | Unset: phone=phone, address=address, external_id=external_id, + classification=classification, created_at=created_at, updated_at=updated_at, created_by=created_by, diff --git a/robosystems_client/models/operation_envelope_learn_classification_defaults_response.py b/robosystems_client/models/operation_envelope_learn_classification_defaults_response.py new file mode 100644 index 0000000..2cdc7a6 --- /dev/null +++ b/robosystems_client/models/operation_envelope_learn_classification_defaults_response.py @@ -0,0 +1,166 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.operation_envelope_learn_classification_defaults_response_status import ( + OperationEnvelopeLearnClassificationDefaultsResponseStatus, +) +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.learn_classification_defaults_response import ( + LearnClassificationDefaultsResponse, + ) + + +T = TypeVar("T", bound="OperationEnvelopeLearnClassificationDefaultsResponse") + + +@_attrs_define +class OperationEnvelopeLearnClassificationDefaultsResponse: + """ + Attributes: + operation (str): Kebab-case operation name + operation_id (str): op_-prefixed ULID for audit and SSE correlation + status (OperationEnvelopeLearnClassificationDefaultsResponseStatus): Operation lifecycle state + at (str): ISO-8601 UTC timestamp + result (LearnClassificationDefaultsResponse | None | Unset): Command-specific result payload + created_by (None | str | Unset): User ID that initiated the operation + idempotent_replay (bool | Unset): True when this envelope came from the idempotency cache — the underlying + command did not execute again. False on fresh executions. Default: False. + """ + + operation: str + operation_id: str + status: OperationEnvelopeLearnClassificationDefaultsResponseStatus + at: str + result: LearnClassificationDefaultsResponse | None | Unset = UNSET + created_by: None | str | Unset = UNSET + idempotent_replay: bool | Unset = False + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + from ..models.learn_classification_defaults_response import ( + LearnClassificationDefaultsResponse, + ) + + operation = self.operation + + operation_id = self.operation_id + + status = self.status.value + + at = self.at + + result: dict[str, Any] | None | Unset + if isinstance(self.result, Unset): + result = UNSET + elif isinstance(self.result, LearnClassificationDefaultsResponse): + result = self.result.to_dict() + else: + result = self.result + + created_by: None | str | Unset + if isinstance(self.created_by, Unset): + created_by = UNSET + else: + created_by = self.created_by + + idempotent_replay = self.idempotent_replay + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "operation": operation, + "operationId": operation_id, + "status": status, + "at": at, + } + ) + if result is not UNSET: + field_dict["result"] = result + if created_by is not UNSET: + field_dict["createdBy"] = created_by + if idempotent_replay is not UNSET: + field_dict["idempotentReplay"] = idempotent_replay + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.learn_classification_defaults_response import ( + LearnClassificationDefaultsResponse, + ) + + d = dict(src_dict) + operation = d.pop("operation") + + operation_id = d.pop("operationId") + + status = OperationEnvelopeLearnClassificationDefaultsResponseStatus(d.pop("status")) + + at = d.pop("at") + + def _parse_result( + data: object, + ) -> LearnClassificationDefaultsResponse | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + result_type_0 = LearnClassificationDefaultsResponse.from_dict(data) + + return result_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(LearnClassificationDefaultsResponse | None | Unset, data) + + result = _parse_result(d.pop("result", UNSET)) + + def _parse_created_by(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + created_by = _parse_created_by(d.pop("createdBy", UNSET)) + + idempotent_replay = d.pop("idempotentReplay", UNSET) + + operation_envelope_learn_classification_defaults_response = cls( + operation=operation, + operation_id=operation_id, + status=status, + at=at, + result=result, + created_by=created_by, + idempotent_replay=idempotent_replay, + ) + + operation_envelope_learn_classification_defaults_response.additional_properties = d + return operation_envelope_learn_classification_defaults_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/robosystems_client/models/operation_envelope_learn_classification_defaults_response_status.py b/robosystems_client/models/operation_envelope_learn_classification_defaults_response_status.py new file mode 100644 index 0000000..d95adc1 --- /dev/null +++ b/robosystems_client/models/operation_envelope_learn_classification_defaults_response_status.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class OperationEnvelopeLearnClassificationDefaultsResponseStatus(str, Enum): + COMPLETED = "completed" + FAILED = "failed" + PENDING = "pending" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/update_agent_request.py b/robosystems_client/models/update_agent_request.py index 6a08591..6fd2acb 100644 --- a/robosystems_client/models/update_agent_request.py +++ b/robosystems_client/models/update_agent_request.py @@ -6,6 +6,9 @@ from attrs import define as _attrs_define from attrs import field as _attrs_field +from ..models.update_agent_request_classification_mode_type_0 import ( + UpdateAgentRequestClassificationModeType0, +) from ..types import UNSET, Unset if TYPE_CHECKING: @@ -39,8 +42,13 @@ class UpdateAgentRequest: address (None | Unset | UpdateAgentRequestAddressType0): is_active (bool | None | Unset): Toggle activation. Inactive agents are hidden from new-transaction pickers. is_1099_recipient (bool | None | Unset): + classification_element_id (None | str | Unset): The chart account this counterparty's bank lines are usually + classified to, suggested on each new line. An empty string clears it. Its still-open lines are re-suggested at + once. Omit to keep. + classification_mode (None | Unset | UpdateAgentRequestClassificationModeType0): `suggest` offers the default on + each line; `always_ask` offers none, for a counterparty whose lines go to different accounts. Omit to keep. metadata_patch (UpdateAgentRequestMetadataPatch | Unset): Deep-merged into agent.metadata. Pass `{}` to leave - unchanged. + unchanged. The default classification is set with the fields above, not here. """ agent_id: str @@ -55,6 +63,8 @@ class UpdateAgentRequest: address: None | Unset | UpdateAgentRequestAddressType0 = UNSET is_active: bool | None | Unset = UNSET is_1099_recipient: bool | None | Unset = UNSET + classification_element_id: None | str | Unset = UNSET + classification_mode: None | Unset | UpdateAgentRequestClassificationModeType0 = UNSET metadata_patch: UpdateAgentRequestMetadataPatch | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -133,6 +143,22 @@ def to_dict(self) -> dict[str, Any]: else: is_1099_recipient = self.is_1099_recipient + classification_element_id: None | str | Unset + if isinstance(self.classification_element_id, Unset): + classification_element_id = UNSET + else: + classification_element_id = self.classification_element_id + + classification_mode: None | str | Unset + if isinstance(self.classification_mode, Unset): + classification_mode = UNSET + elif isinstance( + self.classification_mode, UpdateAgentRequestClassificationModeType0 + ): + classification_mode = self.classification_mode.value + else: + classification_mode = self.classification_mode + metadata_patch: dict[str, Any] | Unset = UNSET if not isinstance(self.metadata_patch, Unset): metadata_patch = self.metadata_patch.to_dict() @@ -166,6 +192,10 @@ def to_dict(self) -> dict[str, Any]: field_dict["is_active"] = is_active if is_1099_recipient is not UNSET: field_dict["is_1099_recipient"] = is_1099_recipient + if classification_element_id is not UNSET: + field_dict["classification_element_id"] = classification_element_id + if classification_mode is not UNSET: + field_dict["classification_mode"] = classification_mode if metadata_patch is not UNSET: field_dict["metadata_patch"] = metadata_patch @@ -292,6 +322,38 @@ def _parse_is_1099_recipient(data: object) -> bool | None | Unset: is_1099_recipient = _parse_is_1099_recipient(d.pop("is_1099_recipient", UNSET)) + def _parse_classification_element_id(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + classification_element_id = _parse_classification_element_id( + d.pop("classification_element_id", UNSET) + ) + + def _parse_classification_mode( + data: object, + ) -> None | Unset | UpdateAgentRequestClassificationModeType0: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + classification_mode_type_0 = UpdateAgentRequestClassificationModeType0(data) + + return classification_mode_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Unset | UpdateAgentRequestClassificationModeType0, data) + + classification_mode = _parse_classification_mode( + d.pop("classification_mode", UNSET) + ) + _metadata_patch = d.pop("metadata_patch", UNSET) metadata_patch: UpdateAgentRequestMetadataPatch | Unset if isinstance(_metadata_patch, Unset): @@ -312,6 +374,8 @@ def _parse_is_1099_recipient(data: object) -> bool | None | Unset: address=address, is_active=is_active, is_1099_recipient=is_1099_recipient, + classification_element_id=classification_element_id, + classification_mode=classification_mode, metadata_patch=metadata_patch, ) diff --git a/robosystems_client/models/update_agent_request_classification_mode_type_0.py b/robosystems_client/models/update_agent_request_classification_mode_type_0.py new file mode 100644 index 0000000..924620c --- /dev/null +++ b/robosystems_client/models/update_agent_request_classification_mode_type_0.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class UpdateAgentRequestClassificationModeType0(str, Enum): + ALWAYS_ASK = "always_ask" + SUGGEST = "suggest" + + def __str__(self) -> str: + return str(self.value) diff --git a/robosystems_client/models/update_agent_request_metadata_patch.py b/robosystems_client/models/update_agent_request_metadata_patch.py index d99c962..29f1134 100644 --- a/robosystems_client/models/update_agent_request_metadata_patch.py +++ b/robosystems_client/models/update_agent_request_metadata_patch.py @@ -11,7 +11,10 @@ @_attrs_define class UpdateAgentRequestMetadataPatch: - """Deep-merged into agent.metadata. Pass `{}` to leave unchanged.""" + """Deep-merged into agent.metadata. Pass `{}` to leave unchanged. The default classification is set with the fields + above, not here. + + """ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) diff --git a/tests/test_ledger_client.py b/tests/test_ledger_client.py index 67df6ea..4cc10d9 100644 --- a/tests/test_ledger_client.py +++ b/tests/test_ledger_client.py @@ -1281,6 +1281,7 @@ def test_list_agents(self, mock_execute, mock_config, graph_id): "createdAt": "2026-03-15T12:00:00Z", "updatedAt": "2026-03-15T12:00:00Z", "createdBy": "user_1", + "classification": None, } ] } @@ -1314,12 +1315,27 @@ def test_get_agent(self, mock_execute, mock_config, graph_id): "createdAt": "2026-03-15T12:00:00Z", "updatedAt": "2026-03-15T12:00:00Z", "createdBy": "user_1", + "classification": { + "elementId": "elem_payroll", + "accountName": "Payroll", + "mode": "suggest", + "confirmations": 7, + "overrides": 1, + "setBy": "user_1", + "setAt": "2026-10-10T00:00:00Z", + "learnedFrom": "evt_1", + }, } } client = LedgerClient(mock_config) result = client.get_agent(graph_id, "agt_1") assert result is not None assert result.id == "agt_1" + assert result.classification is not None + assert ( + result.classification.account_name, + result.classification.confirmations, + ) == ("Payroll", 7) variables = mock_execute.call_args[0][2] assert variables["id"] == "agt_1" @@ -2169,6 +2185,41 @@ def test_update_agent_metadata_patch(self, mock_op, mock_config, graph_id): body = mock_op.call_args.kwargs["body"] assert body.metadata_patch.to_dict() == {"region": "us-west"} + @patch("robosystems_client.clients.ledger_client.op_update_agent") + def test_update_agent_sets_a_default_account(self, mock_op, mock_config, graph_id): + mock_op.return_value = _mock_response(_envelope("update-agent", {})) + client = LedgerClient(mock_config) + client.update_agent( + graph_id, + { + "agent_id": "agt_1", + "classification_element_id": "elem_payroll", + "classification_mode": "suggest", + }, + ) + sent = mock_op.call_args.kwargs["body"].to_dict() + assert sent["classification_element_id"] == "elem_payroll" + assert sent["classification_mode"] == "suggest" + + @patch("robosystems_client.clients.ledger_client.op_learn_classification_defaults") + def test_learn_classification_defaults(self, mock_op, mock_config, graph_id): + mock_op.return_value = _mock_response( + _envelope( + "learn-classification-defaults", + { + "agents_learned": 12, + "agents_kept": 0, + "lines_read": 61, + "open_lines_resuggested": 9, + "dry_run": True, + }, + ) + ) + client = LedgerClient(mock_config) + result = client.learn_classification_defaults(graph_id, dry_run=True) + assert result["agents_learned"] == 12 + assert mock_op.call_args.kwargs["body"].to_dict() == {"dry_run": True} + # ── Event handlers ────────────────────────────────────────────────────────