From f2362cc3ea280a1d70580ed8d7df3e6fe95506a0 Mon Sep 17 00:00:00 2001 From: hiohiohio Date: Wed, 12 Jun 2024 09:38:46 +0900 Subject: [PATCH] feat: add kyc_results into broker account (#467) --- alpaca/broker/models/accounts.py | 39 +++++++++++++---- .../broker_client/test_accounts_routes.py | 42 ++++++++++++++++++- 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/alpaca/broker/models/accounts.py b/alpaca/broker/models/accounts.py index c9f24074..a067afd2 100644 --- a/alpaca/broker/models/accounts.py +++ b/alpaca/broker/models/accounts.py @@ -1,10 +1,9 @@ from datetime import datetime -from typing import List, Optional +from typing import Any, Dict, List, Optional from uuid import UUID -from pydantic import TypeAdapter, model_validator, field_validator, ValidationInfo +from pydantic import TypeAdapter, ValidationInfo, field_validator, model_validator -from alpaca.broker.models.documents import AccountDocument from alpaca.broker.enums import ( AgreementType, ClearingBroker, @@ -13,14 +12,33 @@ TaxIdType, VisaType, ) +from alpaca.broker.models.documents import AccountDocument +from alpaca.common.models import ModelWithID +from alpaca.common.models import ValidateBaseModel as BaseModel from alpaca.trading.enums import AccountStatus -from alpaca.common.models import ( - ModelWithID, - ValidateBaseModel as BaseModel, -) from alpaca.trading.models import TradeAccount as BaseTradeAccount +class KycResults(BaseModel): + """ + Hold information about the result of KYC. + ref. https://docs.alpaca.markets/reference/getaccount + + Attributes: + reject (Optional[Dict[str, Any]]): The reason for the rejection + accept (Optional[Dict[str, Any]]): The reason for the acceptance + indeterminate (Optional[Dict[str, Any]]): The reason for the indeterminate result + additional_information (Optional[str]): Used to display a custom message + summary (Optional[str]): Either pass or fail. Used to indicate if KYC has completed and passed or not. + """ + + reject: Optional[Dict[str, Any]] = None + accept: Optional[Dict[str, Any]] = None + indeterminate: Optional[Dict[str, Any]] = None + additional_information: Optional[str] = None + summary: Optional[str] = None + + class Contact(BaseModel): """User contact details within Account Model @@ -218,6 +236,7 @@ class Account(ModelWithID): account_number (str): A more human friendly identifier for this account status (AccountStatus): The approval status of this account crypto_status (Optional[AccountStatus]): The crypto trading status. Only present if crypto trading is enabled. + kyc_results (Optional[KycResult]): Hold information about the result of KYC. currency (str): The currency the account's values are returned in last_equity (str): The total equity value stored in the account created_at (str): The timestamp when the account was created @@ -232,6 +251,7 @@ class Account(ModelWithID): account_number: str status: AccountStatus crypto_status: Optional[AccountStatus] = None + kyc_results: Optional[KycResults] = None currency: str last_equity: str created_at: str @@ -250,6 +270,11 @@ def __init__(self, **response): crypto_status=( response["crypto_status"] if "crypto_status" in response else None ), + kyc_results=( + TypeAdapter(KycResults).validate_python(response["kyc_results"]) + if "kyc_results" in response and response["kyc_results"] is not None + else None + ), currency=(response["currency"]), last_equity=(response["last_equity"]), created_at=(response["created_at"]), diff --git a/tests/broker/broker_client/test_accounts_routes.py b/tests/broker/broker_client/test_accounts_routes.py index 002a245f..32cfe670 100644 --- a/tests/broker/broker_client/test_accounts_routes.py +++ b/tests/broker/broker_client/test_accounts_routes.py @@ -125,6 +125,7 @@ def test_create_account(reqmock, client: BrokerClient): assert reqmock.called_once assert type(returned_account) == Account assert returned_account.id == UUID(created_id) + assert returned_account.kyc_results is None def test_create_lct_account(reqmock, client: BrokerClient): @@ -209,7 +210,8 @@ def test_create_lct_account(reqmock, client: BrokerClient): }, "account_type": "trading", "trading_configurations": null, - "currency": "EUR" + "currency": "EUR", + "kyc_results": null } """, ) @@ -232,6 +234,7 @@ def test_create_lct_account(reqmock, client: BrokerClient): assert type(returned_account) == Account assert returned_account.id == UUID(created_id) assert returned_account.currency == currency + assert returned_account.kyc_results is None def test_get_account(reqmock, client: BrokerClient): @@ -316,7 +319,14 @@ def test_get_account(reqmock, client: BrokerClient): "email_address": "agitated_golick_69906574@example.com" }, "account_type": "trading", - "trading_configurations": null + "trading_configurations": null, + "kyc_results": { + "reject": {"IDENTITY_VERIFICATION": {}}, + "accept": {"IDENTITY_VERIFICATION": {}}, + "indeterminate": {"IDENTITY_VERIFICATION": {}}, + "additional_information": "additional_information_test", + "summary": "pass" + } } """, ) @@ -327,6 +337,13 @@ def test_get_account(reqmock, client: BrokerClient): assert type(account) == Account assert account.id == UUID(account_id) + assert account.kyc_results is not None + assert account.kyc_results.reject == {"IDENTITY_VERIFICATION": {}} + assert account.kyc_results.accept == {"IDENTITY_VERIFICATION": {}} + assert account.kyc_results.indeterminate == {"IDENTITY_VERIFICATION": {}} + assert account.kyc_results.additional_information == "additional_information_test" + assert account.kyc_results.summary == "pass" + def test_get_account_account_not_found(reqmock, client: BrokerClient): account_id = "2a87c088-ffb6-472b-a4a3-cd9305c8605c" @@ -473,6 +490,13 @@ def test_update_account(reqmock, client: BrokerClient): assert account.id == UUID(account_id) assert account.identity.family_name == family_name + assert account.kyc_results is not None + assert account.kyc_results.reject == {} + assert account.kyc_results.accept == {} + assert account.kyc_results.indeterminate == {} + assert account.kyc_results.additional_information is None + assert account.kyc_results.summary == "pass" + def test_update_account_validates_account_id(reqmock, client: BrokerClient): # dummy update request just to test param parsing @@ -604,6 +628,13 @@ def test_list_accounts_no_params(reqmock, client: BrokerClient): assert account.trusted_contact is None assert account.agreements is None + assert account.kyc_results is not None + assert account.kyc_results.reject == {} + assert account.kyc_results.accept == {} + assert account.kyc_results.indeterminate == {} + assert account.kyc_results.additional_information is None + assert account.kyc_results.summary == "pass" + def test_list_accounts_parses_entities_if_present(reqmock, client: BrokerClient): reqmock.get( @@ -724,6 +755,13 @@ def test_list_accounts_parses_entities_if_present(reqmock, client: BrokerClient) assert account.trusted_contact is None assert account.agreements is None + assert account.kyc_results is not None + assert account.kyc_results.reject == {} + assert account.kyc_results.accept == {} + assert account.kyc_results.indeterminate == {} + assert account.kyc_results.additional_information is None + assert account.kyc_results.summary == "pass" + def test_get_trade_account_by_id(reqmock, client: BrokerClient): account_id = "5fc0795e-1f16-40cc-aa90-ede67c39d7a9"