diff --git a/method/errors.py b/method/errors.py index 9e10c68..d97b578 100644 --- a/method/errors.py +++ b/method/errors.py @@ -33,7 +33,7 @@ def __init__(self, opts: MethodErrorOpts): def catch(fn): def wrapper(*args, **kwargs): res = fn(*args, **kwargs) - if (res is not None) and ('error' in res): + if (res is not None) and ('error' in res) and ('id' not in res): raise MethodError.generate(res['error']) return res return wrapper diff --git a/method/method.py b/method/method.py index f23e17f..ae12d51 100644 --- a/method/method.py +++ b/method/method.py @@ -9,6 +9,7 @@ from method.resources.RoutingNumber import RoutingNumberResource from method.resources.Webhook import WebhookResource from method.resources.HealthCheck import PingResponse, HealthCheckResource +from method.resources.Connection import ConnectionResource class Method: @@ -22,6 +23,7 @@ class Method: routing_numbers: RoutingNumberResource webhooks: WebhookResource healthcheck: HealthCheckResource + connections: ConnectionResource def __init__(self, opts: ConfigurationOpts = None, **kwargs: ConfigurationOpts): _opts: ConfigurationOpts = {**(opts or {}), **kwargs} # type: ignore @@ -37,6 +39,7 @@ def __init__(self, opts: ConfigurationOpts = None, **kwargs: ConfigurationOpts): self.routing_numbers = RoutingNumberResource(config) self.webhooks = WebhookResource(config) self.healthcheck = HealthCheckResource(config) + self.connections = ConnectionResource(config) def ping(self) -> PingResponse: return self.healthcheck.get() diff --git a/method/resources/Account.py b/method/resources/Account.py index e57357c..8db0908 100644 --- a/method/resources/Account.py +++ b/method/resources/Account.py @@ -21,7 +21,8 @@ AccountCapabilitiesLiterals = Literal[ 'payments:receive', - 'payments:send' + 'payments:send', + 'data:retrieve' ] @@ -30,6 +31,13 @@ 'disabled' ] +AccountDetailTypesLiterals = Literal[ + 'bnpl_loan', + 'depository', + 'credit_card', + 'student_loan' +] + class AccountACH(TypedDict): routing: int @@ -74,6 +82,68 @@ class AccountCreateOpts(TypedDict): metadata: Optional[Dict[str, Any]] +class AccountDetailBNPLLoanUpcomingPaymentDue(TypedDict): + amount: int + date: str + + +class AccountDetailBNPLLoan(TypedDict): + name: Optional[str] + reference_id: str + balance: int + purchase_date: str + next_payment_due_date: Optional[str] + total_payments_count: int + payments_made_count: int + remaining_payments_count: int + autopay_enabled: bool + payoff_progress: int + interest_rate: int + description: Optional[str] + total_cost: int + total_paid: int + status: Literal['paid_off', 'refunded', 'in_progress'] + upcoming_payments_due: List[AccountDetailBNPLLoanUpcomingPaymentDue] + + +class AccountDetailDepository(TypedDict): + name: Optional[str] + reference_number: str + balance: int + + +class AccountDetailCreditCard(TypedDict): + name: Optional[str] + reference_number: str + balance: int + last_payment_amount: int + last_payment_date: Optional[str] + next_payment_due_date: Optional[str] + next_payment_minimum_amount: int + + +# TODO[mdelcarmen] +class AccountDetailStudentLoan(TypedDict): + pass + + +class AccountDetail(TypedDict): + type: AccountDetailTypesLiterals + bnpl_loan: Optional[AccountDetailBNPLLoan] + depository: Optional[AccountDetailDepository] + credit_card: Optional[AccountDetailCreditCard] + student_loan: Optional[AccountDetailStudentLoan] + + +class AccountTransaction(TypedDict): + id: str + reference_id: str + date: str + amount: int + status: Literal['pending', 'success'] + description: Optional[str] + + class AccountListOpts(TypedDict): holder_id: Optional[str] @@ -100,3 +170,9 @@ def list(self, params: Optional[AccountListOpts] = None) -> List[Account]: def create(self, opts: AccountCreateOpts, request_opts: Optional[RequestOpts] = None) -> Account: return super(AccountResource, self)._create(opts, request_opts) + + def get_detail(self, _id: str) -> AccountDetail: + return super(AccountResource, self)._get_with_sub_path('{_id}/detail'.format(_id=_id)) + + def get_transactions(self, _id: str) -> List[AccountTransaction]: + return super(AccountResource, self)._get_with_sub_path('{_id}/transactions'.format(_id=_id)) diff --git a/method/resources/Connection.py b/method/resources/Connection.py new file mode 100644 index 0000000..ef8fe0d --- /dev/null +++ b/method/resources/Connection.py @@ -0,0 +1,56 @@ +from typing import TypedDict, Optional, List, Dict, Any, Literal + +from method.resources import Account +from method.errors import ResourceError +from method.resource import Resource +from method.configuration import Configuration + + +ConnectionSourcesLiterals = Literal[ + 'plaid', + 'mch_5500' +] + + +ConnectionStatusesLiterals = Literal[ + 'success', + 'reauth_required', + 'syncing', + 'failed' +] + + +class Connection(TypedDict): + id: str + entity_id: str + accounts: List[str] + source: ConnectionSourcesLiterals + status: ConnectionStatusesLiterals + error: Optional[ResourceError] + created_at: str + updated_at: str + last_synced_at: str + + +class ConnectionUpdateOpts(TypedDict): + status: Literal['syncing'] + + +class ConnectionResource(Resource): + def __init__(self, config: Configuration): + super(ConnectionResource, self).__init__(config.add_path('connections')) + + def get(self, _id: str) -> Connection: + return super(ConnectionResource, self)._get_with_id(_id) + + def list(self) -> List[Connection]: + return super(ConnectionResource, self)._list(None) + + def update(self, _id: str, opts: ConnectionUpdateOpts) -> Connection: + return super(ConnectionResource, self)._update_with_id(_id, opts) + + def get_accounts(self, _id: str) -> List[Account]: + return super(ConnectionResource, self)._get_with_sub_path('{_id}/accounts'.format(_id=_id)) + + def get_public_account_tokens(self, _id: str) -> List[str]: + return super(ConnectionResource, self)._get_with_sub_path('{_id}/public_account_tokens'.format(_id=_id)) diff --git a/method/resources/Element.py b/method/resources/Element.py index d505c7b..15a817f 100644 --- a/method/resources/Element.py +++ b/method/resources/Element.py @@ -1,4 +1,4 @@ -from typing import TypedDict, Optional, Literal +from typing import TypedDict, Optional, Literal, List from method.resource import Resource from method.configuration import Configuration @@ -25,7 +25,8 @@ class Element(TypedDict): class ElementExchangePublicAccountOpts(TypedDict): - public_account_token: str + public_account_token: Optional[str] + public_account_tokens: Optional[List[str]] class ElementResource(Resource): @@ -37,3 +38,6 @@ def create_token(self, opts: ElementTokenCreateOpts) -> Element: def exchange_public_account_token(self, opts: ElementExchangePublicAccountOpts) -> Account: return super(ElementResource, self)._create_with_sub_path('/accounts/exchange', opts) + + def exchange_public_account_tokens(self, opts: ElementExchangePublicAccountOpts) -> Account: + return super(ElementResource, self)._create_with_sub_path('/accounts/exchange', opts) diff --git a/method/resources/Webhook.py b/method/resources/Webhook.py index 04e73d5..fdac5e2 100644 --- a/method/resources/Webhook.py +++ b/method/resources/Webhook.py @@ -11,6 +11,10 @@ 'account.update', 'entity.update', 'entity.create', + 'payment_reversal.create', + 'payment_reversal.update', + 'connection.create', + 'connection.update', 'account_verification.create', 'account_verification.update', 'account_verification.sent', diff --git a/setup.py b/setup.py index 1d55d26..d76e1e0 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='method-python', - version='0.0.15', + version='0.0.16', description='Python library for the Method API', author='Marco del Carmen', author_email='marco@mdelcarmen.me',