From 979970cb7f5a5df1661a8eee9405c79cd48439e3 Mon Sep 17 00:00:00 2001 From: Bilal Hussain Date: Mon, 4 Nov 2024 18:32:11 -0600 Subject: [PATCH 1/5] support for events --- method/method.py | 4 +- method/resources/Events/Event.py | 40 ++++++++++++++ method/resources/Simulate/Events.py | 16 ++++++ method/resources/Simulate/Simulate.py | 4 +- method/resources/Webhook.py | 48 ++++++++++++++--- method/resources/__init__.py | 3 +- test/resources/Event_test.py | 77 +++++++++++++++++++++++++++ 7 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 method/resources/Events/Event.py create mode 100644 method/resources/Simulate/Events.py create mode 100644 test/resources/Event_test.py diff --git a/method/method.py b/method/method.py index d0bbb0d..572bc66 100644 --- a/method/method.py +++ b/method/method.py @@ -9,12 +9,13 @@ from method.resources.Webhook import WebhookResource from method.resources.HealthCheck import PingResponse, HealthCheckResource from method.resources.Simulate import SimulateResource - +from method.resources.Events import EventResource class Method: accounts: AccountResource entities: EntityResource elements: ElementResource + events: EventResource merchants: MerchantResource payments: PaymentResource reports: ReportResource @@ -29,6 +30,7 @@ def __init__(self, opts: ConfigurationOpts = None, **kwargs: ConfigurationOpts): self.accounts = AccountResource(config) self.entities = EntityResource(config) self.elements = ElementResource(config) + self.events = EventResource(config) self.merchants = MerchantResource(config) self.payments = PaymentResource(config) self.reports = ReportResource(config) diff --git a/method/resources/Events/Event.py b/method/resources/Events/Event.py new file mode 100644 index 0000000..db94675 --- /dev/null +++ b/method/resources/Events/Event.py @@ -0,0 +1,40 @@ +from typing import TypedDict, Optional, Dict, Any, List, Literal +from method.resource import MethodResponse, Resource, ResourceListOpts +from method.configuration import Configuration +from method.resources.Webhook import WebhookTypesLiterals + +EventResourceTypesLiterals = Literal[ + 'account', + 'credit_score', + 'attribute', + 'connect' +] + +class EventDiff(TypedDict): + before: Optional[Dict[str, Any]] + after: Optional[Dict[str, Any]] + +class Event(TypedDict): + id: str + type: WebhookTypesLiterals + resource_id: str + resource_type: EventResourceTypesLiterals + data: Dict[str, Any] + diff: EventDiff + updated_at: str + created_at: str + +class EventListOpts(ResourceListOpts): + resource_id: Optional[str] + resource_type: Optional[EventResourceTypesLiterals] + type: Optional[WebhookTypesLiterals] + +class EventResource(Resource): + def __init__(self, config: Configuration): + super(EventResource, self).__init__(config.add_path('events')) + + def retrieve(self, evt_id: str) -> MethodResponse[Event]: + return super(EventResource, self)._get_with_id(evt_id) + + def list(self, params: Optional[EventListOpts] = None) -> MethodResponse[List[Event]]: + return super(EventResource, self)._list(params) \ No newline at end of file diff --git a/method/resources/Simulate/Events.py b/method/resources/Simulate/Events.py new file mode 100644 index 0000000..daa2bb1 --- /dev/null +++ b/method/resources/Simulate/Events.py @@ -0,0 +1,16 @@ +from typing import Dict, TypedDict, Optional +from method.resource import MethodResponse, Resource +from method.configuration import Configuration +from method.resources.Webhook import WebhookTypesLiterals + +class SimulateEventsOpts(TypedDict): + type: WebhookTypesLiterals + entity_id: Optional[str] + account_id: Optional[str] + +class SimulateEventsResource(Resource): + def __init__(self, config: Configuration): + super(SimulateEventsResource, self).__init__(config.add_path('events')) + + def create(self, opts: SimulateEventsOpts) -> MethodResponse[Dict]: + return super(SimulateEventsResource, self)._create(opts) \ No newline at end of file diff --git a/method/resources/Simulate/Simulate.py b/method/resources/Simulate/Simulate.py index db3b21f..e1803c9 100644 --- a/method/resources/Simulate/Simulate.py +++ b/method/resources/Simulate/Simulate.py @@ -2,14 +2,16 @@ from method.configuration import Configuration from method.resources.Simulate.Payments import SimulatePaymentResource from method.resources.Simulate.Accounts import SimulateAccountResource - +from method.resources.Simulate.Events import SimulateEventsResource class SimulateResource(Resource): payments: SimulatePaymentResource accounts: SimulateAccountResource + events: SimulateEventsResource def __init__(self, config: Configuration): _config = config.add_path('simulate') super(SimulateResource, self).__init__(_config) self.payments = SimulatePaymentResource(_config) self.accounts = SimulateAccountResource(_config) + self.events = SimulateEventsResource(_config) diff --git a/method/resources/Webhook.py b/method/resources/Webhook.py index 247bde6..d13ebb1 100644 --- a/method/resources/Webhook.py +++ b/method/resources/Webhook.py @@ -11,22 +11,58 @@ 'account.update', 'entity.update', 'entity.create', + 'account_verification.create', + 'account_verification.update', 'payment_reversal.create', 'payment_reversal.update', 'connection.create', 'connection.update', - 'account_verification.create', - 'account_verification.update', 'transaction.create', 'transaction.update', 'report.create', 'report.update', + 'product.create', + 'product.update', + 'subscription.create', + 'subscription.update', 'credit_score.create', 'credit_score.update', - - # Deprecated - 'account_verification.sent', - 'account_verification.returned' + 'payoff.create', + 'payoff.update', + 'entity_verification_session.create', + 'entity_verification_session.update', + 'connect.create', + 'connect.update', + 'balance.create', + 'balance.update', + 'identity.create', + 'identity.update', + 'account_verification_session.create', + 'account_verification_session.update', + 'card_brand.create', + 'card_brand.update', + 'sensitive.create', + 'sensitive.update', + 'update.create', + 'update.update', + 'attribute.create', + 'attribute.update', + 'account.opened', + 'account.closed', + 'credit_score.increased', + 'credit_score.decreased', + 'attribute.credit_health_credit_card_usage.increased', + 'attribute.credit_health_credit_card_usage.decreased', + 'attribute.credit_health_derogatory_marks.increased', + 'attribute.credit_health_derogatory_marks.decreased', + 'attribute.credit_health_hard_inquiries.increased', + 'attribute.credit_health_hard_inquiries.decreased', + 'attribute.credit_health_total_accounts.increased', + 'attribute.credit_health_total_accounts.decreased', + 'attribute.credit_health_credit_age.increased', + 'attribute.credit_health_credit_age.decreased', + 'attribute.credit_health_payment_history.increased', + 'attribute.credit_health_payment_history.decreased' ] diff --git a/method/resources/__init__.py b/method/resources/__init__.py index 3957c5c..6a3f311 100644 --- a/method/resources/__init__.py +++ b/method/resources/__init__.py @@ -10,4 +10,5 @@ from method.resources.HealthCheck import PingResponse, HealthCheckResource from method.resources.Merchant import Merchant, MerchantProviderIds, MerchantResource from method.resources.Report import Report, ReportCreateOpts, ReportResource -from method.resources.Webhook import Webhook, WebhookCreateOpts, WebhookResource \ No newline at end of file +from method.resources.Webhook import Webhook, WebhookCreateOpts, WebhookResource +from method.resources.Events.Event import Event, EventResource \ No newline at end of file diff --git a/test/resources/Event_test.py b/test/resources/Event_test.py new file mode 100644 index 0000000..195fef9 --- /dev/null +++ b/test/resources/Event_test.py @@ -0,0 +1,77 @@ +import os +import pytest +from method import Method +from dotenv import load_dotenv +from utils import await_results + +load_dotenv() + +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', + } + }) + + 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': {} + }) + + 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() + 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 + } + +def test_simulate_account_closed(setup): + method.simulate.events.create({ + 'type': 'account.closed', + 'account_id': setup['account_response'][0]['id'] + }) + + # Wait for event to be created + await_results(lambda: True, delay=5) + + events_list_response = method.events.list({ + 'resource_id': setup['account_response'][0]['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.closed', + 'resource_id': setup['account_response'][0]['id'], + 'resource_type': 'account', + 'data': event_response['data'], + 'diff': event_response['diff'] + } + + assert event_retrieve_response == expect_results \ No newline at end of file From a5ee21546382c3bf8d169b62aa833d1578e21056 Mon Sep 17 00:00:00 2001 From: Bilal Hussain Date: Tue, 5 Nov 2024 18:23:43 -0600 Subject: [PATCH 2/5] fixed tests + other bugs --- method/resources/Events/__init__.py | 1 + method/resources/Webhook.py | 2 ++ test/resources/Account_test.py | 4 ++-- test/resources/Entity_test.py | 2 +- test/resources/Webhook_test.py | 6 ++++-- 5 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 method/resources/Events/__init__.py diff --git a/method/resources/Events/__init__.py b/method/resources/Events/__init__.py new file mode 100644 index 0000000..e6e7365 --- /dev/null +++ b/method/resources/Events/__init__.py @@ -0,0 +1 @@ +from method.resources.Events.Event import Event, EventResource \ No newline at end of file diff --git a/method/resources/Webhook.py b/method/resources/Webhook.py index d13ebb1..f3a72ec 100644 --- a/method/resources/Webhook.py +++ b/method/resources/Webhook.py @@ -73,6 +73,7 @@ class Webhook(TypedDict): metadata: Optional[Dict[str, Any]] created_at: str updated_at: str + expand_event: bool class WebhookCreateOpts(TypedDict): @@ -80,6 +81,7 @@ class WebhookCreateOpts(TypedDict): url: str auth_token: Optional[str] metadata: Optional[Dict[str, Any]] + expand_event: Optional[bool] class WebhookResource(Resource): diff --git a/test/resources/Account_test.py b/test/resources/Account_test.py index b057b7d..be993b3 100644 --- a/test/resources/Account_test.py +++ b/test/resources/Account_test.py @@ -267,7 +267,7 @@ def test_create_card_brands(setup): 'account_id': test_credit_card_account['id'], 'network': 'visa', 'status': 'completed', - 'issuer': None, + 'issuer': card_brand_create_response['issuer'], 'last4': '1580', 'brands': card_brand_create_response['brands'], 'shared': False, @@ -288,7 +288,7 @@ def test_retrieve_card_brands(setup): 'account_id': test_credit_card_account['id'], 'network': 'visa', 'status': 'completed', - 'issuer': None, + 'issuer': card_brand_create_response['issuer'], 'last4': '1580', 'brands': card_brand_create_response['brands'], 'shared': False, diff --git a/test/resources/Entity_test.py b/test/resources/Entity_test.py index 56a245f..496cdb7 100644 --- a/test/resources/Entity_test.py +++ b/test/resources/Entity_test.py @@ -371,7 +371,7 @@ def get_credit_score(): { 'score': credit_score_retrieve_response['scores'][0]['score'], 'source': 'equifax', - 'model': 'vantage_3', + 'model': 'vantage_4', 'factors': credit_score_retrieve_response['scores'][0]['factors'], 'created_at': credit_score_retrieve_response['scores'][0]['created_at'] } diff --git a/test/resources/Webhook_test.py b/test/resources/Webhook_test.py index 889bf93..ed21739 100644 --- a/test/resources/Webhook_test.py +++ b/test/resources/Webhook_test.py @@ -28,7 +28,8 @@ def test_create_webhooks(): 'url': 'https://dev.methodfi.com', 'metadata': None, 'created_at': webhooks_create_response['created_at'], - 'updated_at': webhooks_create_response['updated_at'] + 'updated_at': webhooks_create_response['updated_at'], + 'expand_event': webhooks_create_response['expand_event'] } assert webhooks_create_response == expect_results @@ -45,7 +46,8 @@ def test_retrieve_webhook(): 'url': 'https://dev.methodfi.com', 'metadata': None, 'created_at': webhooks_retrieve_response['created_at'], - 'updated_at': webhooks_retrieve_response['updated_at'] + 'updated_at': webhooks_retrieve_response['updated_at'], + 'expand_event': webhooks_retrieve_response['expand_event'] } assert webhooks_retrieve_response == expect_results From 2f9b42aa561cf034645a733171a5700da6074f2b Mon Sep 17 00:00:00 2001 From: Bilal Hussain Date: Wed, 6 Nov 2024 12:21:15 -0600 Subject: [PATCH 3/5] fixed entity test --- test/resources/Entity_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/resources/Entity_test.py b/test/resources/Entity_test.py index 496cdb7..6f10e87 100644 --- a/test/resources/Entity_test.py +++ b/test/resources/Entity_test.py @@ -1,3 +1,4 @@ +from datetime import datetime, timedelta import os from method.resources.Entities.Attributes import EntityAttributes import pytest @@ -239,7 +240,8 @@ def test_update_entity(): def test_list_entities(): global entities_list_response - entities_list_response = method.entities.list() + # list only those entities created in past day, in the format of YYYY-MM-DD + entities_list_response = method.entities.list( {'from_date': (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d')} ) entities_list_response = [entity['id'] for entity in entities_list_response] assert entities_create_response['id'] in entities_list_response From bc04a298e1b8bea8f37a47434f5bcb4927636f5e Mon Sep 17 00:00:00 2001 From: Bilal Hussain Date: Wed, 6 Nov 2024 12:25:00 -0600 Subject: [PATCH 4/5] fixed event test --- test/resources/Event_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/resources/Event_test.py b/test/resources/Event_test.py index 195fef9..a3d4c14 100644 --- a/test/resources/Event_test.py +++ b/test/resources/Event_test.py @@ -1,8 +1,8 @@ +from time import sleep import os import pytest from method import Method from dotenv import load_dotenv -from utils import await_results load_dotenv() @@ -54,7 +54,7 @@ def test_simulate_account_closed(setup): }) # Wait for event to be created - await_results(lambda: True, delay=5) + sleep(1) events_list_response = method.events.list({ 'resource_id': setup['account_response'][0]['id'] From 4569b19710b132bce4862918c9f1a5b14b72ca55 Mon Sep 17 00:00:00 2001 From: Bilal Hussain Date: Wed, 6 Nov 2024 12:29:38 -0600 Subject: [PATCH 5/5] version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d617c8c..1b930f6 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='method-python', - version='1.1.1', + version='1.1.2', description='Python library for the Method API', long_description='Python library for the Method API', long_description_content_type='text/x-rst',