diff --git a/method/method.py b/method/method.py index 572bc66..42b16c8 100644 --- a/method/method.py +++ b/method/method.py @@ -10,6 +10,8 @@ from method.resources.HealthCheck import PingResponse, HealthCheckResource from method.resources.Simulate import SimulateResource from method.resources.Events import EventResource +from method.resources.CardProduct import CardProductResource +from method.resources.Opal import OpalResource class Method: accounts: AccountResource @@ -22,6 +24,8 @@ class Method: webhooks: WebhookResource healthcheck: HealthCheckResource simulate: SimulateResource + card_products: CardProductResource + opal: OpalResource def __init__(self, opts: ConfigurationOpts = None, **kwargs: ConfigurationOpts): _opts: ConfigurationOpts = {**(opts or {}), **kwargs} # type: ignore @@ -37,6 +41,8 @@ def __init__(self, opts: ConfigurationOpts = None, **kwargs: ConfigurationOpts): self.webhooks = WebhookResource(config) self.healthcheck = HealthCheckResource(config) self.simulate = SimulateResource(config) - + self.card_products = CardProductResource(config) + self.opal = OpalResource(config) + def ping(self) -> MethodResponse[PingResponse]: return self.healthcheck.retrieve() diff --git a/method/resource.py b/method/resource.py index a293244..37e50b4 100644 --- a/method/resource.py +++ b/method/resource.py @@ -126,6 +126,7 @@ def finalize_response(self, request_start_time: Optional[int] = None, request_en class RequestOpts(TypedDict): idempotency_key: Optional[str] + prefer: Optional[str] class ResourceListOpts(TypedDict): @@ -146,7 +147,7 @@ def __init__(self, config: Configuration): 'Authorization': 'Bearer {token}'.format(token=config.api_key), 'Content-Type': 'application/json', 'User-Agent': 'Method-Python/v{version}'.format(version=version('method-python')), - 'method-version': '2024-04-04' + 'method-version': '2025-07-04' }) def _make_request(self, method: str, path: Optional[str] = None, data: Optional[Dict] = None, params: Optional[Dict] = None, headers: Optional[Dict] = None, raw: bool = False, download: bool = False) -> Union[MethodResponse[T], str]: @@ -224,6 +225,8 @@ def _create(self, data: Dict, params: Optional[Dict] = None, request_opts: Optio headers = {} if request_opts and request_opts.get('idempotency_key'): headers['Idempotency-Key'] = request_opts.get('idempotency_key') + if request_opts and request_opts.get('prefer'): + headers['Prefer'] = request_opts.get('prefer') return self._make_request('POST', data=data, headers=headers, params=params) @MethodError.catch diff --git a/method/resources/Accounts/CardBrands.py b/method/resources/Accounts/CardBrands.py index 2701276..cef53f4 100644 --- a/method/resources/Accounts/CardBrands.py +++ b/method/resources/Accounts/CardBrands.py @@ -6,19 +6,21 @@ class AccountCardBrandInfo(TypedDict): - art_id: str - url: str - name: str + id: str + card_product_id: str + description: str + name: str + issuer: str + network: str + type: Literal['specific', 'generic', 'in_review'] + url: str class AccountCardBrand(TypedDict): id: str account_id: str - network: str - issuer: str - last4: str brands: List[AccountCardBrandInfo] - status: Literal['completed', 'failed'] + status: Literal['completed', 'in_progress', 'failed'] shared: bool source: Optional[Literal['method', 'network']] error: Optional[ResourceError] diff --git a/method/resources/Accounts/Products.py b/method/resources/Accounts/Products.py index f39d801..4beca6d 100644 --- a/method/resources/Accounts/Products.py +++ b/method/resources/Accounts/Products.py @@ -17,7 +17,7 @@ class AccountProduct(TypedDict): status: AccountProductTypeStatusLiterals status_error: Optional[ResourceError] latest_request_id: str - latest_successful_request_id: str + latest_successful_request_id: Optional[str] is_subscribable: bool created_at: str updated_at: str @@ -38,8 +38,5 @@ class AccountProductResource(Resource): def __init__(self, config: Configuration): super(AccountProductResource, self).__init__(config.add_path('products')) - def retrieve(self, prd_id: str) -> MethodResponse[AccountProduct]: - return super(AccountProductResource, self)._get_with_id(prd_id) - def list(self) -> MethodResponse[AccountProductListResponse]: return super(AccountProductResource, self)._list() diff --git a/method/resources/Accounts/Types.py b/method/resources/Accounts/Types.py index 07db79e..6e27c65 100644 --- a/method/resources/Accounts/Types.py +++ b/method/resources/Accounts/Types.py @@ -193,6 +193,21 @@ class AccountLiabilityStudentLoans(AccountLiabilityBase): original_loan_amount: Optional[int] term_length: Optional[int] +AccountLiabilitySubTypesLiterals = Literal[ + 'business', + 'unsecured', + 'lease', + 'loan', + 'heloc', + 'charge', + 'flexible_spending', + 'secured', + 'purchase', + 'note', + 'private', + 'federal', + 'rent' +] class AccountLiability(TypedDict): mch_id: str @@ -200,6 +215,7 @@ class AccountLiability(TypedDict): ownership: Optional[AccountOwnershipLiterals] fingerprint: Optional[str] type: Optional[AccountLiabilityTypesLiterals] + sub_type: Optional[AccountLiabilitySubTypesLiterals] name: Optional[str] diff --git a/method/resources/CardProduct.py b/method/resources/CardProduct.py new file mode 100644 index 0000000..8eece9a --- /dev/null +++ b/method/resources/CardProduct.py @@ -0,0 +1,39 @@ +from typing import TypedDict, Optional, List, Literal + +from method.resource import MethodResponse, Resource +from method.configuration import Configuration +from method.errors import ResourceError + + +CardProductTypeLiterals = Literal[ + 'specific', + 'generic', + 'in_review', +] + +class CardProductBrand(TypedDict): + id: str + description: str + network: str + default_image: str + + +class CardProduct(TypedDict): + id: str + name: str + issuer: str + type: CardProductTypeLiterals + brands: List[CardProductBrand] + error: Optional[ResourceError] + created_at: str + updated_at: str + + +class CardProductResource(Resource): + def __init__(self, config: Configuration): + super(CardProductResource, self).__init__(config.add_path('card_product')) + + + def retrieve(self, prt_id: str) -> MethodResponse[CardProduct]: + return super(CardProductResource, self)._get_with_id(prt_id) + diff --git a/method/resources/Entities/Connect.py b/method/resources/Entities/Connect.py index 6e2e4c3..555e03f 100644 --- a/method/resources/Entities/Connect.py +++ b/method/resources/Entities/Connect.py @@ -1,6 +1,6 @@ from typing import TypedDict, Optional, Literal, List -from method.resource import MethodResponse, Resource, ResourceListOpts +from method.resource import MethodResponse, RequestOpts, Resource, ResourceListOpts from method.configuration import Configuration from method.errors import ResourceError @@ -40,8 +40,8 @@ class EntityConnect(TypedDict): id: str status: EntityConnectResponseStatusLiterals accounts: Optional[List[str]] - requested_products: Optional[List[str]] - requested_subscriptions: Optional[List[str]] + requested_products: List[AccountProductsEligibleForAutomaticExecutionLiteral] + requested_subscriptions: List[AccountSubscriptionsEligibleForAutomaticExecutionLiteral] error: Optional[ResourceError] created_at: str updated_at: str @@ -76,7 +76,8 @@ def list( def create( self, opts: ConnectCreateOpts = {}, - params: Optional[ConnectExpandOpts] = None + params: Optional[ConnectExpandOpts] = None, + request_opts: Optional[RequestOpts] = None ) -> MethodResponse[EntityConnect]: - return super(EntityConnectResource, self)._create(data=opts, params=params) + return super(EntityConnectResource, self)._create(data=opts, params=params, request_opts=request_opts) \ No newline at end of file diff --git a/method/resources/Entities/Products.py b/method/resources/Entities/Products.py index 2a4447f..e51bb7c 100644 --- a/method/resources/Entities/Products.py +++ b/method/resources/Entities/Products.py @@ -17,7 +17,7 @@ class EntityProduct(TypedDict): status: EntityProductTypeStatusLiterals status_error: Optional[ResourceError] latest_request_id: str - latest_successful_request_id: str + latest_successful_request_id: Optional[str] is_subscribable: bool created_at: str updated_at: str @@ -35,8 +35,5 @@ class EntityProductResource(Resource): def __init__(self, config: Configuration): super(EntityProductResource, self).__init__(config.add_path('products')) - def retrieve(self, prd_id: str) -> MethodResponse[EntityProduct]: - return super(EntityProductResource, self)._get_with_id(prd_id) - def list(self) -> MethodResponse[EntityProductListResponse]: return super(EntityProductResource, self)._list() diff --git a/method/resources/Opal/Opal.py b/method/resources/Opal/Opal.py new file mode 100644 index 0000000..db256dc --- /dev/null +++ b/method/resources/Opal/Opal.py @@ -0,0 +1,12 @@ +from method.resource import Resource +from method.configuration import Configuration +from method.resources.Opal.Token import OpalTokenResource + + +class OpalResource(Resource): + token: OpalTokenResource + + def __init__(self, config: Configuration): + _config = config.add_path('opal') + super(OpalResource, self).__init__(_config) + self.token = OpalTokenResource(_config) diff --git a/method/resources/Opal/Token.py b/method/resources/Opal/Token.py new file mode 100644 index 0000000..ed93f21 --- /dev/null +++ b/method/resources/Opal/Token.py @@ -0,0 +1,104 @@ +from typing import TypedDict, Optional, Literal, List, Dict + +from method.resource import MethodResponse, Resource +from method.configuration import Configuration + +OpalModesLiterals = Literal[ + 'identity_verification', + 'connect', + 'card_connect', + 'account_verification', + 'transactions' +] + + +SkipPIILiterals = Literal[ + 'name', + 'dob', + 'address', + 'ssn_4' +] + + +AccountFiltersAccountTypesLiterals = Literal[ + 'credit_card', + 'auto_loan', + 'mortgage', + 'personal_loan', + 'student_loan' +] + + +SelectionTypeLiterals = Literal['single', 'multiple', 'all'] + + +class OpalAccountFiltersInclude(TypedDict): + account_types: List[AccountFiltersAccountTypesLiterals] + + +class OpalAccountFiltersExclude(TypedDict): + account_types: List[AccountFiltersAccountTypesLiterals] + mch_ids: List[str] + unverified_account_numbers: bool + + +class ConnectAccountFilters(TypedDict): + include: OpalAccountFiltersInclude + exclude: OpalAccountFiltersExclude + + +class CardConnectAccountFiltersExclude(TypedDict): + mch_ids: List[str] + unverified_account_numbers: bool + + +class CardConnectAccountFilters(TypedDict): + exclude: CardConnectAccountFiltersExclude + + +class OpalIdentityVerificationCreateOpts(TypedDict): + skip_pii: List[SkipPIILiterals] + + +class OpalConnectCreateOpts(TypedDict): + skip_pii: List[SkipPIILiterals] + selection_type: SelectionTypeLiterals + account_filters: ConnectAccountFilters + + +class OpalCardConnectCreateOpts(TypedDict): + skip_pii: List[SkipPIILiterals] + selection_type: SelectionTypeLiterals + account_filters: CardConnectAccountFilters + + +class OpalAccountVerificationCreateOpts(TypedDict): + account_id: str + + +class OpalTransactionsCreateOpts(TypedDict): + transactions: Dict[str, any] + + +class OpalTokenCreateOpts(TypedDict): + mode: OpalModesLiterals + entity_id: str + identity_verification: Optional[OpalIdentityVerificationCreateOpts] + connect: Optional[OpalConnectCreateOpts] + card_connect: Optional[OpalCardConnectCreateOpts] + account_verification: Optional[OpalAccountVerificationCreateOpts] + transactions: Optional[OpalTransactionsCreateOpts] + + +class OpalToken(TypedDict): + token: str + valid_until: str + session_id: str + + +class OpalTokenResource(Resource): + def __init__(self, config: Configuration): + super(OpalTokenResource, self).__init__(config.add_path('token')) + + def create(self, opts: OpalTokenCreateOpts) -> MethodResponse[OpalToken]: + return super(OpalTokenResource, self)._create(opts) diff --git a/method/resources/Opal/__init__.py b/method/resources/Opal/__init__.py new file mode 100644 index 0000000..ff85858 --- /dev/null +++ b/method/resources/Opal/__init__.py @@ -0,0 +1,2 @@ +from method.resources.Opal.Opal import OpalResource +from method.resources.Opal.Token import OpalTokenResource, OpalToken diff --git a/method/resources/Simulate/Attributes.py b/method/resources/Simulate/Attributes.py new file mode 100644 index 0000000..de27a6c --- /dev/null +++ b/method/resources/Simulate/Attributes.py @@ -0,0 +1,28 @@ +from method.resource import MethodResponse, Resource +from method.configuration import Configuration +from typing import Optional, Literal, List, TypedDict + + +AttributesBehaviorsLiterals = Literal[ + 'new_soft_inquiry' +] + + +class SimulateEntityAttributesOpts(TypedDict): + behaviors: List[AttributesBehaviorsLiterals] + + +class SimulateAttributesInstance(Resource): + def __init__(self, entity_id: str, config: Configuration): + super(SimulateAttributesInstance, self).__init__(config.add_path(entity_id)) + + def create(self, opts: SimulateEntityAttributesOpts) -> MethodResponse[Optional[None]]: + return super(SimulateAttributesInstance, self)._create(opts) + + +class SimulateAttributesResource(Resource): + def __init__(self, config: Configuration): + super(SimulateAttributesResource, self).__init__(config.add_path('attributes')) + + def __call__(self, entity_id: str) -> SimulateAttributesInstance: + return SimulateAttributesInstance(entity_id, self.config) diff --git a/method/resources/Simulate/Connect.py b/method/resources/Simulate/Connect.py new file mode 100644 index 0000000..e84fe05 --- /dev/null +++ b/method/resources/Simulate/Connect.py @@ -0,0 +1,39 @@ +from method.resource import MethodResponse, Resource +from method.configuration import Configuration +from typing import Optional, Literal, List, TypedDict + + +ConnectBehaviorsLiterals = Literal[ + 'new_credit_card_account', + 'new_auto_loan_account', + 'new_mortgage_account', + 'new_student_loan_account', + 'new_personal_loan_account' +] + + +class SimulateEntityConnectOpts(TypedDict): + behaviors: List[ConnectBehaviorsLiterals] + + +class SimulateConnectInstance(Resource): + def __init__(self, entity_id: str, config: Configuration): + super(SimulateConnectInstance, self).__init__(config.add_path(entity_id)) + + def create(self, opts: SimulateEntityConnectOpts) -> MethodResponse[Optional[None]]: + """ + For Entities that have been successfully verified, you may simulate Connect in the dev environment. + https://docs.methodfi.com/reference/simulations/connect/create + + Args: + opts: SimulateEntityConnectOpts + """ + return super(SimulateConnectInstance, self)._create(opts) + + +class SimulateConnectResource(Resource): + def __init__(self, config: Configuration): + super(SimulateConnectResource, self).__init__(config.add_path('connect')) + + def __call__(self, entity_id: str) -> SimulateConnectInstance: + return SimulateConnectInstance(entity_id, self.config) diff --git a/method/resources/Simulate/Entities.py b/method/resources/Simulate/Entities.py index 13a229c..976de72 100644 --- a/method/resources/Simulate/Entities.py +++ b/method/resources/Simulate/Entities.py @@ -1,13 +1,19 @@ from method.resource import Resource from method.configuration import Configuration from method.resources.Simulate.CreditScores import SimulateCreditScoresResource +from method.resources.Simulate.Connect import SimulateConnectResource +from method.resources.Simulate.Attributes import SimulateAttributesResource class SimulateEntitySubResources: credit_scores: SimulateCreditScoresResource + connect: SimulateConnectResource + attributes: SimulateAttributesResource def __init__(self, _id: str, config: Configuration): self.credit_scores = SimulateCreditScoresResource(config.add_path(_id)) + self.connect = SimulateConnectResource(config.add_path(_id)) + self.attributes = SimulateAttributesResource(config.add_path(_id)) class SimulateEntityResource(Resource): diff --git a/method/resources/Webhook.py b/method/resources/Webhook.py index 8896287..ec288a8 100644 --- a/method/resources/Webhook.py +++ b/method/resources/Webhook.py @@ -33,6 +33,7 @@ 'entity_verification_session.update', 'connect.create', 'connect.update', + 'connect.available', 'balance.create', 'balance.update', 'identity.create', @@ -41,6 +42,7 @@ 'account_verification_session.update', 'card_brand.create', 'card_brand.update', + 'card_brand.available', 'sensitive.create', 'sensitive.update', 'update.create', @@ -65,6 +67,8 @@ 'attribute.credit_health_payment_history.decreased', 'attribute.credit_health_open_accounts.increased', 'attribute.credit_health_open_accounts.decreased', + 'method_jwk.create', + 'method_jwk.update', ] diff --git a/setup.py b/setup.py index eee7121..aaf63e6 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='method-python', - version='1.2.6', + version='2.0.0', description='Python library for the Method API', long_description='Python library for the Method API', long_description_content_type='text/x-rst', diff --git a/test/resources/Account_test.py b/test/resources/Account_test.py index b2528aa..218e245 100644 --- a/test/resources/Account_test.py +++ b/test/resources/Account_test.py @@ -161,13 +161,15 @@ def test_create_liability_account(setup): 'mask': '8721', 'ownership': 'unknown', 'type': 'credit_card', - 'name': 'Chase Credit Card', + 'name': 'Chase Sapphire Reserve', + 'sub_type': 'flexible_spending', }, 'latest_verification_session': accounts_create_liability_response['latest_verification_session'], 'balance': None, 'update': accounts_create_liability_response['update'], 'attribute': accounts_create_liability_response['attribute'], 'card_brand': None, + 'payoff': None, 'payment_instrument': None, 'payoff': None, 'products': accounts_create_liability_response['products'], @@ -301,10 +303,7 @@ def test_create_card_brands(setup): expect_results: AccountCardBrand = { 'id': card_brand_create_response['id'], 'account_id': test_credit_card_account['id'], - 'network': 'visa', 'status': 'in_progress', - 'issuer': card_brand_create_response['issuer'], - 'last4': '1580', 'brands': card_brand_create_response['brands'], 'shared': False, 'source': card_brand_create_response['source'], @@ -325,10 +324,7 @@ def test_retrieve_card_brands(setup): expect_results = { 'id': card_brand_create_response['id'], 'account_id': test_credit_card_account['id'], - 'network': 'visa', 'status': 'completed', - 'issuer': card_brand_create_response['issuer'], - 'last4': '1580', 'shared': False, 'source': "network", 'error': None, @@ -343,7 +339,11 @@ def test_retrieve_card_brands(setup): assert brand['id'] == 'pdt_15_brd_1' assert brand['name'] == 'Chase Sapphire Reserve' assert brand['url'] == 'https://static.methodfi.com/card_brands/1b7ccaba6535cb837f802d968add4700.png' - assert isinstance(brand['art_id'], str) and brand['art_id'].startswith('art_') + assert brand['card_product_id'] == 'pdt_15' + assert brand['type'] == 'specific' + assert brand['network'] == 'visa' + assert brand['issuer'] == 'Chase' + assert brand['description'] == 'Chase Sapphire Reserve' @pytest.mark.asyncio async def test_list_card_brands(setup): @@ -354,10 +354,7 @@ async def test_list_card_brands(setup): assert result['id'] == card_brand_create_response['id'] assert result['account_id'] == test_credit_card_account['id'] - assert result['network'] == 'visa' assert result['status'] == 'completed' - assert result['issuer'] == card_brand_create_response['issuer'] - assert result['last4'] == '1580' assert result['shared'] is False assert result['source'] == 'network' assert result['error'] is None @@ -368,7 +365,11 @@ async def test_list_card_brands(setup): assert brand['id'] == 'pdt_15_brd_1' assert brand['name'] == 'Chase Sapphire Reserve' assert brand['url'] == 'https://static.methodfi.com/card_brands/1b7ccaba6535cb837f802d968add4700.png' - assert isinstance(brand['art_id'], str) and brand['art_id'].startswith('art_') + assert brand['card_product_id'] == 'pdt_15' + assert brand['type'] == 'specific' + assert brand['network'] == 'visa' + assert brand['issuer'] == 'Chase' + assert brand['description'] == 'Chase Sapphire Reserve' def test_create_payoffs(setup): global payoff_create_response @@ -505,11 +506,8 @@ def test_update_account_verification_sessions(setup): @pytest.mark.asyncio async def test_retrieve_account_verification_session(setup): test_credit_card_account = setup['test_credit_card_account'] - - def get_verification_session(): - return method.accounts(test_credit_card_account['id']).verification_sessions.retrieve(verification_session_update['id']) - verification_session_retrieve_response = await await_results(get_verification_session) + verification_session_retrieve_response = method.accounts(test_credit_card_account['id']).verification_sessions.retrieve(verification_session_update['id']) expect_results: AccountVerificationSession = { 'id': verification_session_update['id'], diff --git a/test/resources/Entity_test.py b/test/resources/Entity_test.py index c6210dc..9d09a75 100644 --- a/test/resources/Entity_test.py +++ b/test/resources/Entity_test.py @@ -21,11 +21,14 @@ method = Method(env='dev', api_key=API_KEY) entities_create_response = None +entities_create_async_response = None entitiy_with_identity_cap = None entities_retrieve_response = None entities_update_response = None +entities_update_async_response = None entities_list_response = None entities_connect_create_response = None +entities_connect_async_create_response = None entities_account_list_response = None entities_account_ids = None entities_create_credit_score_response = None @@ -44,11 +47,17 @@ def test_create_entity(): global entities_create_response + global entities_create_async_response entities_create_response = method.entities.create({ 'type': 'individual', 'individual': {}, 'metadata': {} }) + entities_create_async_response = method.entities.create({ + 'type': 'individual', + 'individual': {}, + 'metadata': {} + }) entities_create_response['restricted_subscriptions'] = entities_create_response['restricted_subscriptions'].sort() @@ -181,6 +190,7 @@ def test_retrieve_entity(): def test_update_entity(): global entities_update_response + global entities_update_async_response entities_update_response = method.entities.update(entities_create_response['id'], { 'individual': { 'first_name': 'John', @@ -189,6 +199,14 @@ def test_update_entity(): } }) + entities_update_async_response = method.entities.update(entities_create_async_response['id'], { + 'individual': { + 'first_name': 'John', + 'last_name': 'Doe', + 'phone': '+15121231111' + } + }) + entities_update_response['restricted_subscriptions'] = entities_update_response['restricted_subscriptions'].sort() entities_update_response['restricted_products'] = entities_update_response['restricted_products'].sort() @@ -273,6 +291,14 @@ def test_create_entity_phone_verification(): } }) + method.entities(entities_create_async_response['id']).verification_sessions.create({ + 'type': 'phone', + 'method': 'byo_sms', + 'byo_sms': { + 'timestamp': '2021-09-01T00:00:00.000Z', + } + }) + expect_results: EntityVerificationSession = { 'id': entities_create_phone_verification_response['id'], 'entity_id': entities_create_response['id'], @@ -298,6 +324,12 @@ def test_create_entity_individual_verification(): 'kba': {} }) + method.entities(entities_create_async_response['id']).verification_sessions.create({ + 'type': 'identity', + 'method': 'kba', + 'kba': {} + }) + expect_results: EntityVerificationSession = { 'id': entities_create_individual_verification_response['id'], 'entity_id': entities_create_response['id'], @@ -385,6 +417,52 @@ async def test_list_entity_connect(): assert connect_list_response[0] == expect_results +def test_create_entity_connect_async(): + global entities_connect_async_create_response + entities_connect_async_create_response = method.entities(entities_create_async_response['id']).connect.create({ + 'products': [ 'update' ], + 'subscriptions': [ 'update' ] + }, + {}, + { + 'prefer': 'respond-async' + }) + + expect_results: EntityConnect = { + 'id': entities_connect_async_create_response['id'], + 'entity_id': entities_create_async_response['id'], + 'status': 'pending', + 'accounts': None, + 'requested_products': [ 'update' ], + 'requested_subscriptions': [ 'update' ], + 'error': None, + 'created_at': entities_connect_async_create_response['created_at'], + 'updated_at': entities_connect_async_create_response['updated_at'], + } + + assert entities_connect_async_create_response == expect_results + +@pytest.mark.asyncio +async def test_retrieve_entity_connect_async(): + def get_connect(): + return method.entities(entities_create_async_response['id']).connect.retrieve(entities_connect_async_create_response['id']) + + connect_async_retrieve_response = await await_results(get_connect) + + expect_results: EntityConnect = { + 'id': entities_connect_async_create_response['id'], + 'entity_id': entities_create_async_response['id'], + 'status': 'completed', + 'accounts': connect_async_retrieve_response['accounts'], + 'requested_products': [ 'update' ], + 'requested_subscriptions': [ 'update' ], + 'error': None, + 'created_at': connect_async_retrieve_response['created_at'], + 'updated_at': connect_async_retrieve_response['updated_at'], + } + + assert connect_async_retrieve_response == expect_results + # ENTITY CREDIT SCORE TESTS def test_create_entity_credit_score(): diff --git a/test/resources/Event_test.py b/test/resources/Event_test.py index 0877ed5..aef2bfb 100644 --- a/test/resources/Event_test.py +++ b/test/resources/Event_test.py @@ -1,82 +1,84 @@ -# TODO: Add back tests once events list is working in dev -# from time import sleep -# import os -# import pytest -# from method import Method -# from dotenv import load_dotenv +from time import sleep +import os +import pytest +from method import Method +from dotenv import load_dotenv -# load_dotenv() +load_dotenv() -# API_KEY = os.getenv('API_KEY') -# method = Method(env='dev', api_key=API_KEY) +API_KEY = os.getenv('API_KEY') +method = Method(env='dev', api_key=API_KEY) -# @pytest.fixture(scope='module') -# def setup(): -# entity_response = method.entities.create({ -# 'type': 'individual', -# 'individual': { -# 'first_name': 'Kevin', -# 'last_name': 'Doyle', -# 'phone': '+15121231111', -# } -# }) +@pytest.fixture(scope='module') +def setup(): + entity_response = method.entities.create({ + 'type': 'individual', + 'individual': { + 'first_name': 'Kevin', + 'last_name': 'Doyle', + 'phone': '+15121231111', + } + }) -# method.entities(entity_response['id']).verification_sessions.create({ -# 'type': 'phone', -# 'method': 'byo_sms', -# 'byo_sms': { -# 'timestamp': '2024-03-15T00:00:00.000Z' -# } -# }) + method.entities(entity_response['id']).verification_sessions.create({ + 'type': 'phone', + 'method': 'byo_sms', + 'byo_sms': { + 'timestamp': '2024-03-15T00:00:00.000Z' + } + }) -# method.entities(entity_response['id']).verification_sessions.create({ -# 'type': 'identity', -# 'method': 'kba', -# 'kba': {} -# }) + method.entities(entity_response['id']).verification_sessions.create({ + 'type': 'identity', + 'method': 'kba', + 'kba': {} + }) -# connect_response = method.entities(entity_response['id']).connect.create() -# account_response = method.accounts.list({'holder_id': entity_response['id']}) -# attribute_response = method.entities(entity_response['id']).attributes.create({ -# 'attributes': ['credit_health_credit_card_usage'] -# }) -# credit_score_response = method.entities(entity_response['id']).credit_scores.create() + connect_response = method.entities(entity_response['id']).connect.create() + account_response = method.accounts.list({'holder_id': entity_response['id']}) + attribute_response = method.entities(entity_response['id']).attributes.create({ + 'attributes': ['credit_health_credit_card_usage'] + }) + credit_score_response = method.entities(entity_response['id']).credit_scores.create() -# return { -# 'entity_response': entity_response, -# 'connect_response': connect_response, -# 'account_response': account_response, -# 'attribute_response': attribute_response, -# 'credit_score_response': credit_score_response -# } + return { + 'entity_response': entity_response, + 'connect_response': connect_response, + 'account_response': account_response, + 'attribute_response': attribute_response, + 'credit_score_response': credit_score_response + } -# def test_simulate_account_opened(setup): -# method.simulate.events.create({ -# 'type': 'account.opened', -# 'entity_id': setup['entity_response']['id'] -# }) +def test_simulate_account_opened(setup): + response = method.simulate.events.create({ + 'type': 'account.opened', + 'entity_id': setup['entity_response']['id'] + }) + + assert response is not None, "Event simulation failed" -# max_retries = 3 -# for _ in range(max_retries): -# sleep(10) -# events_list_response = method.events.list({ -# 'type': 'account.opened' -# }) -# if events_list_response and len(events_list_response) > 0: -# break + # max_retries = 3 + # events_list_response = None + # for _ in range(max_retries): + # sleep(10) + # events_list_response = method.events.list({ + # 'type': 'account.opened' + # }) + # if events_list_response is not None and len(events_list_response) > 0: + # break -# event_response = events_list_response[0] -# event_retrieve_response = method.events.retrieve(event_response['id']) + # event_response = events_list_response[0] + # event_retrieve_response = method.events.retrieve(event_response['id']) -# expect_results = { -# 'id': event_response['id'], -# 'created_at': event_response['created_at'], -# 'updated_at': event_response['updated_at'], -# 'type': 'account.opened', -# 'resource_id': event_response['resource_id'], -# 'resource_type': event_response['resource_type'], -# 'data': event_response['data'], -# 'diff': event_response['diff'] -# } + # expect_results = { + # 'id': event_response['id'], + # 'created_at': event_response['created_at'], + # 'updated_at': event_response['updated_at'], + # 'type': 'account.opened', + # 'resource_id': event_response['resource_id'], + # 'resource_type': event_response['resource_type'], + # 'data': event_response['data'], + # 'diff': event_response['diff'] + # } -# assert event_retrieve_response == expect_results \ No newline at end of file + # assert event_retrieve_response == expect_results \ No newline at end of file diff --git a/test/resources/Opal_test.py b/test/resources/Opal_test.py new file mode 100644 index 0000000..e536ead --- /dev/null +++ b/test/resources/Opal_test.py @@ -0,0 +1,46 @@ +import os +import pytest +from method import Method +from dotenv import load_dotenv + +load_dotenv() + +API_KEY = os.getenv('API_KEY') + +method = Method(env='dev', api_key=API_KEY) + +opal_create_identity_verification_token_response = None + +@pytest.fixture(scope='module') +def setup(): + entity_1_response = method.entities.create({ + 'type': 'individual', + 'individual': { + 'first_name': 'Kevin', + 'last_name': 'Doyle', + 'dob': '1930-03-11', + 'email': 'kevin.doyle@gmail.com', + 'phone': '+15121231111', + }, + }) + + return { + 'entity_1_id': entity_1_response['id'], + } + + +def test_create_identity_verification_token(setup): + global opal_create_identity_verification_token_response + + opal_create_identity_verification_token_response = method.opal.token.create({ + 'entity_id': setup['entity_1_id'], + 'mode': 'identity_verification', + 'identity_verification': { + 'skip_pii': ['ssn_4' ] + } + }) + + assert 'token' in opal_create_identity_verification_token_response + assert 'valid_until' in opal_create_identity_verification_token_response + assert 'session_id' in opal_create_identity_verification_token_response + assert len(opal_create_identity_verification_token_response) == 3 diff --git a/test/resources/utils.py b/test/resources/utils.py index 9695d9b..eb6bbdd 100644 --- a/test/resources/utils.py +++ b/test/resources/utils.py @@ -5,7 +5,7 @@ async def sleep(ms: int): async def await_results(fn): result = None - retries = 10 + retries = 25 while retries > 0: try: result = fn() @@ -17,4 +17,7 @@ async def await_results(fn): raise error # Rethrow the error to fail the test retries -= 1 + if result['status'] not in ['completed', 'failed']: + raise Exception('Result status is not completed or failed') + return result