diff --git a/README.md b/README.md index 9901e04..6989686 100644 --- a/README.md +++ b/README.md @@ -12,409 +12,1001 @@ pip install method-python ```python from method import Method -method = Method(env='production', api_key='{API_KEY}') +method = Method(env="production", api_key="{API_KEY}") # or -method = Method({'env': 'production', 'api_key': '{API_KEY}'}) +method = Method({"env": "production", "api_key": "{API_KEY}"}) ``` ## Entities -### Create Individual Entity +Entities are a representation of your application’s end-users (individuals or corporation). Throughout Method’s ecosystem, an Entity is the legal owner of an account, and is the primary object for many of Method’s API endpoints. + +> Entities should be persisted with a 1:1 relationship throughout the lifecycle of your end-user. + +### PII Requirements + +Entity PII requirements are pre-defined during onboarding based on your team’s specific use case. Entities require at a minimum name + phone number for most services. Some Products and Subscriptions may require additional information to be provided to Method to enable certain features. Contact your Method CSM for more information. + +### Core Methods + +#### Create Individual Entity ```python entity = method.entities.create({ - 'type': 'individual', - 'individual': { - 'first_name': 'Kevin', - 'last_name': 'Doyle', - 'phone': '+16505555555', - 'email': 'kevin.doyle@gmail.com', - 'dob': '1997-03-18', + "type": "individual", + "individual": { + "first_name": "Kevin", + "last_name": "Doyle", + "phone": "+16505555555", + "email": "kevin.doyle@gmail.com", + "dob": "1997-03-18", }, - 'address': { - 'line1': '3300 N Interstate 35', - 'line2': None, - 'city': 'Austin', - 'state': 'TX', - 'zip': '78705' - } -}) + "address": { + "line1": "3300 N Interstate 35", + "line2": None, + "city": "Austin", + "state": "TX", + "zip": "78705", + }, +}); ``` -### Create Corporation Entity +#### Create Corporation Entity ```python entity = method.entities.create({ - 'type': 'c_corporation', - 'corporation': { - 'name': 'Alphabet Inc.', - 'dba': 'Google', - 'ein': '641234567', - 'owners': [ + "type": "corporation", + "corporation": { + "name": "Alphabet Inc.", + "dba": "Google", + "ein": "641234567", + "owners": [ { - 'first_name': 'Sergey', - 'last_name': 'Brin', - 'phone': '+16505555555', - 'email': 'sergey@google.com', - 'dob': '1973-08-21', - 'address': { - 'line1': '600 Amphitheatre Parkway', - 'line2': None, - 'city': 'Mountain View', - 'state': 'CA', - 'zip': '94043' - } - } - ] + "first_name": "Sergey", + "last_name": "Brin", + "phone": "+16505555555", + "email": "sergey@google.com", + "dob": "1973-08-21", + "address": { + "line1": "600 Amphitheatre Parkway", + "line2": None, + "city": "Mountain View", + "state": "CA", + "zip": "94043", + }, + }, + ], }, - 'address': { - 'line1': '1600 Amphitheatre Parkway', - 'line2': None, - 'city': 'Mountain View', - 'state': 'CA', - 'zip': '94043' - } -}) + "address": { + "line1": "1600 Amphitheatre Parkway", + "line2": None, + "city": "Mountain View", + "state": "CA", + "zip": "94043", + }, +}); ``` -### Retrieve Entity +#### Retrieve Entity ```python -entity = method.entities.get('ent_au22b1fbFJbp8') +entity = method.entities.retrieve("ent_au22b1fbFJbp8"); ``` -### Update Entity +#### Update Entity ```python -entity = method.entities.update('ent_au22b1fbFJbp8', { - 'individual': { - 'first_name': 'Kevin', - 'last_name': 'Doyle', - 'email': 'kevin.doyle@gmail.com', - 'dob': '1997-03-18', - } -}) +entity = method.entities.update("ent_au22b1fbFJbp8", { + "individual": { + "first_name": "Kevin", + "last_name": "Doyle", + "email": "kevin.doyle@gmail.com", + "dob": "1997-03-18", + }, +}); +``` + +#### List Entities + +```python +entities = method.entities.list(); +``` + +#### Withdraw an Entity"s consent + +```python +entity = method.entities.withdraw_consent("ent_au22b1fbFJbp8"); +``` + +### Connect + +The Connect endpoint identifies & connects all the liability accounts (e.g. Credit Card, Mortgage, Auto Loans, Student Loans, etc.) for an Entity across Method’s network of 1500+ FI / Lenders. + +#### Create a Connect + +```python +entity = method + .entities("ent_qKNBB68bfHGNA") + .connect + .create(); ``` -### List Entities +#### Retrieve a Connect ```python -entities = method.entities.list() +entity = method + .entities("ent_qKNBB68bfHGNA") + .connect + .retrieve("cxn_4ewMmBbjYDMR4"); ``` -### Refresh Capabilities +### Entity Verification Sessions + +The Entity Verification Sessions manages phone and identity verification sessions for Entities. Entities need to verify their identity and/or phone in order for them to be used throughout the Method API. +​ +####Verification Requirements + +Entity verification requirements differ on a team-by-team basis. A team’s unique verification process is pre-defined during onboarding based on your team’s specific use case. Contact your Method CSM for more information. + +The method key in entity.verification object will enumerate the phone & identity verifications available for your Entity. Refer to the Entity Object. + +>Any Entity Verification Session will expire 10 minutes after creation. If the verification is not completed within that time limit, another verification session will need to be created. + +#### Retrieve a Verification Session ```python -entity = method.entities.refresh_capabilities('ent_au22b1fbFJbp8') +response = method + .entities("ent_au22b1fbFJbp8") + .verification_sessions + .retrieve("evf_qTNNzCQ63zHJ9"); ``` -### Create Individual Auth Session +#### Create a BYO KYC Verification ```python -response = method.entities.create_auth_session('ent_au22b1fbFJbp8') +response = method + .entities("ent_XgYkTdiHyaz3e") + .verification_sessions + .create({ + "type": "identity", + "method": "byo_kyc", + "byo_kyc": {}, + }); ``` -### Update Individual Auth Session +#### Create a KBA Verification ```python -response = method.entities.update_auth_session('ent_au22b1fbFJbp8', { - "answers": [ - { - "question_id": "qtn_ywWqCnXDGGmmg", - "answer_id": "ans_74H68MJjqNhk8" +response = method + .entities("ent_hy3xhPDfWDVxi") + .verification_sessions + .create({ + "type": "identity", + "method": "kba", + "kba": {}, + }); +``` + +#### Update a KBA Verification + +```python +response = method + .entities("ent_hy3xhPDfWDVxi") + .verification_sessions + .update("evf_ywizPrR6WDxDG", { + "type": "identity", + "method": "kba", + "kba": { + "answers": [ + { + "question_id": "qtn_xgP6cGhq34fHW", + "answer_id": "ans_dbKCwDGwrrBgi" + }, + { + "question_id": "qtn_kmfdEftQ9zc6T", + "answer_id": "ans_LXN83xnJAUNFb" + }, + { + "question_id": "qtn_6mWegPLBpAFxb", + "answer_id": "ans_EKi47D8wA6YN3" + } + ] + } + }); +``` + +#### Create a BYO SMS Verification + +```python +response = method + .entities("ent_XgYkTdiHyaz3e") + .verification_sessions + .create({ + "type": "phone", + "method": "byo_sms", + "byo_sms": { + "timestamp": "2023-12-28T14:35:15.816Z", }, - ... - ] -}) + }); +``` + +#### Create a SMS Verification + +```python +response = method + .entities("ent_au22b1fbFJbp8") + .verification_sessions + .create({ + "type": "phone", + "method": "sms", + "sms": {}, + }); +``` + +#### Update a SMS Verification + +```python +response = method + .entities("ent_au22b1fbFJbp8") + .verification_sessions + .update("evf_3VT3bHTCnPbrm", { + "type": "phone", + "method": "sms", + "sms": { "sms_code": "884134" }, + }); +``` + +#### Create a SNA Verification + +```python +response = method + .entities("ent_au22b1fbFJbp8") + .verification_sessions + .create({ + "type": "phone", + "method": "sna", + "sna": {}, + }); +``` + +#### Update a SNA Verification + +```python +response = method + .entities("ent_BYdNCVApmp7Gx") + .verification_sessions + .update("evf_qTNNzCQ63zHJ9", { + "type": "phone", + "method": "sna", + "sna": {}, + }); +``` + +### Credit Scores + +The Credit Score endpoint returns the latest credit score and score factors for an Entity. + +#### Create Individual Credit Scores + +```python +entity = method + .entities("ent_au22b1fbFJbp8") + .credit_scores + .create(); +``` + +#### Retrieve Individual Credit Scores + +```python +entity = method + .entities("ent_au22b1fbFJbp8") + .credit_scores + .retrieve("crs_pn4ca33GXFaCE"); +``` + +### Identities + +The identities endpoint is used to retrieve the underlying identity (PII) of an Entity. The Identity endpoint requires an Entity to have at least a name and phone number. + +For an active entity or entities with a matched identity (verification.identity.matched) the identity endpoint will return a single identity with the PII. + +For all other entities the identity endpoint could return multiple identities as the provided PII was not enough to disambiguate and match a single identity. + +>The Identity endpoint is restricted to most teams. Contact your Method CSM for more information. + +#### Create Identities + +```python +entity = method + .entities("ent_au22b1fbFJbp8") + .identities + .create(); +``` + +#### Retrieve Identities + +```python +entity = method + .entities("ent_au22b1fbFJbp8") + .identities + .retrieve("idn_NhTRUVEknYaFM"); +``` + +### Entity Products + +The Entity Products endpoint outlines the Products (capabilities) an Entity has access to, and provides an overview of the status of all the Products. + +>Access to most products requires an Entity to be active. However, some products have restricted access requiring team-by-team enablement. + +#### List all Products + +```python +response = method + .entities("ent_TYHMaRJUUeJ7U") + .products + .list(); +``` + +#### Retrieve a Product + +```python +response = method + .entities("ent_TYHMaRJUUeJ7U") + .products + .retrieve("prd_jPRDcQPMk43Ek"); +``` + +### Entity Subscriptions + +The Entity Subscriptions endpoint controls the state of all Subscriptions for an Entity. + +Subscriptions are Products that can provide continuous updates via Webhooks. (e.g. Credit Score Subscription provides updates on an Entity’s credit score) + +>Most subscriptions are accessible by default. However, some subscriptions have restricted access requiring team-by-team enablement and elevated account verification. + +#### Create a Subscription + +```python +response = method + .entities("ent_TYHMaRJUUeJ7U") + .subscriptions + .create("credit_score"); +``` + +#### List all Subscriptions + +```python +response = method + .entities("ent_TYHMaRJUUeJ7U") + .subscriptions + .list(); +``` + +#### Retrieve a Subscription + +```python +response = method + .entities("ent_TYHMaRJUUeJ7U") + .subscriptions + .retrieve("sub_6f7XtMLymQx3f"); +``` + +#### Delete a Subscription + +```python +response = method + .entities("ent_TYHMaRJUUeJ7U") + .subscriptions + .delete("sub_6f7XtMLymQx3f"); ``` ## Accounts -### Create Ach Account +Accounts are a representation of an Entity’s financial accounts. An Account can be a checking or savings account (ACH) or a credit card, student loan, mortgage, personal loan, etc. (Liability). + +### Core + +#### Create Ach Account ```python account = method.accounts.create({ - 'holder_id': 'ent_y1a9e1fbnJ1f3', - 'ach': { - 'routing': '367537407', - 'number': '57838927', - 'type': 'checking' - } -}) + "holder_id": "ent_y1a9e1fbnJ1f3", + "ach": { + "routing": "367537407", + "number": "57838927", + "type": "checking", + }, +}); ``` -### Create Liability Account +#### Create Liability Account ```python account = method.accounts.create({ - 'holder_id': 'ent_au22b1fbFJbp8', - 'liability': { - 'mch_id': 'mch_2', - 'account_number': '1122334455' + "holder_id": "ent_au22b1fbFJbp8", + "liability": { + "mch_id": "mch_2", + "account_number": "1122334455", } -}) +}); ``` -### Retrieve Account +#### Retrieve Account ```python -account = method.accounts.get('acc_Zc4F2aTLt8CBt') +account = method.accounts.retrieve("acc_Zc4F2aTLt8CBt"); ``` -### List Accounts +#### List Accounts ```python -accounts = method.accounts.list() +accounts = method.accounts.list(); ``` -## ACH Verification +#### Withdraw an Account"s consent -### Create Micro-Deposits Verification +```python +account = method.accounts.withdraw_consent("acc_yVf3mkzbhz9tj"); +``` + +### Updates + +The Updates endpoint retrieves in real-time account data including Balance, due dates, APRs, directly from the Account’s financial institution. + +#### Create an Update + +```python +response = method + .accounts("acc_aEBDiLxiR8bqc") + .updates + .create(); +``` + +#### List all Updates + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .updates + .list(); +``` + +#### Retrieve an Update + +```python +response = method + .accounts("acc_aEBDiLxiR8bqc") + .updates + .retrieve("upt_NYV5kfjskTTCJ"); +``` + +### Transactions + +The Transactions endpoint retrieves real-time transaction (authorization, clearing, etc) notifications for Credit Card Accounts directly from the card networks. + +>Subscription to Transactions is required before receiving transactional data for an account + +#### List all Transactions + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .transactions + .list({ + "from_date": "2024-03-13", + "to_date": "2024-03-15", + }); +``` + +#### Retrieve a Transaction + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .transactions + .retrieve("txn_aRrDMAmEAtHti"); +``` + +### Card Brand + +The CardBrand endpoint retrieves the associated credit card metadata (Product / Brand Name, Art, etc) directly from the card issuer. + +#### Create a Card Brand + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .card_brands + .create(); +``` + +#### Retrieve a Card Brand + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .card_brands + .retrieve("cbrd_eVMDNUPfrFk3e"); +``` + +### Payoffs + +The Payoffs endpoint retrieves a payoff quote in real-time from the Account’s financial institution / lender. + +>Payoffs are currently only available for Auto Loan accounts + +#### Create a Payoff + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .payoffs + .create(); +``` + +#### List all Payoffs + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .payoffs + .list(); +``` + +#### Retrieve a Payoff + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .payoffs + .retrieve("pyf_ELGT4hfikTTCJ"); +``` + +### Balances + +The Balance endpoint retrieves the real-time balance from the Account’s financial institution. + +#### Create a Balance + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .balances + .create(); +``` + +#### List all Balances + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .balances + .list(); +``` + +#### Retrieve a Balance + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .balances + .retrieve("bal_ebzh8KaR9HCBG"); +``` + +### Account Sensitive + +The Sensitive endpoint returns underlying sensitive Account information (e.g. PAN, CVV, account number). This product is only available for Liabilities. + +>The Sensitive endpoint is restricted to most teams, and requires PCI compliance to access. Contact your Method CSM for more information. + +#### Create a Sensitive + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .sensitive + .create({ + "expand": [ + "credit_card.number", + "credit_card.exp_month", + "credit_card.exp_year", + "credit_card.cvv" + ], + }); +``` + +#### Retrieve a Sensitive + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .sensitive + .retrieve("astv_9WBBA6TH7n7iX"); +``` + +### Account Products + +The Account Products endpoint outlines the Products (capabilities) an Account has access to, and provides an overview of the status of all the Products. + +>Most products are accessible by default. However, some products have restricted access requiring team-by-team enablement and elevated account verification. + +#### List all Products + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .products + .list(); +``` + +#### Retrieve a Product + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .products + .retrieve("prd_FQFHqVNiCRb7J"); +``` +### Account Subscriptions + +The Account Subscriptions endpoint controls the state of all Subscriptions for an Account. + +Subscriptions are Products that can provide continuous updates via Webhooks. (e.g. Transaction Subscription provides real-time updates on a Credit Card’s transactions) + +>Most subscriptions are accessible by default. However, some subscriptions have restricted access requiring team-by-team enablement and elevated account verification. + +#### Create a Subscription + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .subscriptions + .create("update"); +``` + +#### List all Subscriptions + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .subscriptions + .list(); +``` + +#### Retrieve a Subscription + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .subscriptions + .retrieve("sub_P8c4bjj6xajxF"); +``` + +#### Delete a Subscription + +```python +response = method + .accounts("acc_yVf3mkzbhz9tj") + .subscriptions + .delete("sub_xM4VcfRWcJP8D"); +``` + +### Account Verification Sessions + +The account verification manages required verification to enable products for ACH or Liability accounts. + +For example, ACH Accounts require a verified AccountVerificationSession before they can be used as a source for Payments. + +#### Create Verification ```python verification = method - .accounts('acc_b9q2XVAnNFbp3') - .verification - .create({ 'type': 'micro_deposits' }) + .accounts("acc_b9q2XVAnNFbp3") + .verification_sessions + .create({ "type": "" }); ``` -### Create Plaid Verification +#### Update Micro Deposits Verification ```python verification = method - .accounts('acc_b9q2XVAnNFbp3') - .verification - .create({ - 'type': 'plaid', - 'plaid': { - 'balances': { - 'available': 100, - 'current': 110, - 'iso_currency_code': 'USD', - 'limit': None, - 'unofficial_currency_code': None + .accounts("acc_yVf3mkzbhz9tj") + .verification_sessions + .update("avf_yBQQNKmjRBTqF", { + "micro_deposits": { + "amounts": [10, 34] + } + }); +``` + +#### Update Plaid Verification + +```python +verification = method + .accounts("acc_yVf3mkzbhz9tj") + .verification_sessions + .update("avf_DjkdemgTQfqRD", { + "plaid": { + "balances": { + "available": 100, + "current": 110, + "iso_currency_code": "USD", + "limit": None, + "unofficial_currency_code": None }, - 'transactions': [ + "transactions": [ ... ] } - }) + }); ``` -### Create Teller Verification +#### Update Teller Verification ```python verification = method - .accounts('acc_b9q2XVAnNFbp3') - .verification - .create({ - 'type': 'teller', - 'teller': { - 'balances': { - 'account_id': 'acc_ns9gkibeia6ad0rr6s00q', - 'available': '93011.13', - 'ledger': '93011.13', - 'links': { - 'account': 'https://api.teller.io/accounts/acc_ns9gkibeia6ad0rr6s00q', - 'self': 'https://api.teller.io/accounts/acc_ns9gkibeia6ad0rr6s00q/balances' + .accounts("acc_yVf3mkzbhz9tj") + .verification_sessions + .update("avf_DjkdemgTQfqRD", { + "teller": { + "balances": { + "account_id": "acc_ns9gkibeia6ad0rr6s00q", + "available": "93011.13", + "ledger": "93011.13", + "links": { + "account": "https://reference.teller.io/accounts/acc_ns9gkibeia6ad0rr6s00q", + "self": "https://reference.teller.io/accounts/acc_ns9gkibeia6ad0rr6s00q/balances" } }, - 'transactions': [ - { - 'account_id': 'acc_ns9gkia42a6ad0rr6s000', - 'amount': '-51.19', - 'date': '2022-01-04', - 'description': 'Venmo Payment', - 'details': { - 'category': 'services', - 'counterparty': { - 'name': 'LOUISE BENTLEY', - 'type': 'person' - }, - 'processing_status': 'complete' - }, - 'id': 'txn_ns9gkiph2a6ad0rr6s000', - 'links': { - 'account': 'https://api.teller.io/accounts/acc_ns9gkia42a6ad0rr6s000', - 'self': 'https://api.teller.io/accounts/acc_ns9gkia42a6ad0rr6s000/transactions/txn_ns9gkiph2a6ad0rr6s000' - }, - 'running_balance': None, - 'status': 'pending', - 'type': 'digital_payment' - } + "transactions": [ + ... ] } - }) + }); ``` -### Create MX Verification +#### Update MX Verification ```python verification = method - .accounts('acc_b9q2XVAnNFbp3') - .verification - .create({ - 'type': 'mx', - 'mx': { - 'account ': { - 'institution_code': 'chase', - 'guid': 'ACT-06d7f44b-caae-0f6e-1384-01f52e75dcb1', - 'account_number': None, - 'apr': None, - 'apy': None, - 'available_balance': 1000.23, - 'available_credit': None, - 'balance': 1000.23, - 'cash_balance': 1000.32, - 'cash_surrender_value': 1000.23, - 'created_at': '2016-10-13T17:57:37+00:00' + .accounts("acc_yVf3mkzbhz9tj") + .verification_sessions + .update("avf_DjkdemgTQfqRD", { + "mx": { + "account": { + "institution_code": "chase", + "guid": "ACT-06d7f44b-caae-0f6e-1384-01f52e75dcb1", + "account_number": None, + "apr": None, + "apy": None, + "available_balance": 1000.23, + "available_credit": None, + "balance": 1000.23, + "cash_balance": 1000.32, + "cash_surrender_value": 1000.23, + "created_at": "2016-10-13T17:57:37+00:00", ... }, - 'transactions': [ + "transactions": [ ... ] - } - ) + } + }); ``` -### Update Verification +#### Update Standard Verification ```python verification = method - .accounts('acc_b9q2XVAnNFbp3') - .verification - .update({ - 'micro_deposits': { - 'amounts': [10, 4] + .accounts("acc_yVf3mkzbhz9tj") + .verification_sessions + .update("avf_DjkdemgTQfqRD", { + "standard": { + "number": "4111111111111111", } - }) + }); ``` -### Retrieve Verification +#### Update Pre-auth Verification ```python verification = method - .accounts('acc_b9q2XVAnNFbp3') - .verification - .get() + .accounts("acc_yVf3mkzbhz9tj") + .verification_sessions + .update("avf_DjkdemgTQfqRD", { + "pre_auth": { + "cvv": "031" + } + }); ``` -## Merchants +#### Retrieve Verification + +```python +verification = method + .accounts("acc_b9q2XVAnNFbp3") + .verification_sessions + .retrieve("avf_DjkdemgTQfqRD"); +``` + +## Merchants + +Merchants are resources that represent a specific type of liability for a financial institution. Method supports the majority of the financial institutions in the U.S. -### List Merchants +>Financial institutions that offer multiple liability products are represented in Method as separate Merchants. + +### Core + +#### List Merchants ```python -merchants = method.merchants.list() +merchants = method.merchants.list(); ``` -### Retrieve Merchant +#### Retrieve Merchant ```python -merchant = method.merchants.get('mch_1') +merchant = method.merchants.retrieve("mch_1"); ``` ## Payments -### Create Payment +A Payment is the transfer of funds from a source checking or savings bank account to a destination credit card, auto loan, mortgage, student loan, and more. + +All Payments are processed electronically between the source and destination, and take 2-3 business days depending on the receiving financial institution. +### Core + +#### Create Payment ```python payment = method.payments.create({ - 'amount': 5000, - 'source': 'acc_JMJZT6r7iHi8e', - 'destination': 'acc_AXthnzpBnxxWP', - 'description': 'Loan Pmt' -}) + "amount": 5000, + "source": "acc_JMJZT6r7iHi8e", + "destination": "acc_AXthnzpBnxxWP", + "description": "Loan Pmt", +}); ``` -### Retrieve Payment +#### Retrieve Payment ```python -payment = method.payments.get('pmt_rPrDPEwyCVUcm') +payment = method.payments.retrieve("pmt_rPrDPEwyCVUcm"); ``` -### Delete Payment +#### Delete Payment ```python -payment = method.payments.delete('pmt_rPrDPEwyCVUcm') +payment = method.payments.delete("pmt_rPrDPEwyCVUcm"); ``` -### List Payments +#### List Payments ```python -payments = method.payments.list() +payments = method.payments.list(); ``` -## Reversals +### Reversals -### Retrieve Reversal +#### Retrieve Reversal ```python -reversal = method.payments('pmt_rPrDPEwyCVUcm').reversals.get('rvs_eaBAUJtetgMdR') +reversal = method.payments("pmt_rPrDPEwyCVUcm").reversals.retrieve("rvs_eaBAUJtetgMdR"); ``` -### Update Reversal +#### Update Reversal ```python reversal = method - .payments('pmt_rPrDPEwyCVUcm') + .payments("pmt_rPrDPEwyCVUcm") .reversals - .update('rvs_eaBAUJtetgMdR', { 'status': 'pending' }) + .update("rvs_eaBAUJtetgMdR", { "status": "pending" }); ``` -### List Reversals for Payment +#### List Reversals for Payment ```python -reversals = method.payments('pmt_rPrDPEwyCVUcm').reversals.list() +reversals = method.payments("pmt_rPrDPEwyCVUcm").reversals.list(); ``` ## Webhooks -### Create Webhook +Webhooks allow the Method API to notify your application when certain events occur. + +To receive Webhook notifications, create a Webhook by registering a URL pointing to your application where triggered events should be sent to. This URL is where Method will send event information in an HTTPS POST request. + +​ +Handling webhooks +A Webhook event is considered successfully delivered when the corresponding URL endpoint responds with an HTTP status code of 200 within 5 seconds. + +If the criteria is not met, Method will reattempt 4 more times with each new attempt being delayed according to an exponential backoff algorithm, where the delay period between each attempt exponentially increases. + +>Webhooks that consistently fail to respond with a 200 will automatically be disabled. + +### Core + +#### Create Webhook ```python webhook = method.webhooks.create({ - 'type': 'payment.update', - 'url': 'https://api.example.app/webhook', - 'auth_token': 'md7UqcTSmvXCBzPORDwOkE' -}) + "type": "payment.update", + "url": "https://api.example.app/webhook", + "auth_token": "md7UqcTSmvXCBzPORDwOkE", +}); ``` -### Retrieve Webhook +#### Retrieve Webhook ```python -webhook = method.webhooks.get('whk_cSGjA6d9N8y8R') +webhook = method.webhooks.retrieve("whk_cSGjA6d9N8y8R"); ``` -### Delete Webhoook +#### Delete Webhoook ```python -webhook = method.webhooks.delete('whk_cSGjA6d9N8y8R') +webhook = method.webhooks.delete("whk_cSGjA6d9N8y8R"); ``` -### List Webhooks +#### List Webhooks ```python -webhooks = method.webhooks.list() +webhooks = method.webhooks.list(); ``` ## Reports -### Create Report +Reports provide a filtered snapshot view of a specific resource. Method provides a fixed set of filters (Report Types) which include the following: + + - The resource to filter + - The applied filter + - The snapshot window + +### Core + +#### Create Report ```python -report = method.reports.create({ 'type': 'payments.created.current' }) +report = method.reports.create({ "type": "payments.created.current" }); ``` -### Retrieve Report +#### Retrieve Report + +```python +report = method.reports.retrieve("rpt_cj2mkA3hFyHT5"); +``` + +#### Download Report + +```python +reportCSV = method.reports.download("rpt_cj2mkA3hFyHT5"); +``` + +## Simulations + +To provide a seamless integration experience with Method in the development environment, you can simulate creations or updates for specific resources on-demand. + +This ensures that your application handles all cases for multistep flows that would naturally occur in live environments (sandbox and production). + +>Simulation endpoints are only accessible in the development environment. Attempts to access these endpoints in sandbox or production will result in a 403 Forbidden error. + +### Core + +#### Update a payment"s status ```python -report = method.reports.get('rpt_cj2mkA3hFyHT5') +payment = method + .simulate + .payments + .update("pmt_rPrDPEwyCVUcm", { "status": "processing" }); ``` -### Download Report +#### Create a Transaction ```python -report_csv = method.reports.download('rpt_cj2mkA3hFyHT5') +transaction = method + .simulate + .accounts("acc_r6JUYN67HhCEM") + .transactions + .create(); ``` diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/method/method.py b/method/method.py index 8abe147..ed2901f 100644 --- a/method/method.py +++ b/method/method.py @@ -1,48 +1,39 @@ from method.configuration import Configuration, ConfigurationOpts -from method.resources.Account import AccountResource -from method.resources.Bin import BinResource -from method.resources.Entity import EntityResource -from method.resources.Element import ElementResource +from method.resources.Accounts import AccountResource +from method.resources.Entities import EntityResource +from method.resources.Elements import ElementResource from method.resources.Merchant import MerchantResource -from method.resources.Payment import PaymentResource +from method.resources.Payments import PaymentResource from method.resources.Report import ReportResource -from method.resources.RoutingNumber import RoutingNumberResource from method.resources.Webhook import WebhookResource from method.resources.HealthCheck import PingResponse, HealthCheckResource from method.resources.Simulate import SimulateResource -from method.resources.Transaction import TransactionResource class Method: accounts: AccountResource - bins: BinResource entities: EntityResource elements: ElementResource merchants: MerchantResource payments: PaymentResource reports: ReportResource - routing_numbers: RoutingNumberResource webhooks: WebhookResource healthcheck: HealthCheckResource simulate: SimulateResource - transaction: TransactionResource def __init__(self, opts: ConfigurationOpts = None, **kwargs: ConfigurationOpts): _opts: ConfigurationOpts = {**(opts or {}), **kwargs} # type: ignore config = Configuration(_opts) self.accounts = AccountResource(config) - self.bins = BinResource(config) self.entities = EntityResource(config) self.elements = ElementResource(config) self.merchants = MerchantResource(config) self.payments = PaymentResource(config) self.reports = ReportResource(config) - self.routing_numbers = RoutingNumberResource(config) self.webhooks = WebhookResource(config) self.healthcheck = HealthCheckResource(config) self.simulate = SimulateResource(config) - self.transaction = TransactionResource(config) def ping(self) -> PingResponse: - return self.healthcheck.get() + return self.healthcheck.retrieve() diff --git a/method/resource.py b/method/resource.py index aeb916b..df99933 100644 --- a/method/resource.py +++ b/method/resource.py @@ -1,15 +1,31 @@ from importlib.metadata import version import json -from typing import Optional, List, Dict, Any, TypedDict +from typing import Optional, List, Dict, Any, TypedDict, Literal from hammock import Hammock as Client # type: ignore from method.configuration import Configuration from method.errors import MethodError +ResourceStatusLiterals = Literal[ + 'completed', + 'in_progress', + 'pending', + 'failed' +] + + class RequestOpts(TypedDict): idempotency_key: Optional[str] +class ResourceListOpts(TypedDict): + from_date: Optional[str] + to_date: Optional[str] + page: Optional[int | str] + page_limit: Optional[int | str] + page_cursor: Optional[int | str] + + class Resource: config: Configuration client: Client @@ -20,6 +36,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' }) @MethodError.catch diff --git a/method/resources/Account.py b/method/resources/Account.py deleted file mode 100644 index 9dcfb78..0000000 --- a/method/resources/Account.py +++ /dev/null @@ -1,551 +0,0 @@ -from typing import TypedDict, Optional, Dict, List, Any, Literal, Union - -from method.resource import Resource, RequestOpts -from method.configuration import Configuration -from method.errors import ResourceError -from method.resources.Verification import VerificationResource -from method.resources.AccountSync import AccountSyncResource, AccountSync - - -# Literals, keep ordered alphabetically -AccountCapabilitiesLiterals = Literal[ - 'payments:receive', - 'payments:send', - 'data:retrieve', - 'data:sync' -] - - -AccountClearingSubTypesLiterals = Literal[ - 'single_use' -] - - -AccountLiabilityDataSourcesLiterls = Literal[ - 'credit_report', - 'financial_institution', - 'unavailable' -] - - -AccountLiabilityDataStatusesLiterals = Literal[ - 'active', - 'syncing', - 'unavailable', - 'failed', - 'pending' -] - - -AccountLiabilityPaymentStatuesLiterals = Literal[ - 'active', - 'activating', - 'unavailable' -] - - -AccountLiabilitySyncTypesLiterals = Literal[ - 'manual', - 'auto' -] - - -AccountLiabilityTypesLiterals = Literal[ - 'student_loan', - 'student_loans', - 'credit_card', - 'mortgage', - 'auto_loan', - 'collection', - 'personal_loan', - 'business_loan', - 'insurance', - 'credit_builder', - 'subscription', - 'utility', - 'medical', - 'loan' -] - - -AccountStatusesLiterals = Literal[ - 'active', - 'disabled', - 'closed', - 'processing' -] - - -AccountSubTypesLiterals = Literal[ - 'checking', - 'savings' -] - - -AccountTypesLiterals = Literal[ - 'ach', - 'liability', - 'clearing' -] - -AutoPayStatusesLiterals = Literal[ - 'unknown', - 'active', - 'inactive' -] - - -TradelineAccountOwnershipLiterals = Literal[ - 'primary', - 'authorized', - 'joint', - 'unknown' -] - - -PastDueStatusesLiterals = Literal[ - 'unknown', - 'overdue', - 'on_time' -] - - -DelinquencyStatusLiterals = Literal[ - 'good_standing', - 'past_due', - 'major_delinquency', - 'unavailable' -] - - -DelinquencyPeriodLiterals = Literal[ - 'less_than_30', - '30', - '60', - '90', - '120', - 'over_120' -] - -AccountPayoffStatusesLiterals = Literal[ - 'completed', - 'in_progress', - 'pending', - 'failed' -] - -class AccountACH(TypedDict): - routing: int - number: int - type: AccountSubTypesLiterals - - -class AccountLiabilityLoan(TypedDict): - name: str - balance: Optional[int] - opened_at: Optional[str] - original_loan_amount: Optional[int] - sub_type: Optional[str] - term_length: Optional[int] - closed_at: Optional[str] - last_payment_amount: Optional[int] - last_payment_date: Optional[str] - next_payment_minimum_amount: Optional[int] - next_payment_due_date: Optional[str] - interest_rate_type: Literal['fixed', 'variable'] - interest_rate_percentage: Optional[int] - interest_rate_source: Optional[Literal['financial_institution', 'public_data', 'method']] - - -class AccountLiabilityCreditCard(AccountLiabilityLoan): - sub_type: Optional[Literal['flexible_spending', 'charge', 'secured', 'unsecured', 'purchase', 'business']] - last_statement_balance: Optional[int] - remaining_statement_balance: Optional[int] - available_credit: Optional[int] - auto_pay_status: Optional[AutoPayStatusesLiterals] - auto_pay_amount: Optional[int] - auto_pay_date: Optional[str] - past_due_status: Optional[PastDueStatusesLiterals] - past_due_balance: Optional[int] - past_due_date: Optional[str] - credit_limit: Optional[int] - pending_purchase_authorization_amount: Optional[int] - pending_credit_authorization_amount: Optional[int] - interest_saving_balance: Optional[int] - next_statement_date: Optional[str] - - -class AccountLiabilityAutoLoan(AccountLiabilityLoan): - sub_type: Optional[Literal['lease', 'loan']] - payoff_amount: Optional[int] - payoff_amount_term: Optional[int] - past_due_status: Optional[PastDueStatusesLiterals] - past_due_balance: Optional[int] - past_due_date: Optional[str] - late_fees_amount: Optional[int] - expected_payoff_date: Optional[str] - principal_balance: Optional[int] - per_diem_amount: Optional[int] - mileage_allocation: Optional[int] - - -class AccountLiabilityStudentLoan(AccountLiabilityLoan): - sub_type: Optional[Literal['federal', 'private']] - sequence: Optional[int] - disbursed_at: Optional[str] - expected_payoff_date: Optional[str] - payoff_amount: Optional[int] - payoff_amount_term: Optional[int] - principal_balance: Optional[int] - - -class DelinquencyHistoryItem(TypedDict): - start_date: str - end_date: str - status: DelinquencyStatusLiterals - period: Optional[DelinquencyPeriodLiterals] - - -class TrendedDataItem(TypedDict): - month: Optional[int] - year: Optional[int] - balance: Optional[int] - available_credit: Optional[int] - scheduled_payment: Optional[int] - actual_payment: Optional[int] - high_credit: Optional[int] - credit_limit: Optional[int] - amount_past_due: Optional[int] - last_payment_date: Optional[str] - account_status: str - payment_status:str - - -class AccountLiabilityStudentLoansDisbursement(AccountLiabilityLoan): - sequence: int - disbursed_at: Optional[str] - expected_payoff_date: Optional[str] - delinquent_status: Optional[str] - delinquent_amount: Optional[int] - delinquent_period: Optional[int] - delinquent_action: Optional[str] - delinquent_start_date: Optional[str] - delinquent_major_start_date: Optional[str] - delinquent_status_updated_at: Optional[str] - delinquent_history: Optional[List[DelinquencyHistoryItem]] - delinquent_action: Optional[List[TrendedDataItem]] - - -class AccountLiabilityStudentLoans(AccountLiabilityLoan): - sub_type: Optional[Literal['federal', 'private']] - disbursed_at: Optional[str] - expected_payoff_date: Optional[str] - interest_rate_type: Optional[Literal['fixed', 'variable']] - expected_payoff_date: Optional[str] - disbursements: Optional[AccountLiabilityStudentLoansDisbursement] - - -class AccountLiabilityMortgage(AccountLiabilityLoan): - principal_balance: Optional[int] - expected_payoff_date: Optional[str] - address_street: Optional[str] - address_city: Optional[str] - address_state: Optional[str] - address_zip: Optional[str] - property_value: Optional[int] - past_due_status: Optional[PastDueStatusesLiterals] - past_due_balance: Optional[int] - past_due_date: Optional[str] - payoff_amount: Optional[int] - payoff_amount_term: Optional[int] - year_to_date_interest_paid: Optional[int] - year_to_date_principal_paid: Optional[int] - year_to_date_taxes_paid: Optional[int] - year_start_principal_balance: Optional[int] - escrow_balance: Optional[int] - - -class AccountLiabilityPersonalLoan(AccountLiabilityLoan): - sub_type: Optional[Literal['line_of_credit', 'heloc', 'secured', 'unsecured', 'note']] - expected_payoff_date: Optional[str] - available_credit: Optional[int] - principal_balance: Optional[int] - year_to_date_interest_paid: Optional[int] - - -class AccountLiabilityCreditBuilder(AccountLiabilityLoan): - pass - - -class AccountLiabilityCollection(AccountLiabilityLoan): - pass - - -class AccountLiabilityBusinessLoan(TypedDict): - name: str - balance: Optional[int] - opened_at: Optional[str] - - -class AccountLiabilityInsurance(TypedDict): - name: str - balance: Optional[int] - opened_at: Optional[str] - - -class AccountLiabilitySubscription(TypedDict): - name: str - balance: Optional[int] - opened_at: Optional[str] - - -class AccountLiabilityUtility(TypedDict): - name: str - balance: Optional[int] - opened_at: Optional[str] - - -class AccountLiabilityMedical(TypedDict): - name: str - balance: Optional[int] - opened_at: Optional[str] - - -class AccountLiability(TypedDict): - mch_id: str - mask: str - payment_status: AccountLiabilityPaymentStatuesLiterals - data_status: AccountLiabilityDataStatusesLiterals - data_last_successful_sync: Optional[str] - data_status_error: Optional[ResourceError] - data_source: AccountLiabilityDataSourcesLiterls - data_updated_at: Optional[str] - data_sync_type: AccountLiabilitySyncTypesLiterals - ownership: TradelineAccountOwnershipLiterals - hash: str - fingerprint: str - type: AccountLiabilityTypesLiterals - loan: Optional[AccountLiabilityLoan] - student_loan: Optional[AccountLiabilityStudentLoan] - student_loans: Optional[AccountLiabilityStudentLoans] - credit_card: Optional[AccountLiabilityCreditCard] - mortgage: Optional[AccountLiabilityMortgage] - auto_loan: Optional[AccountLiabilityAutoLoan] - personal_loan: Optional[AccountLiabilityPersonalLoan] - business_loan: Optional[AccountLiabilityBusinessLoan] - collection: Optional[AccountLiabilityCollection] - insurance: Optional[AccountLiabilityInsurance] - credit_builder: Optional[AccountLiabilityCreditBuilder] - subscription: Optional[AccountLiabilitySubscription] - utility: Optional[AccountLiabilityUtility] - medical: Optional[AccountLiabilityMedical] - - -class AccountClearing(TypedDict): - routing: int - number: int - - -class AccountCreateOpts(TypedDict): - holder_id: str - metadata: Optional[Dict[str, Any]] - - -class AccountACHCreateOpts(AccountCreateOpts): - ach: AccountACH - - -class LiabilityCreateOpts(TypedDict): - mch_id: str - account_number: Optional[str] - number: Optional[str] - - -class AccountLiabilityCreateOpts(AccountCreateOpts): - liability: LiabilityCreateOpts - - -class ClearingCreateOpts(TypedDict): - type: AccountClearingSubTypesLiterals - - -class AccountClearingCreateOpts(AccountCreateOpts): - clearing: ClearingCreateOpts - - -class Account(TypedDict): - id: str - holder_id: str - status: AccountStatusesLiterals - type: AccountTypesLiterals - ach: Optional[AccountACH] - liability: Optional[AccountLiability] - clearing: Optional[AccountClearing] - capabilities: List[AccountCapabilitiesLiterals] - available_capabilities: List[AccountCapabilitiesLiterals] - error: Optional[ResourceError] - created_at: str - updated_at: str - metadata: Optional[Dict[str, Any]] - - -class AccountDetail(TypedDict): - id: str - type: AccountTypesLiterals - aggregator: Optional[str] - name: str - institution_name: str - institution_logo: str - mask: str - created_at: str - updated_at: str - metadata: Optional[Dict[str, Any]] - - -AccountListOpts = TypedDict('AccountListOpts', { - 'to_date': Optional[str], - 'from_date': Optional[str], - 'page': Optional[int], - 'page_limit': Optional[int], - 'page_cursor': Optional[str], - 'status': Optional[str], - 'type': Optional[str], - 'holder_id': Optional[str], - 'liability.mch_id': Optional[str], - 'liability.type': Optional[str] -}) - - -class AccountCreateBulkSyncOpts(TypedDict): - acc_ids: List[str] - - -class AccountCreateBulkSyncResponse(TypedDict): - success: List[str] - failed: List[str] - results: List[AccountSync] - - -class AccountSensitive(TypedDict): - number: Optional[str] - encrypted_number: Optional[str] - bin_4: Optional[str] - bin_6: Optional[str] - payment_address: Optional[Any] - - -class AccountCreateBulkSensitiveResponse(TypedDict): - success: List[str] - failed: List[str] - results: List[AccountSensitive] - - -class AccountCreateBulkSensitiveOpts(TypedDict): - acc_ids: List[str] - - -class AccountWithdrawConsentOpts(TypedDict): - type: Literal['withdraw'] - reason: Optional[Literal['holder_withdrew_consent']] - - -class LiabilityMortgageUpdateOpts(TypedDict): - address_street: str - address_city: str - address_state: str - address_zip: str - - -class LiabilityCreditCardUpdateNumberOpts(TypedDict): - number: str - - -class LiabilityCreditCardUpdateExpirationOpts(TypedDict): - expiration_month: int - expiration_year: int - - -class LiabilityUpdateOpts(TypedDict): - mortgage: Optional[LiabilityMortgageUpdateOpts] - credit_card: Union[LiabilityCreditCardUpdateNumberOpts, LiabilityCreditCardUpdateExpirationOpts] - - -class CreditReportTradelinePaymentHistoryItem(TypedDict): - code: int - date: str - - -class AccountPaymentHistory(TypedDict): - payment_history: List[CreditReportTradelinePaymentHistoryItem] - - -class AccountPayoff(TypedDict): - id: str - status: AccountPayoffStatusesLiterals - amount: Optional[int] - term: Optional[int] - per_diem_amount: Optional[int] - error: Optional[ResourceError] - created_at: str - updated_at: str - - -class AccountSubResources: - verification: VerificationResource - sync: AccountSyncResource - - def __init__(self, _id: str, config: Configuration): - self.verification = VerificationResource(config.add_path(_id)) - self.syncs = AccountSyncResource(config.add_path(_id)) - -class AccountResource(Resource): - def __init__(self, config: Configuration): - super(AccountResource, self).__init__(config.add_path('accounts')) - - def __call__(self, _id: str) -> AccountSubResources: - return AccountSubResources(_id, self.config) - - def get(self, _id: str) -> Account: - return super(AccountResource, self)._get_with_id(_id) - - def update(self, _id: str, opts: LiabilityUpdateOpts) -> Account: - return super(AccountResource, self)._update_with_id(_id, opts) - - def list(self, params: Optional[AccountListOpts] = None) -> List[Account]: - return super(AccountResource, self)._list(params) - - def create(self, opts: Union[AccountACHCreateOpts, AccountLiabilityCreateOpts, AccountClearingCreateOpts], request_opts: Optional[RequestOpts] = None) -> Account: - return super(AccountResource, self)._create(opts, request_opts) - - def get_payment_history(self, _id: str) -> AccountPaymentHistory: - return super(AccountResource, self)._get_with_sub_path('{_id}/payment_history'.format(_id=_id)) - - def get_details(self, _id: str) -> AccountDetail: - return super(AccountResource, self)._get_with_sub_path('{_id}/details'.format(_id=_id)) - - def bulk_sync(self, acc_ids: AccountCreateBulkSyncOpts) -> AccountCreateBulkSyncResponse: - return super(AccountResource, self)._create_with_sub_path('bulk_sync',{ acc_ids }) - - def sync(self, _id: str) -> AccountSync: - return super(AccountResource, self)._create_with_sub_path('{_id}/syncs'.format(_id=_id), {}) - - def bulk_sensitive(self, acc_ids: AccountCreateBulkSensitiveOpts) -> AccountCreateBulkSensitiveResponse: - return super(AccountResource, self)._create_with_sub_path('bulk_sensitive',{ acc_ids }) - - def sensitive(self, _id: str) -> AccountSensitive: - return super(AccountResource, self)._get_with_sub_path('{_id}/sensitive'.format(_id=_id)) - - def enroll_auto_syncs(self, _id: str) -> Account: - return super(AccountResource, self)._create_with_sub_path('{_id}/sync_enrollment'.format(_id=_id), {}) - - def unenroll_auto_syncs(self, _id: str) -> Account: - return super(AccountResource, self)._delete_with_sub_path('{_id}/sync_enrollment'.format(_id=_id)) - - def withdraw_consent(self, _id: str, data: AccountWithdrawConsentOpts = { 'type': 'withdraw', 'reason': 'holder_withdrew_consent' }) -> Account: - return super(AccountResource, self)._create_with_sub_path('{_id}/consent'.format(_id=_id), data) - - def get_payoff(self, _id: str, pyf_id: str) -> AccountPayoff: - return super(AccountResource, self)._get_with_sub_path('{_id}/payoffs/{pyf_id}'.format(_id=_id, pyf_id=pyf_id)) - - def create_payoff(self, _id: str) -> AccountPayoff: - return super(AccountResource, self)._create_with_sub_path('{_id}/payoffs'.format(_id=_id), {}) diff --git a/method/resources/AccountSync.py b/method/resources/AccountSync.py deleted file mode 100644 index dba089e..0000000 --- a/method/resources/AccountSync.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import TypedDict, Optional - -from method.resource import Resource, RequestOpts -from method.configuration import Configuration -from method.errors import ResourceError -from method.resources.Verification import VerificationResource - - -class AccountSync(TypedDict): - id: str - acc_id: str - status: str - error: Optional[ResourceError] - created_at: str - updated_at: str - -class AccountSyncCreateOpts(TypedDict): - pass - -class AccountSyncResource(Resource): - def __init__(self, config: Configuration): - super(AccountSyncResource, self).__init__(config.add_path('syncs')) - - def get(self, _id: str) -> AccountSync: - return super(AccountSyncResource, self)._get_with_id(_id) - - def create(self, opts: Optional[AccountSyncCreateOpts] = {}) -> AccountSync: - return super(AccountSyncResource, self)._create(opts) diff --git a/method/resources/Accounts/Account.py b/method/resources/Accounts/Account.py new file mode 100644 index 0000000..8309306 --- /dev/null +++ b/method/resources/Accounts/Account.py @@ -0,0 +1,124 @@ +from typing import TypedDict, Optional, Dict, List, Any, Literal, Union, TypeVar +from method.resource import Resource, RequestOpts +from method.errors import ResourceError +from method.configuration import Configuration +from method.resources.Accounts.Types import AccountACH, AccountStatusesLiterals, AccountTypesLiterals, \ + AccountProductTypesLiterals, AccountSubscriptionTypesLiterals, AccountExpandableFieldsLiterals, \ + AccountLiability +from method.resources.Accounts.Balances import AccountBalance, AccountBalancesResource +from method.resources.Accounts.CardBrands import AccountCardBrand, AccountCardBrandsResource +from method.resources.Accounts.Payoffs import AccountPayoff, AccountPayoffsResource +from method.resources.Accounts.Sensitive import AccountSensitive, AccountSensitiveResource +from method.resources.Accounts.Subscriptions import AccountSubscriptionsResource +from method.resources.Accounts.Transactions import AccountTransaction, AccountTransactionsResource +from method.resources.Accounts.Updates import AccountUpdate, AccountUpdatesResource +from method.resources.Accounts.VerificationSessions import AccountVerificationSession, AccountVerificationSessionResource + + +class AccountCreateOpts(TypedDict): + holder_id: str + metadata: Optional[Dict[str, Any]] + + +class AccountACHCreateOpts(AccountCreateOpts): + ach: AccountACH + + +class LiabilityCreateOpts(TypedDict): + mch_id: str + account_number: Optional[str] + number: Optional[str] + + +class AccountLiabilityCreateOpts(AccountCreateOpts): + liability: LiabilityCreateOpts + + +AccountListOpts = TypedDict('AccountListOpts', { + 'to_date': Optional[str], + 'from_date': Optional[str], + 'page': Optional[int], + 'page_limit': Optional[int], + 'page_cursor': Optional[str], + 'status': Optional[str], + 'type': Optional[str], + 'holder_id': Optional[str], + 'expand': Optional[List[AccountExpandableFieldsLiterals]], + 'liability.mch_id': Optional[str], + 'liability.type': Optional[str] +}) + + +class Account(TypedDict): + id: str + holder_id: str + status: AccountStatusesLiterals + type: AccountTypesLiterals + ach: Optional[AccountACH] + liability: Optional[AccountLiability] + products: List[AccountProductTypesLiterals] + restricted_products: List[AccountProductTypesLiterals] + subscriptions: Optional[List[AccountSubscriptionTypesLiterals]] + available_subscriptions: Optional[List[AccountSubscriptionTypesLiterals]] + restricted_subscriptions: Optional[List[AccountSubscriptionTypesLiterals]] + sensitive: Optional[Union[str, AccountSensitive]] + balance: Optional[Union[str, AccountBalance]] + card_brand: Optional[Union[str, AccountCardBrand]] + payoff: Optional[Union[str, AccountPayoff]] + transaction: Optional[Union[str, AccountTransaction]] + update: Optional[Union[str, AccountUpdate]] + latest_verification_session: Optional[Union[str, AccountVerificationSession]] + error: Optional[ResourceError] + created_at: str + updated_at: str + metadata: Optional[Dict[str, str]] + + +T = TypeVar('T', bound='Account') + + +class AccountWithdrawConsentOpts(TypedDict): + type: Literal['withdraw'] + reason: Optional[Literal['holder_withdrew_consent']] + + +class AccountSubResources: + balances: AccountBalancesResource + card_brands: AccountCardBrandsResource + payoffs: AccountPayoffsResource + sensitive: AccountSensitiveResource + subscriptions: AccountSubscriptionsResource + transactions: AccountTransactionsResource + updates: AccountUpdatesResource + verification_sessions: AccountVerificationSessionResource + + + def __init__(self, _id: str, config: Configuration): + self.balances = AccountBalancesResource(config.add_path(_id)) + self.card_brands = AccountCardBrandsResource(config.add_path(_id)) + self.payoffs = AccountPayoffsResource(config.add_path(_id)) + self.sensitive = AccountSensitiveResource(config.add_path(_id)) + self.subscriptions = AccountSubscriptionsResource(config.add_path(_id)) + self.transactions = AccountTransactionsResource(config.add_path(_id)) + self.updates = AccountUpdatesResource(config.add_path(_id)) + self.verification_sessions = AccountVerificationSessionResource(config.add_path(_id)) + + +class AccountResource(Resource): + def __init__(self, config: Configuration): + super(AccountResource, self).__init__(config.add_path('accounts')) + + def __call__(self, acc_id: str) -> AccountSubResources: + return AccountSubResources(acc_id, self.config) + + def retrieve(self, acc_id: str, params: Optional[Dict[str, List[AccountExpandableFieldsLiterals]]] = None) -> Account: + return super(AccountResource, self)._get_with_sub_path_and_params(acc_id, params) + + def list(self, params: Optional[AccountListOpts[T]] = None) -> List[Account]: + return super(AccountResource, self)._list(params) + + def create(self, opts: Union[AccountACHCreateOpts, AccountLiabilityCreateOpts], request_opts: Optional[RequestOpts] = None) -> Account: + return super(AccountResource, self)._create(opts, request_opts) + + def withdraw_consent(self, acc_id: str, data: AccountWithdrawConsentOpts = { 'type': 'withdraw', 'reason': 'holder_withdrew_consent' }) -> Account: + return super(AccountResource, self)._create_with_sub_path('{acc_id}/consent'.format(acc_id=acc_id), data) diff --git a/method/resources/Accounts/Balances.py b/method/resources/Accounts/Balances.py new file mode 100644 index 0000000..2a8dc8f --- /dev/null +++ b/method/resources/Accounts/Balances.py @@ -0,0 +1,33 @@ +from typing import TypedDict, Optional, Literal + +from method.resource import Resource +from method.configuration import Configuration +from method.errors import ResourceError + +AccountBalanceStatusLiterals = Literal[ + 'completed', + 'in_progress', + 'pending', + 'failed' +] + + +class AccountBalance(TypedDict): + id: str + account_id: str + status: AccountBalanceStatusLiterals + amount: Optional[int] + error: Optional[ResourceError] + created_at: str + updated_at: str + + +class AccountBalancesResource(Resource): + def __init__(self, config: Configuration): + super(AccountBalancesResource, self).__init__(config.add_path('balances')) + + def retrieve(self, bal_id: str) -> AccountBalance: + return super(AccountBalancesResource, self)._get_with_id(bal_id) + + def create(self) -> AccountBalance: + return super(AccountBalancesResource, self)._create({}) diff --git a/method/resources/Accounts/CardBrands.py b/method/resources/Accounts/CardBrands.py new file mode 100644 index 0000000..18cfbde --- /dev/null +++ b/method/resources/Accounts/CardBrands.py @@ -0,0 +1,36 @@ +from typing import TypedDict, Optional, Literal, List + +from method.resource import Resource +from method.configuration import Configuration +from method.errors import ResourceError + + +class AccountCardBrand(TypedDict): + art_id: str + url: str + name: str + + +class AccountCardBrand(TypedDict): + id: str + account_id: str + network: str + issuer: str + last4: str + brands: List[AccountCardBrand] + status: Literal['completed', 'failed'] + shared: bool + error: Optional[ResourceError] + created_at: str + updated_at: str + + +class AccountCardBrandsResource(Resource): + def __init__(self, config: Configuration): + super(AccountCardBrandsResource, self).__init__(config.add_path('card_brands')) + + def retrieve(self, crbd_id: str) -> AccountCardBrand: + return super(AccountCardBrandsResource, self)._get_with_id(crbd_id) + + def create(self) -> AccountCardBrand: + return super(AccountCardBrandsResource, self)._create({}) diff --git a/method/resources/Accounts/ExternalTypes.py b/method/resources/Accounts/ExternalTypes.py new file mode 100644 index 0000000..5ed9fbe --- /dev/null +++ b/method/resources/Accounts/ExternalTypes.py @@ -0,0 +1,262 @@ +from typing import TypedDict, Optional, List, Literal + + +PlaidTransactionTypesLiterals = Literal[ + 'digital', + 'place', + 'special', + 'unresolved' +] + + +PlaidTransactionPaymentChannelTypesLiterals = Literal[ + 'online', + 'in_store', + 'other' +] + + +PlaidTransactionCodeLiterals = Literal[ + 'adjustment', + 'atm', + 'bank charge', + 'bill payment', + 'cash', + 'cashback', + 'cheque', + 'direct debit', + 'interest', + 'purchase', + 'standing order', + 'transfer', + 'null' +] + + +PlaidCounterpartyTypeLiterals = Literal[ + 'merchant', + 'financial_institution', + 'payment_app', + 'marketplace', + 'payment_terminal', + 'income_source' +] + + +class PlaidBalance(TypedDict): + available: Optional[int] + current: Optional[int] + iso_currency_code: Optional[str] + limit: Optional[int] + unofficial_currency_code: Optional[str] + + +class PlaidLocation(TypedDict): + address: Optional[str] + city: Optional[str] + region: Optional[str] + postal_code: Optional[str] + country: Optional[str] + lat: Optional[int] + lon: Optional[int] + store_number: Optional[str] + + +class PlaidPaymentMeta(TypedDict): + reference_number: Optional[str] + ppd_id: Optional[str] + payee: Optional[str] + by_order_of: Optional[str] + payer: Optional[str] + payment_method: Optional[str] + payment_processor: Optional[str] + reason: Optional[str] + + +class PlaidPersonalFinanceCategory(TypedDict): + primary: str + detailed: str + confidence_level: Optional[str] + + +class PlaidTransactionCounterparty(TypedDict): + name: str + entity_id: Optional[str] + type: PlaidCounterpartyTypeLiterals; + website: Optional[str] + logo_url: Optional[str] + confidence_level: Optional[str] + + +class PlaidTransaction(TypedDict): + account_id: str + amount: int + iso_currency_code: Optional[str] + unofficial_currency_code: Optional[str] + category: Optional[List[str]] + category_id: Optional[str] + check_number: Optional[str] + date: str + location: PlaidLocation; + name: str + merchant_name: Optional[str] + original_description: Optional[str] + payment_meta: PlaidPaymentMeta; + pending: bool + pending_transaction_id: Optional[str] + account_owner: Optional[str] + transaction_id: str + transaction_type: Optional[PlaidTransactionTypesLiterals]; + logo_url: Optional[str] + website: Optional[str] + authorized_date: Optional[str] + authorized_datetime: Optional[str] + datetime: Optional[str] + payment_channel: PlaidTransactionPaymentChannelTypesLiterals + personal_finance_category: Optional[PlaidPersonalFinanceCategory] + transaction_code: Optional[PlaidTransactionCodeLiterals] + personal_finance_category_icon_url: str + counterparties: List[PlaidTransactionCounterparty] + merchant_entity_id: Optional[str] + + +class MXAccount(TypedDict): + account_number: str + account_ownership: str + annuity_policy_to_date: str + annuity_provider: str + annuity_term_year: int + apr: int + apy: int + available_balance: int + available_credit: int + balance: int + cash_balance: int + cash_surrender_value: int + created_at: str + credit_limit: int + currency_code: str + day_payment_is_due: int + death_benefit: int + guid: str + holdings_value: int + id: str + imported_at: str + interest_rate: int + institution_code: str + insured_name: str + is_closed: bool + is_hidden: bool + is_manual: bool + last_payment: int + last_payment_at: str + loan_amount: int + margin_balance: int + matures_on: str + member_guid: str + member_id: str + member_is_managed_by_user: bool + metadata: str + minimum_balance: int + minimum_payment: int + name: str + nickname: str + original_balance: int + pay_out_amount: int + payment_due_at: str + payoff_balance: int + premium_amount: int + property_type: str + routing_number: str + skip_webhook: bool + started_on: str + subtype: str + today_ugl_amount: int + today_ugl_percentage: int + total_account_value: int + type: str + updated_at: str + user_guid: str + user_id: str + + +class MXTransaction(TypedDict): + account_guid: str + account_id: str + amount: int + category: str + category_guid: str + check_number_string: str + created_at: str + currency_code: str + date: str + description: str + extended_transaction_type: str + guid: str + id: str + is_bill_pay: bool + is_direct_deposit: bool + is_expense: bool + is_fee: bool + is_income: bool + is_international: bool + is_overdraft_fee: bool + is_payroll_advance: bool + is_recurring: bool + is_subscription: bool + latitude: int + localized_description: str + localized_memo: str + longitude: int + member_guid: str + member_is_managed_by_user: bool + memo: str + merchant_category_code: int + merchant_guid: str + merchant_location_guid: str + metadata: str + original_description: str + posted_at: str + status: str + top_level_category: str + transacted_at: str + type: str + updated_at: str + user_guid: str + user_id: str + + +class TellerLinks(TypedDict): + account: str + self: str + + +class TellerBalance(TypedDict): + ledger: int + links: TellerLinks + account_id: str + available: int + + +class TellerTransactionCounterparty(TypedDict): + type: str + name: str + + +class TellerTransactionDetails(TypedDict): + category: str + counterparty: TellerTransactionCounterparty + processing_status: str + + +class TellerTransaction(TypedDict): + running_balance: Optional[int] + details: TellerTransactionDetails + description: str + account_id: str + date: str + id: str + links: TellerLinks + amount: int + type: str + status: str diff --git a/method/resources/Accounts/Payoffs.py b/method/resources/Accounts/Payoffs.py new file mode 100644 index 0000000..2a32b5a --- /dev/null +++ b/method/resources/Accounts/Payoffs.py @@ -0,0 +1,36 @@ +from typing import TypedDict, Optional, Literal + +from method.resource import Resource +from method.configuration import Configuration +from method.errors import ResourceError + + +AccountPayoffStatusesLiterals = Literal[ + 'completed', + 'in_progress', + 'pending', + 'failed' +] + + +class AccountPayoff(TypedDict): + id: str + account_id: str + status: AccountPayoffStatusesLiterals + amount: Optional[int] + term: Optional[int] + per_diem_amount: Optional[int] + error: Optional[ResourceError] + created_at: str + updated_at: str + + +class AccountPayoffsResource(Resource): + def __init__(self, config: Configuration): + super(AccountPayoffsResource, self).__init__(config.add_path('payoffs')) + + def retrieve(self, pyf_id: str) -> AccountPayoff: + return super(AccountPayoffsResource, self)._get_with_id(pyf_id) + + def create(self) -> AccountPayoff: + return super(AccountPayoffsResource, self)._create({}) diff --git a/method/resources/Accounts/Sensitive.py b/method/resources/Accounts/Sensitive.py new file mode 100644 index 0000000..636d8b2 --- /dev/null +++ b/method/resources/Accounts/Sensitive.py @@ -0,0 +1,57 @@ +from typing import TypedDict, Optional, Literal, List + +from method.resource import Resource +from method.configuration import Configuration +from method.errors import ResourceError + + +AccountSensitiveFieldsLiterals = Literal[ + 'auto_loan.number', + 'mortgage.number', + 'personal_loan.number', + 'credit_card.number', + 'credit_card.billing_zip_code', + 'credit_card.exp_month', + 'credit_card.exp_year', + 'credit_card.cvv' +] + + +class AccountSensitiveLoan(TypedDict): + number: str + + +class AccountSensitiveCreditCard(TypedDict): + number: Optional[str] + billing_zip_code: Optional[str] + exp_month: Optional[str] + exp_year: Optional[str] + cvv: Optional[str] + + +class AccountSensitive(TypedDict): + id: str + account_id: str + auto_loan: Optional[AccountSensitiveLoan] + credit_card: Optional[AccountSensitiveCreditCard] + mortgage: Optional[AccountSensitiveLoan] + personal_loan: Optional[AccountSensitiveLoan] + status: Literal['completed', 'failed'] + error: Optional[ResourceError] + created_at: str + updated_at: str + + +class AccountSensitiveCreateOpts(TypedDict): + expand: List[AccountSensitiveFieldsLiterals] + + +class AccountSensitiveResource(Resource): + def __init__(self, config: Configuration): + super(AccountSensitiveResource, self).__init__(config.add_path('sensitive')) + + def retrieve(self, astv_id: str) -> AccountSensitive: + return super(AccountSensitiveResource, self)._get_with_id(astv_id) + + def create(self, data: AccountSensitiveCreateOpts) -> AccountSensitive: + return super(AccountSensitiveResource, self)._create(data) diff --git a/method/resources/Accounts/Subscriptions.py b/method/resources/Accounts/Subscriptions.py new file mode 100644 index 0000000..d9e4abe --- /dev/null +++ b/method/resources/Accounts/Subscriptions.py @@ -0,0 +1,49 @@ +from typing import TypedDict, Optional, Literal, List + +from method.resource import Resource +from method.configuration import Configuration + + +AccountSubscriptionTypesLiterals = Literal[ + 'transactions', + 'update', + 'update.snapshot' +] + + +class AccountSubscription(TypedDict): + id: str + name: AccountSubscriptionTypesLiterals + status: Literal['active'] + latest_transaction_id: str + created_at: str + updated_at: str + + +AccountSubscriptionsResponse = TypedDict('AccountSubscriptionsResponse', { + 'transactions': Optional[AccountSubscription], + 'update': Optional[AccountSubscription], + 'update.snapshot': Optional[AccountSubscription] +}) + + +class AccountSubscriptionCreateOpts(TypedDict): + enroll: AccountSubscriptionTypesLiterals + + +class AccountSubscriptionsResource(Resource): + def __init__(self, config: Configuration): + super(AccountSubscriptionsResource, self).__init__(config.add_path('subscriptions')) + + def create(self, sub_name: AccountSubscriptionCreateOpts) -> AccountSubscription: + return super(AccountSubscriptionsResource, self)._create({ 'enroll': sub_name }) + + def list(self) -> List[AccountSubscription]: + return super(AccountSubscriptionsResource, self)._get() + + def retrieve(self, sub_id: str) -> AccountSubscriptionsResponse: + return super(AccountSubscriptionsResource, self)._get_with_id(sub_id) + + def delete(self, sub_id: str) -> AccountSubscription: + return super(AccountSubscriptionsResource, self)._delete(sub_id) + \ No newline at end of file diff --git a/method/resources/Accounts/Transactions.py b/method/resources/Accounts/Transactions.py new file mode 100644 index 0000000..544a28f --- /dev/null +++ b/method/resources/Accounts/Transactions.py @@ -0,0 +1,62 @@ +from typing import TypedDict, Optional, Literal, List + +from method.resource import Resource, ResourceListOpts +from method.configuration import Configuration +from method.errors import ResourceError + + +AccountCurrencyTypesLiterals = Literal[ + 'USD' +] + + +AccountTransactionStatusLiterals = Literal[ + 'cleared', + 'auth', + 'refund', + 'unknown' +] + + +class AccountTransactionMerchant(TypedDict): + name: str + category_code: str + city: str + state: str + country: str + acquirer_bin: str + acquirer_card_acceptor_id: str + + +class AccountTransactionNetworkData(TypedDict): + visa_merchant_id: Optional[str] + visa_merchant_name: Optional[str] + visa_store_id: Optional[str] + visa_store_name: Optional[str] + + +class AccountTransaction(TypedDict): + id: str + account_id: str + merchant: AccountTransactionMerchant + network: str + network_data: AccountTransactionNetworkData + amount: int + currency: AccountCurrencyTypesLiterals + billing_amount: int + billing_currency: AccountCurrencyTypesLiterals + status: AccountTransactionStatusLiterals + error: Optional[ResourceError] + created_at: str + updated_at: str + + +class AccountTransactionsResource(Resource): + def __init__(self, config: Configuration): + super(AccountTransactionsResource, self).__init__(config.add_path('transactions')) + + def retrieve(self, txn_id: str) -> AccountTransaction: + return super(AccountTransactionsResource, self)._get_with_id(txn_id) + + def list(self, params: Optional[ResourceListOpts] = None) -> List[AccountTransaction]: + return super(AccountTransactionsResource, self)._list(params) diff --git a/method/resources/Accounts/Types.py b/method/resources/Accounts/Types.py new file mode 100644 index 0000000..868094f --- /dev/null +++ b/method/resources/Accounts/Types.py @@ -0,0 +1,197 @@ +from typing import Literal, Optional, TypedDict + + +AccountTypesLiterals = Literal[ + 'ach', + 'liability' +] + + +AccountStatusesLiterals = Literal[ + 'active', + 'disabled', + 'closed' +] + + +AccountProductTypesLiterals = Literal[ + 'payment', + 'balance', + 'sensitive', + 'card_brand', + 'payoff', + 'update' +] + + +AccountSubscriptionTypesLiterals = Literal[ + 'transactions', + 'update', + 'update.snapshot' +] + + +AccountOwnershipLiterals = Literal[ + 'primary', + 'authorized', + 'joint', + 'unknown' +] + + +AccountUpdateSourceLiterals = Literal[ + 'direct', + 'snapshot' +] + + +AccountLiabilityTypesLiterals = Literal[ + 'auto_loan', + 'credit_card', + 'collection', + 'mortgage', + 'personal_loan', + 'student_loans' +] + + +AchAccountSubTypesLiterals = Literal[ + 'checking', + 'savings' +] + + +AccountExpandableFieldsLiterals = Literal[ + AccountProductTypesLiterals, + 'latest_verification_session' +] + + +AccountInterestRateTypesLiterals = Literal[ + 'fixed', + 'variable' +] + + +AccountInterestRateSourcesLiterals = Literal[ + 'financial_institution', + 'public_data', + 'method' +] + + +AccountLiabilityAutoLoanSubTypesLiterals = Literal[ + 'lease', + 'loan' +] + + +AccountLiabilityCreditCardSubTypesLiterals = Literal[ + 'flexible_spending', + 'charge', + 'secured', + 'unsecured', + 'purchase', + 'business' +] + + +AccountLiabilityCreditCardUsageTypesLiterals = Literal[ + 'transactor', + 'revolver', + 'dormant', + 'unknown' +] + + +AccountLiabilityMortgageSubTypesLiterals = Literal[ + 'loan' +] + + +AccountLiabilityPersonalLoanSubTypesLiterals = Literal[ + 'secured', + 'unsecured', + 'note', + 'line_of_credit', + 'heloc' +] + + +AccountLiabilityStudentLoanSubTypesLiterals = Literal[ + 'federal', + 'private' +] + + +class AccountLiabilityBase(TypedDict): + balance: Optional[int] + closed_at: Optional[str] + last_payment_amount: Optional[int] + last_payment_date: Optional[str] + next_payment_due_date: Optional[str] + next_payment_minimum_amount: Optional[int] + opened_at: Optional[str] + + +class AccountLiabilityLoanBase(AccountLiabilityBase): + expected_payoff_date: Optional[str] + interest_rate_percentage: Optional[float] + interest_rate_source: Optional[AccountInterestRateSourcesLiterals] + interest_rate_type: Optional[AccountInterestRateTypesLiterals] + original_loan_amount: Optional[int] + term_length: Optional[int] + + +class AccountLiabilityAutoLoan(AccountLiabilityLoanBase): + sub_type: Optional[AccountLiabilityAutoLoanSubTypesLiterals] + + +class AccountLiabilityCreditCard(AccountLiabilityBase): + available_credit: Optional[int] + credit_limit: Optional[int] + interest_rate_percentage_max: Optional[float] + interest_rate_percentage_min: Optional[float] + interest_rate_type: Optional[AccountInterestRateTypesLiterals] + sub_type: Optional[AccountLiabilityCreditCardSubTypesLiterals] + usage_pattern: Optional[AccountLiabilityCreditCardUsageTypesLiterals] + + +class AccountLiabilityCollection(AccountLiabilityBase): + pass + + +class AccountLiabilityMortgage(AccountLiabilityLoanBase): + sub_type: Optional[AccountLiabilityMortgageSubTypesLiterals] + + +class AccountLiabilityPersonalLoan(AccountLiabilityLoanBase): + available_credit: Optional[int] + sub_type: Optional[AccountLiabilityPersonalLoanSubTypesLiterals] + + +class AccountLiabilityStudentLoansDisbursement(AccountLiabilityLoanBase): + sequence: int + disbursed_at: Optional[str] + + +class AccountLiabilityStudentLoans(AccountLiabilityBase): + disbursements: Optional[AccountLiabilityStudentLoansDisbursement] + sub_type: Optional[AccountLiabilityStudentLoanSubTypesLiterals] + original_loan_amount: Optional[int] + term_length: Optional[int] + + +class AccountLiability(TypedDict): + mch_id: str + mask: Optional[str] + ownership: Optional[AccountOwnershipLiterals] + fingerprint: Optional[str] + type: Optional[AccountLiabilityTypesLiterals] + name: Optional[str] + + +class AccountACH(TypedDict): + routing: int + number: int + type: AchAccountSubTypesLiterals diff --git a/method/resources/Accounts/Updates.py b/method/resources/Accounts/Updates.py new file mode 100644 index 0000000..76c6d56 --- /dev/null +++ b/method/resources/Accounts/Updates.py @@ -0,0 +1,36 @@ +from typing import TypedDict, Optional, List + +from method.resource import Resource, ResourceListOpts, ResourceStatusLiterals +from method.configuration import Configuration +from method.errors import ResourceError +from method.resources.Accounts.Types import AccountLiabilityTypesLiterals, AccountLiabilityAutoLoan, \ + AccountLiabilityCreditCard, AccountLiabilityMortgage, AccountLiabilityStudentLoans, AccountLiabilityPersonalLoan + + +class AccountUpdate(TypedDict): + id: str + status: ResourceStatusLiterals + account_id: str + type: AccountLiabilityTypesLiterals + auto_loan: Optional[AccountLiabilityAutoLoan] + credit_card: Optional[AccountLiabilityCreditCard] + mortgage: Optional[AccountLiabilityMortgage] + personal_loan: Optional[AccountLiabilityPersonalLoan] + student_loans: Optional[AccountLiabilityStudentLoans] + error: Optional[ResourceError] + created_at: str + updated_at: str + + +class AccountUpdatesResource(Resource): + def __init__(self, config: Configuration): + super(AccountUpdatesResource, self).__init__(config.add_path('updates')) + + def retrieve(self, upt_id: str) -> AccountUpdate: + return super(AccountUpdatesResource, self)._get_with_id(upt_id) + + def list(self, params: Optional[ResourceListOpts] = None) -> List[AccountUpdate]: + return super(AccountUpdatesResource, self)._list(params) + + def create(self) -> AccountUpdate: + return super(AccountUpdatesResource, self)._create({}) diff --git a/method/resources/Accounts/VerificationSessions.py b/method/resources/Accounts/VerificationSessions.py new file mode 100644 index 0000000..559ec80 --- /dev/null +++ b/method/resources/Accounts/VerificationSessions.py @@ -0,0 +1,162 @@ +from typing import TypedDict, Optional, List, Literal, Union + +from method.resource import Resource +from method.configuration import Configuration +from method.errors import ResourceError +from method.resources.Accounts.ExternalTypes import PlaidBalance, PlaidTransaction, MXAccount, MXTransaction, TellerBalance, TellerTransaction + + +AccountVerificationSessionStatusesLiterals = Literal[ + 'pending', + 'in_progress', + 'verified', + 'failed' +] + + +AccountVerificationSessionTypesLiterals = Literal[ + 'micro_deposits', + 'plaid', + 'mx', + 'teller', + 'standard', + 'instant', + 'pre_auth' +] + + +AccountVerificationPassFailLiterals = Literal[ + 'pass', + 'fail' +] + + +class AccountVerificationSessionMicroDeposits(TypedDict): + amounts: List[int] + + +class AccountVerificationSessionPlaid(TypedDict): + balances: PlaidBalance + transactions: List[PlaidTransaction] + + +class AccountVerificationSessionMX(TypedDict): + accounts: MXAccount + transactions: List[MXTransaction] + + +class AccountVerificationSessionTeller(TypedDict): + balances: TellerBalance + transactions: List[TellerTransaction] + + +class AccountVerificationSessionTrustProvisioner(TypedDict): + pass + + +class AccountVerificationSessionAutoVerify(TypedDict): + pass + + +class AccountVerificationSessionThreeDS(TypedDict): + pass + + +class AccountVerificationSessionIssuer(TypedDict): + pass + + +class AccountVerificationSessionStandard(TypedDict): + number: str + + +class AccountVerificationSessionInstant(TypedDict): + exp_year: Optional[str] + exp_month: Optional[str] + exp_check: Optional[AccountVerificationPassFailLiterals] + number: Optional[str] + + +class AccountVerificationSessionPreAuth(AccountVerificationSessionInstant): + cvv: str + cvv_check: Optional[AccountVerificationPassFailLiterals] + billing_zip_code: str + billing_zip_code_check: Optional[AccountVerificationPassFailLiterals] + pre_auth_check: Optional[AccountVerificationPassFailLiterals] + + +class AccountVerificationSessionCreateOpts(TypedDict): + type: AccountVerificationSessionTypesLiterals + + +class AccountVerificationSessionMicroDepositsUpdateOpts(TypedDict): + micro_deposits: AccountVerificationSessionMicroDeposits + + +class AccountVerificationSessionPlaidUpdateOpts(TypedDict): + plaid: AccountVerificationSessionPlaid + + +class AccountVerificationSessionMXUpdateOpts(TypedDict): + mx: AccountVerificationSessionMX + + +class AccountVerificationSessionTellerUpdateOpts(TypedDict): + teller: AccountVerificationSessionTeller + + +class AccountVerificationSessionStandardUpdateOpts(TypedDict): + standard: AccountVerificationSessionStandard + + +class AccountVerificationSessionInstantUpdateOpts(TypedDict): + instant: AccountVerificationSessionInstant + + +class AccountVerificationSessionPreAuthUpdateOpts(TypedDict): + pre_auth: AccountVerificationSessionPreAuth + + +AccountVerificationSessionUpdateOpts = Union[ + AccountVerificationSessionMicroDepositsUpdateOpts, + AccountVerificationSessionPlaidUpdateOpts, + AccountVerificationSessionMXUpdateOpts, + AccountVerificationSessionTellerUpdateOpts, + AccountVerificationSessionStandardUpdateOpts, + AccountVerificationSessionInstantUpdateOpts, + AccountVerificationSessionPreAuthUpdateOpts +] + + +class AccountVerificationSession(TypedDict): + id: str + status: AccountVerificationSessionStatusesLiterals + type: AccountVerificationSessionTypesLiterals + error: Optional[ResourceError] + plaid: Optional[AccountVerificationSessionPlaid] + mx: Optional[AccountVerificationSessionMX] + teller: Optional[AccountVerificationSessionTeller] + micro_deposits: Optional[AccountVerificationSessionMicroDeposits] + trust_provisioner: Optional[AccountVerificationSessionTrustProvisioner] + auto_verify: Optional[AccountVerificationSessionAutoVerify] + standard: Optional[AccountVerificationSessionStandard] + instant: Optional[AccountVerificationSessionInstant] + pre_auth: Optional[AccountVerificationSessionPreAuth] + three_ds: Optional[AccountVerificationSessionThreeDS] + issuer: Optional[AccountVerificationSessionIssuer] + created_at: str + updated_at: str + + +class AccountVerificationSessionResource(Resource): + def __init__(self, config: Configuration): + super(AccountVerificationSessionResource, self).__init__(config.add_path('verification_sessions')) + + def create(self, opts: AccountVerificationSessionCreateOpts) -> AccountVerificationSession: + return super(AccountVerificationSessionResource, self)._create(opts) + + def retrieve(self, avs_id: str) -> AccountVerificationSession: + return super(AccountVerificationSessionResource, self)._get_with_id(avs_id) + + def update(self, avs_id: str, opts: AccountVerificationSessionUpdateOpts) -> AccountVerificationSession: + return super(AccountVerificationSessionResource, self)._update_with_id(avs_id, opts) diff --git a/method/resources/Accounts/__init__.py b/method/resources/Accounts/__init__.py new file mode 100644 index 0000000..b5d6d0f --- /dev/null +++ b/method/resources/Accounts/__init__.py @@ -0,0 +1 @@ +from method.resources.Accounts.Account import Account, AccountResource diff --git a/method/resources/Bin.py b/method/resources/Bin.py deleted file mode 100644 index 9bf7ce3..0000000 --- a/method/resources/Bin.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import TypedDict, Optional, Literal - -from method.resource import Resource -from method.configuration import Configuration - - -BinBrandsLiterals = Literal[ - 'amex', - 'visa', - 'mastercard', - 'discover', - 'diners_club' -] - -BinTypesLiterals = Literal[ - 'debit', - 'credit' -] - - -class Bin(TypedDict): - id: Optional[str] - bin: Optional[str] - brand: BinBrandsLiterals - issuer: Optional[str] - type: Optional[BinTypesLiterals] - category: Optional[str] - bank_url: Optional[str] - sample_pan: Optional[str] - - -class BinResource(Resource): - def __init__(self, config: Configuration): - super(BinResource, self).__init__(config.add_path('bins')) - - def get(self, _id: str) -> Bin: - return super(BinResource, self)._get_with_params({'bin': _id}) diff --git a/method/resources/Element.py b/method/resources/Element.py deleted file mode 100644 index 958a756..0000000 --- a/method/resources/Element.py +++ /dev/null @@ -1,100 +0,0 @@ -from typing import TypedDict, Optional, Literal, List, Dict - -from method.resource import Resource -from method.configuration import Configuration -from method.resources.Account import Account - - -ElementTypesLiterals = Literal[ - 'link' -] - - -UserEventTypeLiterals = Literal[ - 'auth_intro_open', - 'auth_intro_continue', - 'auth_intro_close', - 'auth_name_open', - 'auth_name_continue', - 'auth_name_close', - 'auth_phone_open', - 'auth_phone_continue', - 'auth_phone_close', - 'auth_phone_verify_open', - 'auth_phone_verify_submit', - 'auth_phone_verify_close', - 'auth_dob_open', - 'auth_dob_continue', - 'auth_dob_close', - 'auth_address_open', - 'auth_address_continue', - 'auth_address_close', - 'auth_incorrect_info_open', - 'auth_incorrect_info_try_again', - 'auth_invalid_info_open', - 'auth_invalid_info_exit', - 'auth_secq_open', - 'auth_secq_continue', - 'auth_secq_close', - 'auth_secq_incorrect_open', - 'auth_secq_incorrect_try_again', - 'auth_secq_incorrect_close', - 'auth_consent_open', - 'auth_consent_continue', - 'auth_consent_close', - 'auth_success_open', - 'auth_success_continue', - 'auth_failure_open', - 'auth_failure_continue' -] - - -class LinkElementLinkCreateOpts(TypedDict): - mch_id: Optional[str] - mask: Optional[str] - - -class ElementTokenCreateOpts(TypedDict): - entity_id: str - type: ElementTypesLiterals - team_name: Optional[str] - link: Optional[LinkElementLinkCreateOpts] - - -class Element(TypedDict): - element_token: str - - -class ElementUserEvent(TypedDict): - type: UserEventTypeLiterals - timestamp: str - metadata: Optional[Dict[str, any]] - -class TokenSessionResult(TypedDict): - authenticated: bool - cxn_id: Optional[str] - accounts: List[str] - entity_id: Optional[str] - events: List[ElementUserEvent] - - -class ElementExchangePublicAccountOpts(TypedDict): - public_account_token: Optional[str] - public_account_tokens: Optional[List[str]] - - -class ElementResource(Resource): - def __init__(self, config: Configuration): - super(ElementResource, self).__init__(config.add_path('elements')) - - def create_token(self, opts: ElementTokenCreateOpts) -> Element: - return super(ElementResource, self)._create_with_sub_path('token', opts) - - def get_session_results(self, _id: str) -> TokenSessionResult: - return super(ElementResource, self)._get_with_sub_path('token/{_id}/results'.format(_id=_id)) - - 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/Elements/Element.py b/method/resources/Elements/Element.py new file mode 100644 index 0000000..c4935c0 --- /dev/null +++ b/method/resources/Elements/Element.py @@ -0,0 +1,12 @@ +from method.resource import Resource +from method.configuration import Configuration +from method.resources.Elements.Token import ElementTokenResource + + +class ElementResource(Resource): + token: ElementTokenResource + + def __init__(self, config: Configuration): + _config = config.add_path('elements') + super(ElementResource, self).__init__(_config) + self.token = ElementTokenResource(_config) diff --git a/method/resources/Elements/Token.py b/method/resources/Elements/Token.py new file mode 100644 index 0000000..f1895e9 --- /dev/null +++ b/method/resources/Elements/Token.py @@ -0,0 +1,183 @@ +from typing import TypedDict, Optional, Literal, List, Dict + +from method.resource import Resource +from method.configuration import Configuration +from method.resources.Accounts.Types import AccountLiabilityTypesLiterals + + +UserEventTypeLiterals = Literal[ + 'AUTH_INTRO_OPEN', + 'AUTH_INTRO_CONTINUE', + 'AUTH_INTRO_CLOSE', + + 'AUTH_NAME_OPEN', + 'AUTH_NAME_CONTINUE', + 'AUTH_NAME_CLOSE', + + 'AUTH_PHONE_OPEN', + 'AUTH_PHONE_CONTINUE', + 'AUTH_PHONE_CLOSE', + + 'AUTH_PHONE_VERIFY_OPEN', + 'AUTH_PHONE_VERIFY_SUBMIT', + 'AUTH_PHONE_VERIFY_RESEND_CODE', + 'AUTH_PHONE_VERIFY_CLOSE', + + 'AUTH_DOB_OPEN', + 'AUTH_DOB_CONTINUE', + 'AUTH_DOB_CLOSE', + + 'AUTH_ADDRESS_OPEN', + 'AUTH_ADDRESS_CONTINUE', + 'AUTH_ADDRESS_CLOSE', + + 'AUTH_SSN4_OPEN', + 'AUTH_SSN4_CONTINUE', + 'AUTH_SSN4_CLOSE', + + 'AUTH_SECQ_OPEN', + 'AUTH_SECQ_CONTINUE', + 'AUTH_SECQ_CLOSE', + 'AUTH_SECQ_INCORRECT_TRY_AGAIN', + + 'AUTH_CONSENT_OPEN', + 'AUTH_CONSENT_CONTINUE', + 'AUTH_CONSENT_CLOSE', + + 'AUTH_SUCCESS_OPEN', + 'AUTH_SUCCESS_CONTINUE', + + 'AUTH_FAILURE_OPEN', + 'AUTH_FAILURE_CONTINUE', + + 'AVF_ACCOUNT_LIST_OPEN', + 'AVF_ACCOUNT_LIST_CLOSE', + + 'AVF_LEARN_MORE_OPEN', + 'AVF_LEARN_MORE_CLOSE', + + 'AVF_ACCOUNT_VERIFY_OPEN', + 'AVF_ACCOUNT_VERIFY_SUBMIT', + 'AVF_ACCOUNT_VERIFY_CLOSE', + + 'AVF_SUCCESS_OPEN', + 'AVF_SUCCESS_CONTINUE', + + 'AVF_EMPTY_SUCCESS_OPEN', + 'AVF_EMPTY_SUCCESS_CONTINUE', + + 'AVF_SKIP_ALL', + 'AVF_ERROR', +] + + +ElementTypesLiterals = Literal[ + 'connect', + 'balance_transfer' +] + + +ElementProducts = Literal[ + 'balance', + 'payoff', + 'transactions', + 'card_brand', + 'update', + 'sensitive', + 'payment' +] + + +ElementMetadataOpTypes = Literal[ + 'open', + 'continue', + 'close', + 'success', + 'exit' +] + + +ElementSelectionTypes = Literal[ + 'single', + 'multiple' +] + + +class ElementUserEvent(TypedDict): + type: UserEventTypeLiterals + timestamp: str + metadata: Optional[Dict[str, any]] + + +class IndividualOpts(TypedDict): + first_name: Optional[str] + last_name: Optional[str] + dob: Optional[str] + email: Optional[str] + phone: Optional[str] + phone_verification_type: Optional[str] + phone_verification_timestamp: Optional[str] + + +class ElementEntityOpts(TypedDict): + type: Literal['individual'] + individual: IndividualOpts + + +class ConnectElementFilterOpts(TypedDict): + selection_type: Optional[ElementSelectionTypes] + liability_types: Optional[List[AccountLiabilityTypesLiterals]] + + +class ConnectElementCreateOpts(TypedDict): + products: Optional[List[ElementProducts]] + accounts: Optional[List[str]] + entity: Optional[ElementEntityOpts] + account_filters: Optional[ConnectElementFilterOpts] + + +class BalanceTransferElementCreateOpts(TypedDict): + payment_mount_min: int + minimum_loan_amount: int + payout_residual_amount_max: int + loan_details_requested_amount: int + loan_details_requested_rate: int + loan_details_requested_term: int + loan_details_requested_monthly_payment: int + + +class ElementTokenCreateOpts(TypedDict): + entity_id: str + type: ElementTypesLiterals + team_name: Optional[str] + team_logo: Optional[str] + team_icon: Optional[str] + connect: Optional[ConnectElementCreateOpts] + balance_transfer: Optional[BalanceTransferElementCreateOpts] + + +class ElementToken(TypedDict): + element_token: str + + +class ElementMetadata(TypedDict): + element_type: ElementTypesLiterals + op: ElementMetadataOpTypes + +class ElementResults(TypedDict): + authenticated: bool + cxn_id: Optional[str] + accounts: List[str] + entity_id: Optional[str] + events: List[ElementUserEvent] + + +class ElementTokenResource(Resource): + def __init__(self, config: Configuration): + super(ElementTokenResource, self).__init__(config.add_path('token')) + + def create(self, opts: ElementTokenCreateOpts) -> ElementToken: + return super(ElementTokenResource, self)._create(opts) + + def results(self, pk_elem_id: str) -> ElementResults: + return super(ElementTokenResource, self)._get_with_sub_path('{_id}/results'.format(_id=pk_elem_id)) diff --git a/method/resources/Elements/__init__.py b/method/resources/Elements/__init__.py new file mode 100644 index 0000000..1113597 --- /dev/null +++ b/method/resources/Elements/__init__.py @@ -0,0 +1,2 @@ +from method.resources.Elements.Element import ElementResource +from method.resources.Elements.Token import ElementTokenResource, ElementToken diff --git a/method/resources/Entities/Connect.py b/method/resources/Entities/Connect.py new file mode 100644 index 0000000..dfa30e7 --- /dev/null +++ b/method/resources/Entities/Connect.py @@ -0,0 +1,33 @@ +from typing import TypedDict, Optional, Literal, List + +from method.resource import Resource +from method.configuration import Configuration +from method.errors import ResourceError + + +EntityConnectResponseStatusLiterals = Literal[ + 'completed', + 'in_progress', + 'pending', + 'failed' +] + + +class EntityConnect(TypedDict): + id: str + status: EntityConnectResponseStatusLiterals + accounts: Optional[List[str]] + error: Optional[ResourceError] + created_at: str + updated_at: str + + +class EntityConnectResource(Resource): + def __init__(self, config: Configuration): + super(EntityConnectResource, self).__init__(config.add_path('connect')) + + def retrieve(self, cxn_id: str) -> EntityConnect: + return super(EntityConnectResource, self)._get_with_id(cxn_id) + + def create(self) -> EntityConnect: + return super(EntityConnectResource, self)._create({}) diff --git a/method/resources/Entities/CreditScores.py b/method/resources/Entities/CreditScores.py new file mode 100644 index 0000000..7e5dfbb --- /dev/null +++ b/method/resources/Entities/CreditScores.py @@ -0,0 +1,47 @@ +from typing import TypedDict, Optional, List, Literal + +from method.resource import Resource +from method.configuration import Configuration +from method.errors import ResourceError +from method.resources.Entities.Types import EntityStatusesLiterals, CreditReportBureausLiterals + + +CreditScoresModelLiterals = Literal[ + 'vantage_4', + 'vantage_3' +] + + +class EntityCreditScoresFactorsType(TypedDict): + code: str + description: str + + +class EntityCreditScoresType(TypedDict): + score: int + source: CreditReportBureausLiterals + model: CreditScoresModelLiterals + factors: List[EntityCreditScoresFactorsType] + created_at: str + factors: EntityCreditScoresFactorsType + created_at: str + + +class EntityCreditScores(TypedDict): + id: str + status: EntityStatusesLiterals + credit_scores: Optional[List[EntityCreditScoresType]] + error: Optional[ResourceError] + created_at: str + updated_at: str + + +class EntityCreditScoresResource(Resource): + def __init__(self, config: Configuration): + super(EntityCreditScoresResource, self).__init__(config.add_path('credit_scores')) + + def retrieve(self, crs_id: str) -> EntityCreditScores: + return super(EntityCreditScoresResource, self)._get_with_id(crs_id) + + def create(self) -> EntityCreditScores: + return super(EntityCreditScoresResource, self)._create({}) diff --git a/method/resources/Entities/Entity.py b/method/resources/Entities/Entity.py new file mode 100644 index 0000000..051f9db --- /dev/null +++ b/method/resources/Entities/Entity.py @@ -0,0 +1,163 @@ +from typing import TypedDict, Optional, List, Dict, Any, Literal + +from method.resource import Resource, RequestOpts, ResourceListOpts +from method.configuration import Configuration +from method.errors import ResourceError +from method.resources.Entities.Types import EntityTypesLiterals, EntityCapabilitiesLiterals, EntityStatusesLiterals, \ + CreditReportBureausLiterals, EntityIndividual, EntityCorporation, EntityAddress +from method.resources.Entities.Connect import EntityConnectResource +from method.resources.Entities.CreditScores import EntityCreditScoresResource +from method.resources.Entities.Identities import EntityIdentityResource +from method.resources.Entities.Products import EntityProductResource +from method.resources.Entities.Sensitive import EntitySensitiveResource +from method.resources.Entities.Subscriptions import EntitySubscriptionsResource +from method.resources.Entities.VerificationSessions import EntityVerificationSessionResource + + +class EntityCreateOpts(TypedDict): + type: EntityTypesLiterals + individual: Optional[EntityIndividual] + corporation: Optional[EntityCorporation] + address: Optional[EntityAddress] + metadata: Optional[Dict[str, Any]] + + +class EntityUpdateOpts(TypedDict): + individual: Optional[EntityIndividual] + corporation: Optional[EntityCorporation] + address: Optional[EntityAddress] + + +class EntityListOpts(ResourceListOpts): + status: Optional[str] + type: Optional[str] + + +class EntityAnswer(TypedDict): + id: str + text: str + + +class EntityQuestion(TypedDict): + id: str + text: Optional[str] + answers: List[EntityAnswer] + + +class EntityQuestionResponse(TypedDict): + questions: List[EntityQuestion] + authenticated: bool + cxn_id: List[str] + accounts: List[str] + + +class EntityCreditScoresFactorsType(TypedDict): + code: str + description: str + + +class AnswerOpts(TypedDict): + question_id: str + answer_id: str + + +class EntityUpdateAuthOpts(TypedDict): + answers: List[AnswerOpts] + + +class EntityUpdateAuthResponse(TypedDict): + questions: List[EntityQuestion] + authenticated: bool + cxn_id: Optional[str] + accounts: List[str] + + +class EntityManualAuthOpts(TypedDict): + format: str + bureau: CreditReportBureausLiterals + raw_report: Dict[str, Any] + + +class EntityManualAuthResponse(TypedDict): + authenticated: bool + accounts: List[str] + + +class EntityGetCreditScoreResponse(TypedDict): + score: int + updated_at: str + + +class AnswerOpts(TypedDict): + question_id: str + answer_id: str + + +class EntityUpdateAuthOpts(TypedDict): + answers: List[AnswerOpts] + + +class EntityUpdateAuthResponse(TypedDict): + questions: List[EntityQuestion] + cxn_id: Optional[str] + + +class Entity(TypedDict): + id: str + type: EntityTypesLiterals + individual: Optional[EntityIndividual] + corporation: Optional[EntityCorporation] + capabilities: List[EntityCapabilitiesLiterals] + available_capabilities: List[EntityCapabilitiesLiterals] + pending_capabilities: List[EntityCapabilitiesLiterals] + address: EntityAddress + status: EntityStatusesLiterals + error: Optional[ResourceError] + metadata: Optional[Dict[str, Any]] + created_at: str + updated_at: str + + +class EntitySubResources: + connect: EntityConnectResource + credit_scores: EntityCreditScoresResource + identities: EntityIdentityResource + products: EntityProductResource + sensitive: EntitySensitiveResource + subscriptions: EntitySubscriptionsResource + verification_sessions: EntityVerificationSessionResource + + def __init__(self, _id: str, config: Configuration): + self.connect = EntityConnectResource(config.add_path(_id)) + self.credit_scores = EntityCreditScoresResource(config.add_path(_id)) + self.identities = EntityIdentityResource(config.add_path(_id)) + self.products = EntityProductResource(config.add_path(_id)) + self.sensitive = EntitySensitiveResource(config.add_path(_id)) + self.subscriptions = EntitySubscriptionsResource(config.add_path(_id)) + self.verification_sessions = EntityVerificationSessionResource(config.add_path(_id)) + + +class EntityResource(Resource): + def __init__(self, config: Configuration): + super(EntityResource, self).__init__(config.add_path('entities')) + + def __call__(self, _id: str) -> EntitySubResources: + return EntitySubResources(_id, self.config) + + def create(self, opts: EntityCreateOpts, request_opts: Optional[RequestOpts] = None) -> Entity: + return super(EntityResource, self)._create(opts, request_opts) + + def update(self, _id: str, opts: EntityCreateOpts) -> Entity: + return super(EntityResource, self)._update_with_id(_id, opts) + + def retrieve(self, _id: str) -> Entity: + return super(EntityResource, self)._get_with_id(_id) + + def list(self, params: EntityListOpts = None) -> List[Entity]: + return super(EntityResource, self)._list(params) + + def withdraw_consent(self, _id: str) -> Entity: + return super(EntityResource, self)._create_with_sub_path( + '{_id}/consent'.format(_id=_id), + {'type': 'withdraw', 'reason': 'entity_withdrew_consent'} + ) diff --git a/method/resources/Entities/Identities.py b/method/resources/Entities/Identities.py new file mode 100644 index 0000000..5b9d511 --- /dev/null +++ b/method/resources/Entities/Identities.py @@ -0,0 +1,34 @@ +from typing import TypedDict, Optional, Literal, List + +from method.resource import Resource +from method.configuration import Configuration +from method.errors import ResourceError +from method.resources.Entities.Types import EntityIdentity + + +EntityVerificationSessionStatusLiterals = Literal[ + 'completed', + 'in_progress', + 'pending', + 'failed' +] + + +class EntityIdentity(TypedDict): + id: str + status: EntityVerificationSessionStatusLiterals + identities: List[EntityIdentity] + error: Optional[ResourceError] + created_at: str + updated_at: str + + +class EntityIdentityResource(Resource): + def __init__(self, config: Configuration): + super(EntityIdentityResource, self).__init__(config.add_path('identities')) + + def retrieve(self, identity_id: str) -> EntityIdentity: + return super(EntityIdentityResource, self)._get_with_id(identity_id) + + def create(self, opts: Optional[EntityIdentity] = {}) -> EntityIdentity: + return super(EntityIdentityResource, self)._create(opts) diff --git a/method/resources/Entities/Products.py b/method/resources/Entities/Products.py new file mode 100644 index 0000000..b043eae --- /dev/null +++ b/method/resources/Entities/Products.py @@ -0,0 +1,40 @@ +from typing import TypedDict, Optional, Literal + +from method.resource import Resource +from method.configuration import Configuration +from method.errors import ResourceError + + +EntityProductTypeStatusLiterals = Literal[ + 'unavailable', + 'available', + 'restricted' +] + + +class EntityProduct(TypedDict): + id: str + name: str + status: EntityProductTypeStatusLiterals + status_error: Optional[ResourceError] + latest_request_id: str + is_subscribable: bool + created_at: str + updated_at: str + + +class EntityProductListResponse(TypedDict): + connect: Optional[EntityProduct] + credit_score: Optional[EntityProduct] + identity: Optional[EntityProduct] + + +class EntityProductResource(Resource): + def __init__(self, config: Configuration): + super(EntityProductResource, self).__init__(config.add_path('products')) + + def retrieve(self, prd_id: str) -> EntityProduct: + return super(EntityProductResource, self)._get_with_id(prd_id) + + def list(self) -> EntityProductListResponse: + return super(EntityProductResource, self)._list() diff --git a/method/resources/Entities/Sensitive.py b/method/resources/Entities/Sensitive.py new file mode 100644 index 0000000..45c2cdd --- /dev/null +++ b/method/resources/Entities/Sensitive.py @@ -0,0 +1,29 @@ +from typing import TypedDict, Optional, List + +from method.resource import Resource +from method.configuration import Configuration + +from method.resources.Entities.Types import EntityKYCAddressRecordData, EntityIdentity + + +class EntitySensitive(TypedDict): + first_name: Optional[str] + last_name: Optional[str] + phone: Optional[str] + phone_history: List[str] + email: Optional[str] + dob: Optional[str] + address: Optional[EntityKYCAddressRecordData] + address_history: List[EntityKYCAddressRecordData] + ssn_4: Optional[str] + ssn_6: Optional[str] + ssn_9: Optional[str] + identities: List[EntityIdentity] + + +class EntitySensitiveResource(Resource): + def __init__(self, config: Configuration): + super(EntitySensitiveResource, self).__init__(config.add_path('sensitive')) + + def retrieve(self) -> EntitySensitive: + return super(EntitySensitive, self)._get() diff --git a/method/resources/Entities/Subscriptions.py b/method/resources/Entities/Subscriptions.py new file mode 100644 index 0000000..d71d541 --- /dev/null +++ b/method/resources/Entities/Subscriptions.py @@ -0,0 +1,53 @@ +from typing import TypedDict, Optional, List, Literal + +from method.resource import Resource +from method.configuration import Configuration +from method.errors import ResourceError + + +EntitySubscriptionNamesLiterals = Literal[ + 'connect', + 'credit_score' +] + + +EntitySubscriptionStatusesLiterals = Literal[ + 'active', + 'inactive' +] + + +class EntitySubscription(TypedDict): + id: str + name: EntitySubscriptionNamesLiterals + status: EntitySubscriptionStatusesLiterals + last_request_id: Optional[str] + created_at: str + updated_at: str + + +class EntitySubscriptionResponseOpts(TypedDict): + subscription: Optional[EntitySubscription] + error: Optional[ResourceError] + + +class EntitySubscriptionListResponse(TypedDict): + connect: Optional[EntitySubscriptionResponseOpts] + credit_score: Optional[EntitySubscriptionResponseOpts] + + +class EntitySubscriptionsResource(Resource): + def __init__(self, config: Configuration): + super(EntitySubscriptionsResource, self).__init__(config.add_path('subscriptions')) + + def retrieve(self, sub_id: str) -> EntitySubscriptionResponseOpts: + return super(EntitySubscriptionsResource, self)._get_with_id(sub_id) + + def list(self) -> EntitySubscriptionListResponse: + return super(EntitySubscriptionsResource, self)._list() + + def create(self, sub_name: EntitySubscriptionNamesLiterals) -> EntitySubscriptionResponseOpts: + return super(EntitySubscriptionsResource, self)._create({ 'enroll': sub_name }) + + def delete(self, sub_id: str) -> EntitySubscriptionResponseOpts: + return super(EntitySubscriptionsResource, self)._delete(sub_id) diff --git a/method/resources/Entities/Types.py b/method/resources/Entities/Types.py new file mode 100644 index 0000000..19e4424 --- /dev/null +++ b/method/resources/Entities/Types.py @@ -0,0 +1,129 @@ +from typing import TypedDict, Optional, Literal, List + + +EntityTypesLiterals = Literal[ + 'individual', + 'c_corporation', + 's_corporation', + 'llc', + 'partnership', + 'sole_proprietorship', + 'receive_only' +] + + +EntityCapabilitiesLiterals = Literal[ + 'payments:send', + 'payments:receive', + 'payments:limited-send', + 'data:retrieve' +] + + +EntityStatusesLiterals = Literal[ + 'active', + 'incomplete', + 'disabled' +] + + +CreditScoreStatusesLiterals = Literal[ + 'completed', + 'in_progress', + 'pending', + 'failed' +] + + +EntityIndividualPhoneVerificationTypesLiterals = Literal[ + 'method_sms', + 'method_verified', + 'sms', + 'tos' +] + + +CreditScoreStatusesLiterals = Literal[ + 'completed', + 'in_progress', + 'pending', + 'failed' +] + + +CreditReportBureausLiterals = Literal[ + 'experian', + 'equifax', + 'transunion' +] + + +EntitySensitiveFieldsLiterals = Literal[ + 'first_name', + 'last_name', + 'phone', + 'phone_history', + 'email', + 'dob', + 'address', + 'address_history', + 'ssn_4', + 'ssn_6', + 'ssn_9', + 'identities' +] + + +class EntityIndividual(TypedDict): + first_name: Optional[str] + last_name: Optional[str] + phone: Optional[str] + email: Optional[str] + dob: Optional[str] + + +class EntityAddress(TypedDict): + line1: Optional[str] + line2: Optional[str] + city: Optional[str] + state: Optional[str] + zip: Optional[str] + + +class EntityCorporationOwner(TypedDict): + first_name: Optional[str] + last_name: Optional[str] + phone: Optional[str] + email: Optional[str] + dob: Optional[str] + address: EntityAddress + + +class EntityCorporation(TypedDict): + name: Optional[str] + dba: Optional[str] + ein: Optional[str] + owners: List[EntityCorporationOwner] + + +class EntityReceiveOnly(TypedDict): + name: str + phone: Optional[str] + email: Optional[str] + + +class EntityKYCAddressRecordData(TypedDict): + address: str + city: str + postal_code: str + state: str + address_term: int + + +class EntityIdentity(TypedDict): + first_name: Optional[str] + last_name: Optional[str] + phone: Optional[str] + dob: Optional[str] + address: Optional[EntityKYCAddressRecordData] + ssn: Optional[str] diff --git a/method/resources/Entities/VerificationSessions.py b/method/resources/Entities/VerificationSessions.py new file mode 100644 index 0000000..9226652 --- /dev/null +++ b/method/resources/Entities/VerificationSessions.py @@ -0,0 +1,116 @@ +from typing import TypedDict, Optional, List, Literal + +from method.resource import Resource +from method.configuration import Configuration +from method.errors import ResourceError + + +EntityVerificationSessionStatusLiterals = Literal[ + 'verified', + 'in_progress', + 'pending', + 'failed' +] + + +EntityVerificationSessionMethodsLiterals = Literal[ + 'sms', + 'sna', + 'byo_sms', + 'byo_kyc', + 'kba', + 'element', + 'method_verified' +] + + +EntityVerificationSessionTypeLiterals = Literal[ + 'phone', + 'identity' +] + + +class EntityPhoneSmsVerification(TypedDict): + timestamp: str + + +class EntityPhoneSmsVerificationUpdate(TypedDict): + sms_code: str + + +class EntityPhoneSnaVerification(TypedDict): + urls: str + + +class EntityByoKycVerification(TypedDict): + authenticated: bool + + +class EntityKbaVerificationAnswer(TypedDict): + id: str + text: str + + +class EntityKbaVerificationAnswerUpdate(TypedDict): + question_id: str + answer_id: str + + +class EntityKbaVerificationQuestion(TypedDict): + selected_answer: Optional[str] + id: str + text: str + answers: List[EntityKbaVerificationAnswer] + + +class EntityKbaVerification(TypedDict): + questions: List[EntityKbaVerificationQuestion] + authenticated: bool + + +class EntityVerificationSessionCreateOpts(TypedDict): + type: EntityVerificationSessionTypeLiterals + method: EntityVerificationSessionMethodsLiterals + sms: Optional[object] + sna: Optional[object] + byo_sms: Optional[EntityPhoneSmsVerification] + byo_kyc: Optional[object] + kba: Optional[object] + + +class EntityVerificationSessionUpdateOpts(TypedDict): + type: EntityVerificationSessionTypeLiterals + method: EntityVerificationSessionMethodsLiterals + sms: Optional[EntityPhoneSmsVerificationUpdate] + sma: Optional[object] + kba: Optional[EntityKbaVerificationAnswerUpdate] + + +class EntityVerificationSession(TypedDict): + id: str + entity_id: str + status: EntityVerificationSessionStatusLiterals + type: EntityVerificationSessionTypeLiterals + method: EntityVerificationSessionMethodsLiterals + sms: Optional[EntityPhoneSmsVerification] + sna: Optional[EntityPhoneSnaVerification] + byo_sms: Optional[EntityPhoneSmsVerification] + byo_kyc: Optional[EntityByoKycVerification] + kba: Optional[EntityKbaVerification] + error: Optional[ResourceError] + created_at: str + updated_at: str + + +class EntityVerificationSessionResource(Resource): + def __init__(self, config: Configuration): + super(EntityVerificationSessionResource, self).__init__(config.add_path('verification_sessions')) + + def retrieve(self, verification_session_id: str) -> EntityVerificationSession: + return super(EntityVerificationSessionResource, self)._get_with_id(verification_session_id) + + def create(self, opts: Optional[EntityVerificationSessionCreateOpts] = {}) -> EntityVerificationSession: + return super(EntityVerificationSessionResource, self)._create(opts) + + def update(self, verification_session_id: str, opts: EntityVerificationSessionUpdateOpts) -> EntityVerificationSession: + return super(EntityVerificationSessionResource, self)._update_with_id(verification_session_id, opts) diff --git a/method/resources/Entities/__init__.py b/method/resources/Entities/__init__.py new file mode 100644 index 0000000..8ff4a61 --- /dev/null +++ b/method/resources/Entities/__init__.py @@ -0,0 +1 @@ +from method.resources.Entities.Entity import Entity, EntityResource diff --git a/method/resources/Entity.py b/method/resources/Entity.py deleted file mode 100644 index b55037c..0000000 --- a/method/resources/Entity.py +++ /dev/null @@ -1,361 +0,0 @@ -from typing import TypedDict, Optional, List, Dict, Any, Literal - -from method.resource import Resource, RequestOpts -from method.configuration import Configuration -from method.errors import ResourceError - - -EntityTypesLiterals = Literal[ - 'individual', - 'c_corporation', - 's_corporation', - 'llc', - 'partnership', - 'sole_proprietorship', - 'receive_only' -] - - -EntityCapabilitiesLiterals = Literal[ - 'payments:send', - 'payments:receive', - 'payments:limited-send', - 'data:retrieve' -] - - -EntityStatusesLiterals = Literal[ - 'active', - 'incomplete', - 'disabled' -] - - -CreditScoreStatusesLiterals = Literal[ - 'completed', - 'in_progress', - 'pending', - 'failed' -] - - -EntityIndividualPhoneVerificationTypesLiterals = Literal[ - 'method_sms', - 'method_verified', - 'sms', - 'tos' -] - - -CreditScoreStatusesLiterals = Literal[ - 'completed', - 'in_progress', - 'pending', - 'failed' -] - - -CreditScoresModelLiterals = Literal[ - 'vantage_4', - 'vantage_3' -] - - -CreditReportBureausLiterals = Literal[ - 'experian', - 'equifax', - 'transunion' -] - -EntitySensitiveFieldsLiterals = Literal[ - 'first_name', - 'last_name', - 'phone', - 'phone_history', - 'email', - 'dob', - 'address', - 'address_history', - 'ssn_4', - 'ssn_6', - 'ssn_9', - 'identities' -] - - -class EntityIndividual(TypedDict): - first_name: Optional[str] - last_name: Optional[str] - phone: Optional[str] - email: Optional[str] - dob: Optional[str] - - -class EntityAddress(TypedDict): - line1: Optional[str] - line2: Optional[str] - city: Optional[str] - state: Optional[str] - zip: Optional[str] - - -class EntityCorporationOwner(TypedDict): - first_name: Optional[str] - last_name: Optional[str] - phone: Optional[str] - email: Optional[str] - dob: Optional[str] - address: EntityAddress - - -class EntityCorporation(TypedDict): - name: Optional[str] - dba: Optional[str] - ein: Optional[str] - owners: List[EntityCorporationOwner] - - -class EntityReceiveOnly(TypedDict): - name: str - phone: Optional[str] - email: Optional[str] - - -class Entity(TypedDict): - id: str - type: EntityTypesLiterals - individual: Optional[EntityIndividual] - corporation: Optional[EntityCorporation] - receive_only: Optional[EntityReceiveOnly] - capabilities: List[EntityCapabilitiesLiterals] - available_capabilities: List[EntityCapabilitiesLiterals] - pending_capabilities: List[EntityCapabilitiesLiterals] - address: EntityAddress - status: EntityStatusesLiterals - error: Optional[ResourceError] - metadata: Optional[Dict[str, Any]] - created_at: str - updated_at: str - - -class EntityCreateOpts(TypedDict): - type: EntityTypesLiterals - individual: Optional[EntityIndividual] - corporation: Optional[EntityCorporation] - receive_only: Optional[EntityReceiveOnly] - address: Optional[EntityAddress] - metadata: Optional[Dict[str, Any]] - - -class EntityUpdateOpts(TypedDict): - individual: Optional[EntityIndividual] - corporation: Optional[EntityCorporation] - address: Optional[EntityAddress] - - -class EntityListOpts(TypedDict): - to_date: Optional[str] - from_date: Optional[str] - page: Optional[int] - page_limit: Optional[int] - page_cursor: Optional[str] - status: Optional[str] - type: Optional[str] - - -class EntityAnswer(TypedDict): - id: str - text: str - - -class EntityQuestion(TypedDict): - id: str - text: Optional[str] - answers: List[EntityAnswer] - - -class EntityQuestionResponse(TypedDict): - questions: List[EntityQuestion] - authenticated: bool - cxn_id: List[str] - accounts: List[str] - - -class EntityCreditScoresFactorsType(TypedDict): - code: str - description: str - - -class EntityCreditScoresType(TypedDict): - score: int - source: CreditReportBureausLiterals - model: CreditScoresModelLiterals - factors: List[EntityCreditScoresFactorsType] - created_at: str - factors: EntityCreditScoresFactorsType - created_at: str - - -class EntityCreditScoresResponse(TypedDict): - id: str - status: EntityStatusesLiterals - scores: Optional[List[EntityCreditScoresType]] - error: Optional[ResourceError] - created_at: str - updated_at: str - - -class EntityCreditScoresFactorsType(TypedDict): - code: str - description: str - - -class EntityCreditScoresType(TypedDict): - score: int - source: CreditReportBureausLiterals - model: str - factors: List[EntityCreditScoresFactorsType] - created_at: str - - -class EntityCreditScoresResponse(TypedDict): - id: str - status: EntityStatusesLiterals - credit_scores: Optional[List[EntityCreditScoresType]] - error: Optional[ResourceError] - created_at: str - updated_at: str - - -class AnswerOpts(TypedDict): - question_id: str - answer_id: str - - -class EntityUpdateAuthOpts(TypedDict): - answers: List[AnswerOpts] - - -class EntityUpdateAuthResponse(TypedDict): - questions: List[EntityQuestion] - authenticated: bool - cxn_id: Optional[str] - accounts: List[str] - - -class EntityManualAuthOpts(TypedDict): - format: str - bureau: CreditReportBureausLiterals - raw_report: Dict[str, Any] - - -class EntityManualAuthResponse(TypedDict): - authenticated: bool - accounts: List[str] - - -class EntityGetCreditScoreResponse(TypedDict): - score: int - updated_at: str - - -class AnswerOpts(TypedDict): - question_id: str - answer_id: str - - -class EntityUpdateAuthOpts(TypedDict): - answers: List[AnswerOpts] - - -class EntityUpdateAuthResponse(TypedDict): - questions: List[EntityQuestion] - cxn_id: Optional[str] - - -class EntityKYCAddressRecordData(TypedDict): - address: str - city: str - postal_code: str - state: str - address_term: int - - -class EntityIdentity(TypedDict): - first_name: Optional[str] - last_name: Optional[str] - phone: Optional[str] - dob: Optional[str] - address: Optional[EntityKYCAddressRecordData] - ssn: Optional[str] - - -class EntitySensitiveResponse(TypedDict): - first_name: Optional[str] - last_name: Optional[str] - phone: Optional[str] - phone_history: Optional[List[str]] - email: Optional[str] - dob: Optional[str] - address: Optional[EntityKYCAddressRecordData] - address_history: List[EntityKYCAddressRecordData] - ssn_4: Optional[str] - ssn_6: Optional[str] - ssn_9: Optional[str] - identities: List[EntityIdentity] - - -class EntityResource(Resource): - def __init__(self, config: Configuration): - super(EntityResource, self).__init__(config.add_path('entities')) - - def create(self, opts: EntityCreateOpts, request_opts: Optional[RequestOpts] = None) -> Entity: - return super(EntityResource, self)._create(opts, request_opts) - - def update(self, _id: str, opts: EntityCreateOpts) -> Entity: - return super(EntityResource, self)._update_with_id(_id, opts) - - def get(self, _id: str) -> Entity: - return super(EntityResource, self)._get_with_id(_id) - - def list(self, params: EntityListOpts = None) -> List[Entity]: - return super(EntityResource, self)._list(params) - - def create_auth_session(self, _id: str) -> EntityQuestionResponse: - return super(EntityResource, self)._create_with_sub_path('{_id}/auth_session'.format(_id=_id), {}) - - def get_credit_score(self, _id: str) -> EntityQuestionResponse: - return super(EntityResource, self)._get_with_sub_path('{_id}/credit_score'.format(_id=_id)) - - def get_credit_scores(self, _id: str, crs_id: str) -> EntityCreditScoresResponse: - return super(EntityResource, self)._get_with_sub_path('{_id}/credit_scores/{crs_id}'.format(_id=_id, crs_id=crs_id)) - - def create_credit_scores(self, _id: str) -> EntityCreditScoresResponse: - return super(EntityResource, self)._create_with_sub_path('{_id}/credit_scores'.format(_id=_id), {}) - - def update_auth_session(self, _id: str, opts: EntityUpdateAuthOpts) -> EntityUpdateAuthResponse: - return super(EntityResource, self)._update_with_sub_path('{_id}/auth_session'.format(_id=_id), opts) - - def create_manual_auth_session(self, _id: str, opts: EntityManualAuthOpts) -> EntityManualAuthResponse: - return super(EntityResource, self)._create_with_sub_path('{_id}/manual_auth_session'.format(_id=_id), opts) - - def update_manual_auth_session(self, _id: str, opts: EntityManualAuthOpts) -> EntityManualAuthResponse: - return super(EntityResource, self)._update_with_sub_path('{_id}/manual_auth_session'.format(_id=_id), opts) - - def refresh_capabilities(self, _id: str) -> Entity: - return super(EntityResource, self)._create_with_sub_path('{_id}/refresh_capabilities'.format(_id=_id), {}) - - def get_credit_score(self, _id: str) -> EntityQuestionResponse: - return super(EntityResource, self)._get_with_sub_path('{_id}/credit_score'.format(_id=_id)) - - def get_sensitive_fields(self, _id: str, fields: List[EntitySensitiveFieldsLiterals]) -> EntitySensitiveResponse: - return super(EntityResource, self)._get_with_sub_path_and_params( - '{_id}/sensitive'.format(_id=_id), - {'fields[]': fields}, - ) - - def withdraw_consent(self, _id: str) -> Entity: - return super(EntityResource, self)._create_with_sub_path( - '{_id}/consent'.format(_id=_id), - {'type': 'withdraw', 'reason': 'entity_withdrew_consent'} - ) diff --git a/method/resources/HealthCheck.py b/method/resources/HealthCheck.py index 1388410..d670cd0 100644 --- a/method/resources/HealthCheck.py +++ b/method/resources/HealthCheck.py @@ -14,5 +14,5 @@ class HealthCheckResource(Resource): def __init__(self, config: Configuration): super(HealthCheckResource, self).__init__(config.add_path('ping')) - def get(self) -> PingResponse: + def retrieve(self) -> PingResponse: return super(HealthCheckResource, self)._get_raw() diff --git a/method/resources/Merchant.py b/method/resources/Merchant.py index 2fbea0e..28982de 100644 --- a/method/resources/Merchant.py +++ b/method/resources/Merchant.py @@ -49,6 +49,9 @@ class Merchant(TypedDict): is_temp: bool MerchantListOpts = TypedDict('MerchantListOpts', { + 'page': Optional[str | int], + 'page_limit': Optional[str | int], + 'type': Optional[MerchantTypesLiterals], 'name': Optional[str], 'provider_id.plaid': Optional[str], 'provider_id.mx': Optional[str], @@ -60,7 +63,7 @@ class MerchantResource(Resource): def __init__(self, config: Configuration): super(MerchantResource, self).__init__(config.add_path('merchants')) - def get(self, _id: str) -> Merchant: + def retrieve(self, _id: str) -> Merchant: return super(MerchantResource, self)._get_with_id(_id) def list(self, opts: Optional[MerchantListOpts] = None) -> List[Merchant]: diff --git a/method/resources/Payment.py b/method/resources/Payments/Payment.py similarity index 89% rename from method/resources/Payment.py rename to method/resources/Payments/Payment.py index 404735a..da28630 100644 --- a/method/resources/Payment.py +++ b/method/resources/Payments/Payment.py @@ -1,9 +1,9 @@ from typing import TypedDict, Optional, List, Dict, Any, Literal -from method.resource import Resource, RequestOpts +from method.resource import Resource, RequestOpts, ResourceListOpts from method.configuration import Configuration from method.errors import ResourceError -from method.resources.Reversal import ReversalResource +from method.resources.Payments.Reversal import ReversalResource PaymentStatusesLiterals = Literal[ @@ -80,12 +80,7 @@ class PaymentCreateOpts(TypedDict): fee: Optional[PaymentFee] -class PaymentListOpts(TypedDict): - to_date: Optional[str] - from_date: Optional[str] - page: Optional[int] - page_limit: Optional[int] - page_cursor: Optional[str] +class PaymentListOpts(ResourceListOpts): status: Optional[str] type: Optional[str] source: Optional[str] @@ -111,7 +106,7 @@ def __init__(self, config: Configuration): def __call__(self, _id: str) -> PaymentSubResources: return PaymentSubResources(_id, self.config) - def get(self, _id: str) -> Payment: + def retrieve(self, _id: str) -> Payment: return super(PaymentResource, self)._get_with_id(_id) def list(self, params: Optional[PaymentListOpts] = None) -> List[Payment]: diff --git a/method/resources/Reversal.py b/method/resources/Payments/Reversal.py similarity index 96% rename from method/resources/Reversal.py rename to method/resources/Payments/Reversal.py index ae4deff..e35f930 100644 --- a/method/resources/Reversal.py +++ b/method/resources/Payments/Reversal.py @@ -43,7 +43,7 @@ class ReversalResource(Resource): def __init__(self, config: Configuration): super(ReversalResource, self).__init__(config.add_path('reversals')) - def get(self, _id: str) -> Reversal: + def retrieve(self, _id: str) -> Reversal: return super(ReversalResource, self)._get_with_id(_id) def update(self, _id: str, opts: ReversalUpdateOpts) -> Reversal: diff --git a/method/resources/Payments/__init__.py b/method/resources/Payments/__init__.py new file mode 100644 index 0000000..138ae4b --- /dev/null +++ b/method/resources/Payments/__init__.py @@ -0,0 +1,2 @@ +from method.resources.Payments.Payment import Payment, PaymentResource +from method.resources.Payments.Reversal import ReversalResource diff --git a/method/resources/Report.py b/method/resources/Report.py index cae5593..7d7231d 100644 --- a/method/resources/Report.py +++ b/method/resources/Report.py @@ -44,7 +44,7 @@ class ReportResource(Resource): def __init__(self, config: Configuration): super(ReportResource, self).__init__(config.add_path('reports')) - def get(self, _id: str) -> Report: + def retrieve(self, _id: str) -> Report: return super(ReportResource, self)._get_with_id(_id) def create(self, opts: ReportCreateOpts, request_opts: Optional[RequestOpts] = None) -> Report: diff --git a/method/resources/RoutingNumber.py b/method/resources/RoutingNumber.py deleted file mode 100644 index 220f65b..0000000 --- a/method/resources/RoutingNumber.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import TypedDict, Optional, Literal - -from method.resource import Resource -from method.configuration import Configuration - - -RoutingNumberOfficeTypesLiterals = Literal[ - 'main', - 'branch' -] - - -class RoutingNumberAddress(TypedDict): - line1: str - line2: Optional[str] - city: str - state: str - zip: str - - -class RoutingNumber(TypedDict): - id: str - institution_name: str - routing_number: str - logo: str - office_type: RoutingNumberOfficeTypesLiterals - change_date: str - address: RoutingNumberAddress - phone: Optional[str] - - -class RoutingNumberResource(Resource): - def __init__(self, config: Configuration): - super(RoutingNumberResource, self).__init__(config.add_path('routing_numbers')) - - def get(self, routing_number: str) -> RoutingNumber: - return super(RoutingNumberResource, self)._get_with_params({'routing_number': routing_number}) diff --git a/method/resources/Simulate/Accounts.py b/method/resources/Simulate/Accounts.py new file mode 100644 index 0000000..fd21f05 --- /dev/null +++ b/method/resources/Simulate/Accounts.py @@ -0,0 +1,19 @@ +from typing import Any +from method.resource import Resource +from method.configuration import Configuration +from method.resources.Simulate.Transactions import SimulateTransactionsResource + + +class SimulateAccountSubResources: + transactions: SimulateTransactionsResource + + def __init__(self, _id: str, config: Configuration): + self.transactions = SimulateTransactionsResource(config.add_path(_id)) + + +class SimulateAccountResource(Resource): + def __init__(self, config: Configuration): + super(SimulateAccountResource, self).__init__(config.add_path('accounts')) + + def __call__(self, acc_id) -> SimulateAccountSubResources: + return SimulateAccountSubResources(acc_id, self.config) diff --git a/method/resources/SimulatePayment.py b/method/resources/Simulate/Payments.py similarity index 87% rename from method/resources/SimulatePayment.py rename to method/resources/Simulate/Payments.py index ffe6f4b..df355f9 100644 --- a/method/resources/SimulatePayment.py +++ b/method/resources/Simulate/Payments.py @@ -2,7 +2,7 @@ from method.resource import Resource from method.configuration import Configuration -from method.resources.Payment import Payment, PaymentStatusesLiterals +from method.resources.Payments.Payment import Payment, PaymentStatusesLiterals class SimulatePaymentUpdateOpts(TypedDict): diff --git a/method/resources/Simulate.py b/method/resources/Simulate/Simulate.py similarity index 60% rename from method/resources/Simulate.py rename to method/resources/Simulate/Simulate.py index 8eb9eaf..db3b21f 100644 --- a/method/resources/Simulate.py +++ b/method/resources/Simulate/Simulate.py @@ -1,12 +1,15 @@ from method.resource import Resource from method.configuration import Configuration -from method.resources.SimulatePayment import SimulatePaymentResource +from method.resources.Simulate.Payments import SimulatePaymentResource +from method.resources.Simulate.Accounts import SimulateAccountResource class SimulateResource(Resource): payments: SimulatePaymentResource + accounts: SimulateAccountResource def __init__(self, config: Configuration): _config = config.add_path('simulate') super(SimulateResource, self).__init__(_config) self.payments = SimulatePaymentResource(_config) + self.accounts = SimulateAccountResource(_config) diff --git a/method/resources/Simulate/Transactions.py b/method/resources/Simulate/Transactions.py new file mode 100644 index 0000000..377253f --- /dev/null +++ b/method/resources/Simulate/Transactions.py @@ -0,0 +1,11 @@ +from method.resource import Resource +from method.configuration import Configuration +from method.resources.Accounts.Transactions import AccountTransaction + + +class SimulateTransactionsResource(Resource): + def __init__(self, config: Configuration): + super(SimulateTransactionsResource, self).__init__(config.add_path('transactions')) + + def create(self) -> AccountTransaction: + return super(SimulateTransactionsResource, self)._create({}) diff --git a/method/resources/Simulate/__init__.py b/method/resources/Simulate/__init__.py new file mode 100644 index 0000000..2e5df6c --- /dev/null +++ b/method/resources/Simulate/__init__.py @@ -0,0 +1,3 @@ +from method.resources.Simulate.Simulate import SimulateResource +from method.resources.Simulate.Transactions import SimulateTransactionsResource +from method.resources.Simulate.Payments import SimulatePaymentResource diff --git a/method/resources/Transaction.py b/method/resources/Transaction.py deleted file mode 100644 index 4c52c10..0000000 --- a/method/resources/Transaction.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import TypedDict, List - -from method.resource import Resource -from method.configuration import Configuration - - -class Transaction(TypedDict): - id: str - acc_id: str - mcc: str - description: str - presentable_description: str - amount: str - currency: str - billing_amount: str - billing_currency: str - status: str - created_at: str - updated_at: str - - -class TransactionResource(Resource): - def __init__(self, config: Configuration): - super(TransactionResource, self).__init__(config.add_path('transactions')) - - def list(self) -> List[Transaction]: - return super(TransactionResource, self)._list(None) - - def get(self, _id: str) -> Transaction: - return super(TransactionResource, self)._get_with_id(_id) diff --git a/method/resources/Verification.py b/method/resources/Verification.py deleted file mode 100644 index c5083ab..0000000 --- a/method/resources/Verification.py +++ /dev/null @@ -1,86 +0,0 @@ -from typing import TypedDict, List, Literal, Dict, Any, Optional - -from method.resource import Resource -from method.configuration import Configuration -from method.errors import ResourceError - - -VerificationStatusesLiterals = Literal[ - 'initiated', - 'pending', - 'verified', - 'disabled' -] - - -VerificationTypesLiterals = Literal[ - 'micro_deposits', - 'plaid', - 'mx', - 'teller', - 'auto_verify', - 'trusted_provisioner' -] - - -class Verification(TypedDict): - id: str - status: VerificationStatusesLiterals - type: VerificationTypesLiterals - error: Optional[ResourceError] - initiated_at: str - pending_at: str - verified_at: str - disabled_at: str - created_at: str - updated_at: str - - -class VerificationMicroDepositsUpdate(TypedDict): - amounts: List[int] - - -class VerificationUpdateOpts(TypedDict): - micro_deposits: VerificationMicroDepositsUpdate - - -class VerificationPlaidCreate(TypedDict): - balances: Dict[str, Any] - transactions: List[Dict[str, Any]] - - -class VerificationMXCreate(TypedDict): - account: Dict[str, Any] - transactions: List[Dict[str, Any]] - -class VerificationTellerCreate(TypedDict): - balances: Dict[str, Any] - transactions: List[Dict[str, Any]] - - -class VerificationCreateOpts(TypedDict): - type: VerificationTypesLiterals - plaid: Optional[VerificationPlaidCreate] - mx: Optional[VerificationMXCreate] - teller: Optional[VerificationTellerCreate] - - -class VerificationTestAmountsResponse(TypedDict): - amounts: List[int] - - -class VerificationResource(Resource): - def __init__(self, config: Configuration): - super(VerificationResource, self).__init__(config.add_path('verification')) - - def get(self) -> Verification: - return super(VerificationResource, self)._get() - - def update(self, opts: VerificationUpdateOpts) -> Verification: - return super(VerificationResource, self)._update(opts) - - def create(self, opts: VerificationCreateOpts) -> Verification: - return super(VerificationResource, self)._create(opts) - - def get_test_amounts(self) -> VerificationTestAmountsResponse: - return super(VerificationResource, self)._get_with_sub_path('amounts') diff --git a/method/resources/Webhook.py b/method/resources/Webhook.py index ecd2740..642f0d9 100644 --- a/method/resources/Webhook.py +++ b/method/resources/Webhook.py @@ -50,7 +50,7 @@ class WebhookResource(Resource): def __init__(self, config: Configuration): super(WebhookResource, self).__init__(config.add_path('webhooks')) - def get(self, _id: str) -> Webhook: + def retrieve(self, _id: str) -> Webhook: return super(WebhookResource, self)._get_with_id(_id) def delete(self, _id: str) -> Webhook: diff --git a/method/resources/__init__.py b/method/resources/__init__.py deleted file mode 100644 index 01ae365..0000000 --- a/method/resources/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# type: ignore -from method.resources.Account import Account, AccountResource -from method.resources.AccountSync import AccountSyncResource -from method.resources.Bin import Bin, BinResource -from method.resources.Element import Element, ElementResource -from method.resources.Entity import Entity, EntityResource -from method.resources.Merchant import Merchant, MerchantResource -from method.resources.Payment import Payment, PaymentResource -from method.resources.Report import Report, ReportResource -from method.resources.RoutingNumber import RoutingNumber, RoutingNumberResource -from method.resources.Verification import Verification, VerificationResource -from method.resources.Webhook import Webhook, WebhookResource -from method.resources.HealthCheck import HealthCheckResource -from method.resources.Simulate import SimulatePaymentResource -from method.resources.Transaction import TransactionResource diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..2f4c80e --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +asyncio_mode = auto diff --git a/requirements.txt b/requirements.txt index b077a73..0f3ff42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,4 @@ -hammock==0.2.4 \ No newline at end of file +hammock==0.2.4 +pytest==8.2.1 +pytest-asyncio==0.23.7 +python-dotenv==1.0.1 \ No newline at end of file diff --git a/setup.py b/setup.py index a276f4b..5c3fec6 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='method-python', - version='0.0.44', + version='1.0.0', description='Python library for the Method API', long_description='Python library for the Method API', long_description_content_type='text/x-rst', @@ -10,7 +10,7 @@ author_email='marco@mdelcarmen.me', url='https://github.com/MethodFi/method-python', license='MIT', - packages=find_packages(exclude='tests'), + packages=find_packages(exclude='test'), package_data={'README': ['README.md']}, python_requires=">=3.6", install_requires=[ diff --git a/test/resources/Account_test.py b/test/resources/Account_test.py new file mode 100644 index 0000000..c6ce8a4 --- /dev/null +++ b/test/resources/Account_test.py @@ -0,0 +1,803 @@ +import os +import pytest +from method import Method +from dotenv import load_dotenv +from utils import await_results +from method.resources.Accounts.Account import Account +from method.resources.Accounts.Balances import AccountBalance +from method.resources.Accounts.CardBrands import AccountCardBrand +from method.resources.Accounts.Payoffs import AccountPayoff +from method.resources.Accounts.Sensitive import AccountSensitive +from method.resources.Accounts.Subscriptions import AccountSubscription, AccountSubscriptionsResponse +from method.resources.Accounts.Transactions import AccountTransaction +from method.resources.Accounts.Updates import AccountUpdate +from method.resources.Accounts.VerificationSessions import AccountVerificationSession + +load_dotenv() + +API_KEY = os.getenv('API_KEY') + +method = Method(env='dev', api_key=API_KEY) + +accounts_create_ach_response = None +accounts_create_liability_response = None +accounts_retrieve_response = None +accounts_list_response = None +balances_create_response = None +card_brand_create_response = None +payoff_create_response = None +verification_session_create = None +verification_session_update = None +sensitive_data_response = None +transactions_response = None +create_txn_subscriptions_response = None +create_update_subscriptions_response = None +create_update_snapshot_subscriptions_response = None +create_updates_response = None + + +@pytest.fixture(scope="module") +def setup(): + holder_1_response = method.entities.create({ + 'type': 'individual', + 'individual': { + 'first_name': 'Test', + 'last_name': 'McTesterson', + 'phone': '+15121231111' + } + }) + + phone_verification = method.entities(holder_1_response['id']).verification_sessions.create({ + 'type': 'phone', + 'method': 'byo_sms', + 'byo_sms': { + 'timestamp': '2021-09-01T00:00:00.000Z', + }, + }) + + identity_verification = method.entities(holder_1_response['id']).verification_sessions.create({ + 'type': 'identity', + 'method': 'kba', + 'kba': {}, + }) + + holder_connect_response = method.entities(holder_1_response['id']).connect.create() + + test_credit_card_accounts = method.accounts.list({ + 'holder_id': holder_1_response['id'], + 'liability.type': 'credit_card', + 'liability.mch_id': 'mch_302086', + }) + test_credit_card_account = test_credit_card_accounts[0] + + test_auto_loan_accounts = method.accounts.list({ + 'holder_id': holder_1_response['id'], + 'liability.type': 'auto_loan', + 'liability.mch_id': 'mch_2347', + }) + test_auto_loan_account = test_auto_loan_accounts[0] + + return { + 'holder_1_response': holder_1_response, + 'holder_connect_response': holder_connect_response, + 'test_credit_card_account': test_credit_card_account, + 'test_auto_loan_account': test_auto_loan_account, + 'phone_verification': phone_verification, + 'identity_verification': identity_verification, + } + + +def test_create_ach_account(setup): + holder_1_response = setup['holder_1_response'] + global accounts_create_ach_response + + accounts_create_ach_response = method.accounts.create({ + 'holder_id': holder_1_response['id'], + 'ach': { + 'routing': '062103000', + 'number': '123456789', + 'type': 'checking', + }, + }) + + expect_results: Account = { + 'id': accounts_create_ach_response['id'], + 'holder_id': holder_1_response['id'], + 'type': 'ach', + 'ach': { + 'routing': '062103000', + 'number': '123456789', + 'type': 'checking', + }, + 'latest_verification_session': accounts_create_ach_response['latest_verification_session'], + 'products': ['payment'], + 'restricted_products': [], + 'status': 'active', + 'error': None, + 'metadata': None, + 'created_at': accounts_create_ach_response['created_at'], + 'updated_at': accounts_create_ach_response['updated_at'], + } + + assert accounts_create_ach_response == expect_results + + +def test_create_liability_account(setup): + holder_1_response = setup['holder_1_response'] + global accounts_create_liability_response + + accounts_create_liability_response = method.accounts.create({ + 'holder_id': holder_1_response['id'], + 'liability': { + 'mch_id': 'mch_302086', + 'account_number': '4936494462408721', + }, + }) + + accounts_create_liability_response['products'] = accounts_create_liability_response['products'].sort() + + expect_results: Account = { + 'id': accounts_create_liability_response['id'], + 'holder_id': holder_1_response['id'], + 'type': 'liability', + 'liability': { + 'fingerprint': None, + 'mch_id': 'mch_183', + 'mask': '8721', + 'ownership': 'unknown', + 'type': 'credit_card', + 'name': 'Chase Sapphire Reserve', + }, + 'latest_verification_session': accounts_create_liability_response['latest_verification_session'], + 'balance': None, + 'update': accounts_create_liability_response['update'], + 'card_brand': None, + 'products': [ 'balance', 'payment', 'sensitive', 'update' ].sort(), + 'restricted_products': accounts_create_liability_response['restricted_products'], + 'subscriptions': accounts_create_liability_response['subscriptions'], + 'available_subscriptions': [ 'update' ], + 'restricted_subscriptions': [], + 'status': 'active', + 'error': None, + 'metadata': None, + 'created_at': accounts_create_liability_response['created_at'], + 'updated_at': accounts_create_liability_response['updated_at'] + } + + assert accounts_create_liability_response == expect_results + + +def test_retrieve_account(setup): + accounts_retrieve_response = method.accounts.retrieve(accounts_create_ach_response['id']) + + expect_results: Account = { + 'id': accounts_create_ach_response['id'], + 'holder_id': setup['holder_1_response']['id'], + 'type': 'ach', + 'ach': { + 'routing': '062103000', + 'number': '123456789', + 'type': 'checking', + }, + 'latest_verification_session': accounts_create_ach_response['latest_verification_session'], + 'products': ['payment'], + 'restricted_products': [], + 'status': 'active', + 'error': None, + 'metadata': None, + 'created_at': accounts_retrieve_response['created_at'], + 'updated_at': accounts_retrieve_response['updated_at'], + } + + assert accounts_retrieve_response == expect_results + + +def test_list_accounts(setup): + holder_1_response = setup['holder_1_response'] + holder_connect_response = setup['holder_connect_response'] + accounts_list_response = method.accounts.list({ + 'holder_id': holder_1_response['id'], + 'type': 'liability', + 'status': 'active', + }) + + account_ids = sorted( + [account['id'] for account in accounts_list_response if account['id'] != accounts_create_liability_response['id']], + reverse=True + )[:len(holder_connect_response['accounts']) if holder_connect_response['accounts'] else 0] + + connect_acc_ids = sorted(holder_connect_response['accounts'], reverse=True) if holder_connect_response['accounts'] else ['no_data'] + dupes = account_ids + connect_acc_ids + test_length = len(set(dupes)) + + assert accounts_list_response is not None + assert isinstance(accounts_list_response, list) + assert test_length == len(connect_acc_ids) + + +def test_create_balances(setup): + global balances_create_response + test_credit_card_account = setup['test_credit_card_account'] + + balances_create_response = method.accounts(test_credit_card_account['id']).balances.create() + + expect_results: AccountBalance = { + 'id': balances_create_response['id'], + 'account_id': test_credit_card_account['id'], + 'status': 'pending', + 'amount': None, + 'error': None, + 'created_at': balances_create_response['created_at'], + 'updated_at': balances_create_response['updated_at'], + } + + assert balances_create_response == expect_results + + +@pytest.mark.asyncio +async def test_retrieve_balance(setup): + test_credit_card_account = setup['test_credit_card_account'] + + def get_account_balances(): + return method.accounts(test_credit_card_account['id']).balances.retrieve(balances_create_response['id']) + + balances_retrieve_response = await await_results(get_account_balances) + + expect_results: AccountBalance = { + 'id': balances_create_response['id'], + 'account_id': test_credit_card_account['id'], + 'status': 'completed', + 'amount': 1866688, + 'error': None, + 'created_at': balances_retrieve_response['created_at'], + 'updated_at': balances_retrieve_response['updated_at'], + } + + assert balances_retrieve_response == expect_results + + +def test_create_card_brands(setup): + global card_brand_create_response + test_credit_card_account = setup['test_credit_card_account'] + + card_brand_create_response = method.accounts(test_credit_card_account['id']).card_brands.create() + + expect_results: AccountCardBrand = { + 'id': card_brand_create_response['id'], + 'account_id': test_credit_card_account['id'], + 'network': 'visa', + 'status': 'completed', + 'issuer': None, + 'last4': '1580', + 'brands': card_brand_create_response['brands'], + 'shared': False, + 'error': None, + 'created_at': card_brand_create_response['created_at'], + 'updated_at': card_brand_create_response['updated_at'], + } + + assert card_brand_create_response == expect_results + + +def test_retrieve_card_brands(setup): + test_credit_card_account = setup['test_credit_card_account'] + card_retrieve_response = method.accounts(test_credit_card_account['id']).card_brands.retrieve(card_brand_create_response['id']) + + expect_results: AccountCardBrand = { + 'id': card_brand_create_response['id'], + 'account_id': test_credit_card_account['id'], + 'network': 'visa', + 'status': 'completed', + 'issuer': None, + 'last4': '1580', + 'brands': card_brand_create_response['brands'], + 'shared': False, + 'error': None, + 'created_at': card_retrieve_response['created_at'], + 'updated_at': card_retrieve_response['updated_at'], + } + + assert card_retrieve_response == expect_results + + +def test_create_payoffs(setup): + global payoff_create_response + test_auto_loan_account = setup['test_auto_loan_account'] + + payoff_create_response = method.accounts(test_auto_loan_account['id']).payoffs.create() + + expect_results: AccountPayoff = { + 'id': payoff_create_response['id'], + 'account_id': test_auto_loan_account['id'], + 'amount': None, + 'per_diem_amount': None, + 'term': None, + 'status': 'pending', + 'error': None, + 'created_at': payoff_create_response['created_at'], + 'updated_at': payoff_create_response['updated_at'], + } + + assert payoff_create_response == expect_results + + +@pytest.mark.asyncio +async def test_retrieve_payoffs(setup): + test_auto_loan_account = setup['test_auto_loan_account'] + def get_payoff(): + return method.accounts(test_auto_loan_account['id']).payoffs.retrieve(payoff_create_response['id']) + + payoff_retrieve_response = await await_results(get_payoff) + + expect_results: AccountPayoff = { + 'id': payoff_create_response['id'], + 'account_id': test_auto_loan_account['id'], + 'amount': 6083988, + 'per_diem_amount': None, + 'term': 15, + 'status': 'completed', + 'error': None, + 'created_at': payoff_retrieve_response['created_at'], + 'updated_at': payoff_retrieve_response['updated_at'], + } + + assert payoff_retrieve_response == expect_results + + +def test_create_account_verification_sessions(setup): + global verification_session_create + test_credit_card_account = setup['test_credit_card_account'] + + verification_session_create = method.accounts(test_credit_card_account['id']).verification_sessions.create({ + 'type': 'pre_auth' + }) + + expect_results: AccountVerificationSession = { + 'id': verification_session_create['id'], + 'account_id': test_credit_card_account['id'], + 'status': 'pending', + 'type': 'pre_auth', + 'error': None, + 'pre_auth': { + 'billing_zip_code': 'xxxxx', + 'billing_zip_code_check': None, + 'cvv': None, + 'cvv_check': None, + 'exp_check': None, + 'exp_month': 'xx', + 'exp_year': 'xxxx', + 'number': 'xxxxxxxxxxxxxxxx', + 'pre_auth_check': None, + }, + 'created_at': verification_session_create['created_at'], + 'updated_at': verification_session_create['updated_at'], + } + + assert verification_session_create == expect_results + + +def test_update_account_verification_sessions(setup): + global verification_session_update + test_credit_card_account = setup['test_credit_card_account'] + + verification_session_update = method.accounts(test_credit_card_account['id']).verification_sessions.update(verification_session_create['id'], { + 'pre_auth': { + 'exp_month': '03', + 'exp_year': '2028', + 'billing_zip_code': '78758', + 'cvv': '878' + } + }) + + expect_results: AccountVerificationSession = { + 'id': verification_session_create['id'], + 'account_id': test_credit_card_account['id'], + 'status': 'verified', + 'type': 'pre_auth', + 'error': None, + 'pre_auth': { + 'billing_zip_code': 'xxxxx', + 'billing_zip_code_check': 'pass', + 'cvv': 'xxx', + 'cvv_check': 'pass', + 'exp_check': 'pass', + 'exp_month': 'xx', + 'exp_year': 'xxxx', + 'number': 'xxxxxxxxxxxxxxxx', + 'pre_auth_check': 'pass', + }, + 'created_at': verification_session_update['created_at'], + 'updated_at': verification_session_update['updated_at'], + } + + assert verification_session_update == expect_results + + +@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) + + expect_results: AccountVerificationSession = { + 'id': verification_session_update['id'], + 'account_id': test_credit_card_account['id'], + 'status': 'verified', + 'type': 'pre_auth', + 'error': None, + 'pre_auth': { + 'billing_zip_code': 'xxxxx', + 'billing_zip_code_check': 'pass', + 'cvv': 'xxx', + 'cvv_check': 'pass', + 'exp_check': 'pass', + 'exp_month': 'xx', + 'exp_year': 'xxxx', + 'number': 'xxxxxxxxxxxxxxxx', + 'pre_auth_check': 'pass', + }, + 'created_at': verification_session_retrieve_response['created_at'], + 'updated_at': verification_session_retrieve_response['updated_at'], + } + + assert verification_session_retrieve_response == expect_results + + +def test_create_account_sensitive(setup): + global sensitive_data_response + test_credit_card_account = setup['test_credit_card_account'] + + sensitive_data_response = method.accounts(test_credit_card_account['id']).sensitive.create({ + 'expand': [ + 'credit_card.number', + 'credit_card.exp_month', + 'credit_card.exp_year' + ] + }) + + expect_results: AccountSensitive = { + 'id': sensitive_data_response['id'], + 'account_id': test_credit_card_account['id'], + 'type': 'credit_card', + 'credit_card': { + 'billing_zip_code': None, + 'number': '5555555555551580', + 'exp_month': '03', + 'exp_year': '2028', + 'cvv': None + }, + 'status': 'completed', + 'error': None, + 'created_at': sensitive_data_response['created_at'], + 'updated_at': sensitive_data_response['updated_at'] + } + + assert sensitive_data_response == expect_results + + +def test_create_transaction_subscription(setup): + global create_txn_subscriptions_response + test_credit_card_account = setup['test_credit_card_account'] + + create_txn_subscriptions_response = method.accounts(test_credit_card_account['id']).subscriptions.create('transactions') + + expect_results: AccountSubscription = { + 'id': create_txn_subscriptions_response['id'], + 'name': 'transactions', + 'status': 'active', + 'latest_request_id': None, + 'created_at': create_txn_subscriptions_response['created_at'], + 'updated_at': create_txn_subscriptions_response['updated_at'], + } + + assert create_txn_subscriptions_response == expect_results + + +def test_create_update_subscription(setup): + global create_update_subscriptions_response + test_credit_card_account = setup['test_credit_card_account'] + + create_update_subscriptions_response = method.accounts(test_credit_card_account['id']).subscriptions.create('update') + + expect_results: AccountSubscription = { + 'id': create_update_subscriptions_response['id'], + 'name': 'update', + 'status': 'active', + 'latest_request_id': None, + 'created_at': create_update_subscriptions_response['created_at'], + 'updated_at': create_update_subscriptions_response['updated_at'], + } + + assert create_update_subscriptions_response == expect_results + + +def test_create_snapshot_subscription(setup): + global create_update_snapshot_subscriptions_response + test_auto_loan_account = setup['test_auto_loan_account'] + + create_update_snapshot_subscriptions_response = method.accounts(test_auto_loan_account['id']).subscriptions.create('update.snapshot') + + expect_results: AccountSubscription = { + 'id': create_update_snapshot_subscriptions_response['id'], + 'name': 'update.snapshot', + 'status': 'active', + 'latest_request_id': None, + 'created_at': create_update_snapshot_subscriptions_response['created_at'], + 'updated_at': create_update_snapshot_subscriptions_response['updated_at'], + } + + assert create_update_snapshot_subscriptions_response == expect_results + + +def test_list_subscriptions(setup): + test_credit_card_account = setup['test_credit_card_account'] + test_auto_loan_account = setup['test_auto_loan_account'] + + subscriptions_list_response = method.accounts(test_credit_card_account['id']).subscriptions.list() + subscriptions_update_snapshot_list_response = method.accounts(test_auto_loan_account['id']).subscriptions.list() + + expect_results_card: AccountSubscriptionsResponse = { + 'transactions': { + 'id': create_txn_subscriptions_response['id'], + 'name': 'transactions', + 'status': 'active', + 'latest_request_id': None, + 'created_at': create_txn_subscriptions_response['created_at'], + 'updated_at': create_txn_subscriptions_response['updated_at'] + }, + 'update': { + 'id': create_update_subscriptions_response['id'], + 'name': 'update', + 'status': 'active', + 'latest_request_id': None, + 'created_at': create_update_subscriptions_response['created_at'], + 'updated_at': create_update_subscriptions_response['updated_at'] + } + } + + expect_results_auto_loan: AccountSubscriptionsResponse = { + 'update.snapshot': { + 'id': create_update_snapshot_subscriptions_response['id'], + 'name': 'update.snapshot', + 'status': 'active', + 'latest_request_id': None, + 'created_at': create_update_snapshot_subscriptions_response['created_at'], + 'updated_at': create_update_snapshot_subscriptions_response['updated_at'] + } + } + + assert subscriptions_list_response == expect_results_card + assert subscriptions_update_snapshot_list_response == expect_results_auto_loan + + +def test_retrieve_subscription(setup): + test_credit_card_account = setup['test_credit_card_account'] + test_auto_loan_account = setup['test_auto_loan_account'] + + retrieve_txn_subscription_response = method.accounts(test_credit_card_account['id']).subscriptions.retrieve(create_txn_subscriptions_response['id']) + retrieve_update_subscription_response = method.accounts(test_credit_card_account['id']).subscriptions.retrieve(create_update_subscriptions_response['id']) + retrieve_update_snapshot_subscription_response = method.accounts(test_auto_loan_account['id']).subscriptions.retrieve(create_update_snapshot_subscriptions_response['id']) + + expect_results_txn: AccountSubscription = { + 'id': create_txn_subscriptions_response['id'], + 'name': 'transactions', + 'status': 'active', + 'latest_request_id': None, + 'created_at': retrieve_txn_subscription_response['created_at'], + 'updated_at': retrieve_txn_subscription_response['updated_at'], + } + + expect_results_update: AccountSubscription = { + 'id': create_update_subscriptions_response['id'], + 'name': 'update', + 'status': 'active', + 'latest_request_id': None, + 'created_at': retrieve_update_subscription_response['created_at'], + 'updated_at': retrieve_update_subscription_response['updated_at'], + } + + expect_results_update_snapshot: AccountSubscription = { + 'id': create_update_snapshot_subscriptions_response['id'], + 'name': 'update.snapshot', + 'status': 'active', + 'latest_request_id': None, + 'created_at': retrieve_update_snapshot_subscription_response['created_at'], + 'updated_at': retrieve_update_snapshot_subscription_response['updated_at'], + } + + assert retrieve_txn_subscription_response == expect_results_txn + assert retrieve_update_subscription_response == expect_results_update + assert retrieve_update_snapshot_subscription_response == expect_results_update_snapshot + + +def test_delete_subscription(setup): + test_auto_loan_account = setup['test_auto_loan_account'] + + delete_update_snapshot_subscription_response = method.accounts(test_auto_loan_account['id']).subscriptions.delete(create_update_snapshot_subscriptions_response['id']) + + expect_results_update_snapshot = { + 'id': create_update_snapshot_subscriptions_response['id'], + 'name': 'update.snapshot', + 'status': 'inactive', + 'latest_request_id': None, + 'created_at': delete_update_snapshot_subscription_response['created_at'], + 'updated_at': delete_update_snapshot_subscription_response['updated_at'], + } + + assert delete_update_snapshot_subscription_response == expect_results_update_snapshot + + +def test_list_transactions(setup): + global transactions_response + + test_credit_card_account = setup['test_credit_card_account'] + simulated_transaction = method.simulate.accounts(test_credit_card_account['id']).transactions.create() + + transactions_response = method.accounts(test_credit_card_account['id']).transactions.list() + + transactions_response = transactions_response[0] + + expect_results: AccountTransaction = { + 'id': transactions_response['id'], + 'account_id': test_credit_card_account['id'], + 'merchant': simulated_transaction['merchant'], + 'network': 'visa', + 'network_data': None, + 'amount': simulated_transaction['amount'], + 'currency': 'USD', + 'billing_amount': simulated_transaction['billing_amount'], + 'billing_currency': 'USD', + 'status': 'cleared', + 'error': None, + 'created_at': transactions_response['created_at'], + 'updated_at': transactions_response['updated_at'], + } + + assert transactions_response == expect_results + + +def test_create_updates(setup): + global create_updates_response + test_credit_card_account = setup['test_credit_card_account'] + + create_updates_response = method.accounts(test_credit_card_account['id']).updates.create() + + expect_results: AccountUpdate = { + 'id': create_updates_response['id'], + 'account_id': test_credit_card_account['id'], + 'status': 'pending', + 'source': 'direct', + 'type': 'credit_card', + 'credit_card': { + 'sub_type': None, + 'opened_at': None, + 'closed_at': None, + 'balance': None, + 'last_payment_amount': None, + 'last_payment_date': None, + 'next_payment_due_date': None, + 'next_payment_minimum_amount': None, + 'interest_rate_type': None, + 'interest_rate_percentage_max': None, + 'interest_rate_percentage_min': None, + 'available_credit': None, + 'credit_limit': None, + 'usage_pattern': None + }, + 'error': None, + 'created_at': create_updates_response['created_at'], + 'updated_at': create_updates_response['updated_at'], + } + + assert create_updates_response == expect_results + +@pytest.mark.asyncio +async def test_retrieve_updates(setup): + test_credit_card_account = setup['test_credit_card_account'] + + def get_updates(): + return method.accounts(test_credit_card_account['id']).updates.retrieve(create_updates_response['id']) + + updates_retrieve_response = await await_results(get_updates) + + expect_results: AccountUpdate = { + 'id': create_updates_response['id'], + 'account_id': test_credit_card_account['id'], + 'status': 'completed', + 'source': 'direct', + 'type': 'credit_card', + 'credit_card': { + 'sub_type': 'flexible_spending', + 'opened_at': '2016-12-20', + 'closed_at': None, + 'balance': 1866688, + 'last_payment_amount': 100000, + 'last_payment_date': '2023-01-04', + 'next_payment_due_date': '2023-02-09', + 'next_payment_minimum_amount': 51060, + 'interest_rate_type': 'variable', + 'interest_rate_percentage_max': 27.5, + 'interest_rate_percentage_min': 20.5, + 'available_credit': 930000, + 'credit_limit': 2800000, + 'usage_pattern': None + }, + 'error': None, + 'created_at': updates_retrieve_response['created_at'], + 'updated_at': updates_retrieve_response['updated_at'], + } + + assert updates_retrieve_response == expect_results + + + +def test_list_updates_for_account(setup): + test_credit_card_account = setup['test_credit_card_account'] + + list_updates_response = method.accounts(test_credit_card_account['id']).updates.list() + + update_to_check = next((update for update in list_updates_response if update['id'] == create_updates_response['id']), None) + + expect_results: AccountUpdate = { + 'id': create_updates_response['id'], + 'account_id': test_credit_card_account['id'], + 'status': 'completed', + 'source': 'direct', + 'type': 'credit_card', + 'credit_card': { + 'sub_type': 'flexible_spending', + 'opened_at': '2016-12-20', + 'closed_at': None, + 'balance': 1866688, + 'last_payment_amount': 100000, + 'last_payment_date': '2023-01-04', + 'next_payment_due_date': '2023-02-09', + 'next_payment_minimum_amount': 51060, + 'interest_rate_type': 'variable', + 'interest_rate_percentage_max': 27.5, + 'interest_rate_percentage_min': 20.5, + 'available_credit': 930000, + 'credit_limit': 2800000, + 'usage_pattern': None + }, + 'error': None, + 'created_at': update_to_check['created_at'] if update_to_check else None, + 'updated_at': update_to_check['updated_at'] if update_to_check else None + } + + + assert update_to_check == expect_results + + +def test_withdraw_account_consent(setup): + test_credit_card_account = setup['test_credit_card_account'] + holder_1_response = setup['holder_1_response'] + + withdraw_consent_response = method.accounts.withdraw_consent(test_credit_card_account['id']) + + expect_results: Account = { + 'id': withdraw_consent_response['id'], + 'holder_id': holder_1_response['id'], + 'status': 'disabled', + 'type': None, + 'liability': None, + 'products': [], + 'restricted_products': [], + 'subscriptions': [], + 'available_subscriptions': [], + 'restricted_subscriptions': [], + 'error': { + 'type': 'ACCOUNT_DISABLED', + 'sub_type': 'ACCOUNT_CONSENT_WITHDRAWN', + 'code': 11004, + 'message': 'Account was disabled due to consent withdrawal.', + }, + 'metadata': None, + 'created_at': withdraw_consent_response['created_at'], + 'updated_at': withdraw_consent_response['updated_at'], + } + + assert withdraw_consent_response == expect_results diff --git a/test/resources/Element_test.py b/test/resources/Element_test.py new file mode 100644 index 0000000..159bf06 --- /dev/null +++ b/test/resources/Element_test.py @@ -0,0 +1,62 @@ +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) + +element_create_connect_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_connect_token(setup): + global element_create_connect_token_response + + element_create_connect_token_response = method.elements.token.create({ + 'entity_id': setup['entity_1_id'], + 'type': 'connect', + 'connect': { + 'products': [ 'balance' ], + 'account_filters': { + 'selection_type': 'multiple', + 'liability_types': [ 'credit_card' ] + } + } + }) + + assert 'element_token' in element_create_connect_token_response + assert len(element_create_connect_token_response) == 1 + + +def test_retrieve_element_results(setup): + element_retrieve_results_response = method.elements.token.results(element_create_connect_token_response['element_token']) + + expect_results = { + 'authenticated': False, + 'cxn_id': None, + 'accounts': [], + 'entity_id': setup['entity_1_id'], + 'events': [] + } + + assert element_retrieve_results_response == expect_results diff --git a/test/resources/Entity_test.py b/test/resources/Entity_test.py new file mode 100644 index 0000000..1df2191 --- /dev/null +++ b/test/resources/Entity_test.py @@ -0,0 +1,658 @@ +import os +import pytest +from method import Method +from dotenv import load_dotenv +from utils import await_results +from method.resources.Entities.Entity import Entity +from method.resources.Entities.Connect import EntityConnect +from method.resources.Entities.CreditScores import EntityCreditScores +from method.resources.Entities.Identities import EntityIdentity +from method.resources.Entities.Products import EntityProduct, EntityProductListResponse +from method.resources.Entities.Subscriptions import EntitySubscription +from method.resources.Entities.VerificationSessions import EntityVerificationSession + +load_dotenv() + +API_KEY = os.getenv('API_KEY') + +method = Method(env='dev', api_key=API_KEY) + +entities_create_response = None +entitiy_with_identity_cap = None +entities_retrieve_response = None +entities_update_response = None +entities_list_response = None +entities_connect_create_response = None +entities_account_list_response = None +entities_account_ids = None +entities_create_credit_score_response = None +entities_create_idenitity_response = None +entities_retrieve_product_list_response = None +entities_create_connect_subscription_response = None +entities_create_credit_score_subscription_response = None +entities_create_verification_session_response = None + +# ENTITY CORE METHODS TESTS + +def test_create_entity(): + global entities_create_response + entities_create_response = method.entities.create({ + 'type': 'individual', + 'individual': {}, + 'metadata': {} + }) + + entities_create_response['restricted_subscriptions'] = entities_create_response['restricted_subscriptions'].sort() + + expect_results: Entity = { + 'id': entities_create_response['id'], + 'type': 'individual', + 'individual': { + 'first_name': None, + 'last_name': None, + 'phone': None, + 'dob': None, + 'email': None, + 'ssn': None, + 'ssn_4': None, + }, + 'address': { + 'line1': None, + 'line2': None, + 'city': None, + 'state': None, + 'zip': None, + }, + 'verification': { + 'identity': { + 'verified': False, + 'matched': False, + 'latest_verification_session': None, + 'methods': [ + 'element', + 'kba', + ], + }, + 'phone': { + 'verified': False, + 'latest_verification_session': None, + 'methods': [ + 'element', + 'sna', + 'sms', + 'byo_sms' + ], + }, + }, + 'connect': None, + 'credit_score': None, + 'products': [], + 'restricted_products': entities_create_response['restricted_products'], + 'subscriptions': [], + 'available_subscriptions': [], + 'restricted_subscriptions': [ 'connect', 'credit_score' ].sort(), + 'status': 'incomplete', + 'error': None, + 'metadata': {}, + 'created_at': entities_create_response['created_at'], + 'updated_at': entities_create_response['updated_at'], + } + + assert entities_create_response == expect_results + + +def test_retrieve_entity(): + global entities_retrieve_response + entities_retrieve_response = method.entities.retrieve(entities_create_response['id']) + entities_retrieve_response['restricted_subscriptions'] = entities_retrieve_response['restricted_subscriptions'].sort() + + expect_results: Entity = { + 'id': entities_create_response['id'], + 'type': 'individual', + 'individual': { + 'first_name': None, + 'last_name': None, + 'phone': None, + 'dob': None, + 'email': None, + 'ssn': None, + 'ssn_4': None, + }, + 'address': { + 'line1': None, + 'line2': None, + 'city': None, + 'state': None, + 'zip': None, + }, + 'verification': { + 'identity': { + 'verified': False, + 'matched': False, + 'latest_verification_session': None, + 'methods': [ + 'element', + 'kba', + ], + }, + 'phone': { + 'verified': False, + 'latest_verification_session': None, + 'methods': [ + 'element', + 'sna', + 'sms', + 'byo_sms' + ], + }, + }, + 'connect': None, + 'credit_score': None, + 'products': [], + 'restricted_products': entities_retrieve_response['restricted_products'], + 'subscriptions': [], + 'available_subscriptions': [], + 'restricted_subscriptions': [ 'connect', 'credit_score' ].sort(), + 'status': 'incomplete', + 'error': None, + 'metadata': {}, + 'created_at': entities_retrieve_response['created_at'], + 'updated_at': entities_retrieve_response['updated_at'], + } + + assert entities_retrieve_response == expect_results + + +def test_update_entity(): + global entities_update_response + entities_update_response = method.entities.update(entities_create_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() + + expect_results: Entity = { + 'id': entities_create_response['id'], + 'type': 'individual', + 'individual': { + 'first_name': 'John', + 'last_name': 'Doe', + 'phone': '+15121231111', + 'dob': None, + 'email': None, + 'ssn': None, + 'ssn_4': None, + }, + 'address': { + 'line1': None, + 'line2': None, + 'city': None, + 'state': None, + 'zip': None, + }, + 'verification': { + 'identity': { + 'verified': False, + 'matched': True, + 'latest_verification_session': entities_update_response['verification']['identity']['latest_verification_session'], + 'methods': [ + 'element', + 'kba', + ], + }, + 'phone': { + 'verified': False, + 'latest_verification_session': entities_update_response['verification']['identity']['latest_verification_session'], + 'methods': [ + 'element', + 'sna', + 'sms', + 'byo_sms' + ], + }, + }, + 'connect': None, + 'credit_score': None, + 'products': [ 'identity' ], + 'restricted_products': [ 'connect', 'credit_score' ].sort(), + 'subscriptions': [], + 'available_subscriptions': [], + 'restricted_subscriptions': [ 'connect', 'credit_score' ].sort(), + 'status': 'incomplete', + 'error': None, + 'metadata': {}, + 'created_at': entities_update_response['created_at'], + 'updated_at': entities_update_response['updated_at'], + } + + assert entities_update_response == expect_results + +def test_list_entities(): + global entities_list_response + entities_list_response = method.entities.list() + entities_list_response = [entity['id'] for entity in entities_list_response] + + assert entities_create_response['id'] in entities_list_response + +# ENTITY VERIFICATION TESTS + +def test_create_entity_phone_verification(): + entity_create_phone_verification_response = method.entities(entities_create_response['id']).verification_sessions.create({ + 'type': 'phone', + 'method': 'byo_sms', + 'byo_sms': { + 'timestamp': '2021-09-01T00:00:00.000Z', + } + }) + + expect_results: EntityVerificationSession = { + 'id': entity_create_phone_verification_response['id'], + 'entity_id': entities_create_response['id'], + 'byo_sms': { + 'timestamp': '2021-09-01T00:00:00.000Z', + }, + 'method': 'byo_sms', + 'status': 'verified', + 'type': 'phone', + 'error': None, + 'created_at': entity_create_phone_verification_response['created_at'], + 'updated_at': entity_create_phone_verification_response['updated_at'], + } + + assert entity_create_phone_verification_response == expect_results + + +def test_create_entity_individual_verification(): + entity_create_individual_verification_response = method.entities(entities_create_response['id']).verification_sessions.create({ + 'type': 'identity', + 'method': 'kba', + 'kba': {} + }) + + expect_results: EntityVerificationSession = { + 'id': entity_create_individual_verification_response['id'], + 'entity_id': entities_create_response['id'], + 'kba': { + 'authenticated': True, + 'questions': [], + }, + 'method': 'kba', + 'status': 'verified', + 'type': 'identity', + 'error': None, + 'created_at': entity_create_individual_verification_response['created_at'], + 'updated_at': entity_create_individual_verification_response['updated_at'], + } + + assert entity_create_individual_verification_response == expect_results + + +# ENTITY CONNECT TESTS + +def test_create_entity_connect(): + global entities_connect_create_response + global entities_account_ids + entities_connect_create_response = method.entities(entities_create_response['id']).connect.create() + entities_connect_create_response['accounts'] = entities_connect_create_response['accounts'].sort() + entities_account_list_response = method.accounts.list({ 'holder_id': entities_create_response['id'], 'type': 'liability' }) + entities_account_ids = [account['id'] for account in entities_account_list_response].sort() + + expect_results: EntityConnect = { + 'id': entities_connect_create_response['id'], + 'entity_id': entities_create_response['id'], + 'status': 'completed', + 'accounts': entities_account_ids, + 'error': None, + 'created_at': entities_connect_create_response['created_at'], + 'updated_at': entities_connect_create_response['updated_at'], + } + + assert entities_connect_create_response == expect_results + + +def test_retrieve_entity_connect(): + entities_connect_retrieve_response = method.entities(entities_create_response['id']).connect.retrieve(entities_connect_create_response['id']) + entities_connect_retrieve_response['accounts'] = entities_connect_retrieve_response['accounts'].sort() + + expect_results: EntityConnect = { + 'id': entities_connect_create_response['id'], + 'entity_id': entities_create_response['id'], + 'status': 'completed', + 'accounts': entities_account_ids, + 'error': None, + 'created_at': entities_connect_create_response['created_at'], + 'updated_at': entities_connect_create_response['updated_at'], + } + + assert entities_connect_retrieve_response == expect_results + + +# ENTITY CREDIT SCORE TESTS + +def test_create_entity_credit_score(): + global entities_create_credit_score_response + entities_create_credit_score_response = method.entities(entities_create_response['id']).credit_scores.create() + + expect_results: EntityCreditScores = { + 'id': entities_create_credit_score_response['id'], + 'entity_id': entities_create_response['id'], + 'status': 'pending', + 'scores': None, + 'error': None, + 'created_at': entities_create_credit_score_response['created_at'], + 'updated_at': entities_create_credit_score_response['updated_at'], + } + + assert entities_create_credit_score_response == expect_results + + +@pytest.mark.asyncio +async def test_retrieve_entity_credit_score(): + def get_credit_score(): + return method.entities(entities_create_response['id']).credit_scores.retrieve(entities_create_credit_score_response['id']) + + credit_score_retrieve_response = await await_results(get_credit_score) + + expect_results: EntityCreditScores = { + 'id': entities_create_credit_score_response['id'], + 'entity_id': entities_create_response['id'], + 'status': 'completed', + 'scores': [ + { + 'score': credit_score_retrieve_response['scores'][0]['score'], + 'source': 'equifax', + 'model': 'vantage_3', + 'factors': credit_score_retrieve_response['scores'][0]['factors'], + 'created_at': credit_score_retrieve_response['scores'][0]['created_at'] + } + ], + 'error': None, + 'created_at': credit_score_retrieve_response['created_at'], + 'updated_at': credit_score_retrieve_response['updated_at'] + } + + assert credit_score_retrieve_response == expect_results + +# ENTITY IDENTITY TESTS + +def test_create_entity_identity(): + global entitiy_with_identity_cap + global entities_create_idenitity_response + + entitiy_with_identity_cap = method.entities.create({ + 'type': 'individual', + 'individual': { + 'first_name': 'Kevin', + 'last_name': 'Doyle', + 'phone': '+16505551115', + } + }) + + entities_create_idenitity_response = method.entities(entitiy_with_identity_cap['id']).identities.create() + + expect_results: EntityIdentity = { + 'id': entities_create_idenitity_response['id'], + 'entity_id': entitiy_with_identity_cap['id'], + 'status': 'completed', + 'identities': [ + { + 'address': { + 'address': '3300 N INTERSTATE 35', + 'city': 'AUSTIN', + 'postal_code': '78705', + 'state': 'TX' + }, + 'dob': '1997-03-18', + 'first_name': 'KEVIN', + 'last_name': 'DOYLE', + 'phone': '+16505551115', + 'ssn': '111223333' + }, + { + 'address': { + 'address': '3300 N INTERSTATE 35', + 'city': 'AUSTIN', + 'postal_code': '78705', + 'state': 'TX' + }, + 'dob': '1997-03-18', + 'first_name': 'KEVIN', + 'last_name': 'DOYLE', + 'phone': '+16505551115', + 'ssn': '123456789' + } + ], + 'error': None, + 'created_at': entities_create_idenitity_response['created_at'], + 'updated_at': entities_create_idenitity_response['updated_at'] + }; + + assert entities_create_idenitity_response == expect_results + + +@pytest.mark.asyncio +async def test_retrieve_entity_identity(): + def get_identity(): + return method.entities(entitiy_with_identity_cap['id']).identities.retrieve(entities_create_idenitity_response['id']) + + identity_retrieve_response = await await_results(get_identity) + + expect_results: EntityIdentity = { + 'id': entities_create_idenitity_response['id'], + 'entity_id': entitiy_with_identity_cap['id'], + 'status': 'completed', + 'identities': [ + { + 'address': { + 'address': '3300 N INTERSTATE 35', + 'city': 'AUSTIN', + 'postal_code': '78705', + 'state': 'TX' + }, + 'dob': '1997-03-18', + 'first_name': 'KEVIN', + 'last_name': 'DOYLE', + 'phone': '+16505551115', + 'ssn': '111223333' + }, + { + 'address': { + 'address': '3300 N INTERSTATE 35', + 'city': 'AUSTIN', + 'postal_code': '78705', + 'state': 'TX' + }, + 'dob': '1997-03-18', + 'first_name': 'KEVIN', + 'last_name': 'DOYLE', + 'phone': '+16505551115', + 'ssn': '123456789' + } + ], + 'error': None, + 'created_at': identity_retrieve_response['created_at'], + 'updated_at': identity_retrieve_response['updated_at'] + }; + + assert identity_retrieve_response == expect_results + +# ENTITY PRODUCT TESTS + +def test_retrieve_entity_product_list(): + global entities_retrieve_product_list_response + entities_retrieve_product_list_response = method.entities(entities_create_response['id']).products.list() + + expect_results: EntityProductListResponse = { + 'connect': { + 'id': entities_retrieve_product_list_response.get('connect', {}).get('id', ''), + 'name': 'connect', + 'status': 'available', + 'status_error': None, + 'latest_request_id': entities_retrieve_product_list_response.get('connect', {}).get('latest_request_id', None), + 'is_subscribable': True, + 'created_at': entities_retrieve_product_list_response.get('connect', {}).get('created_at', ''), + 'updated_at': entities_retrieve_product_list_response.get('connect', {}).get('updated_at', ''), + }, + 'credit_score': { + 'id': entities_retrieve_product_list_response.get('credit_score', {}).get('id', ''), + 'name': 'credit_score', + 'status': 'available', + 'status_error': None, + 'latest_request_id': entities_retrieve_product_list_response.get('credit_score', {}).get('latest_request_id', None), + 'is_subscribable': True, + 'created_at': entities_retrieve_product_list_response.get('credit_score', {}).get('created_at', ''), + 'updated_at': entities_retrieve_product_list_response.get('credit_score', {}).get('updated_at', ''), + }, + 'identity': { + 'id': entities_retrieve_product_list_response.get('identity', {}).get('id', ''), + 'name': 'identity', + 'status': 'available', + 'status_error': None, + 'latest_request_id': entities_retrieve_product_list_response.get('identity', {}).get('latest_request_id', None), + 'is_subscribable': False, + 'created_at': entities_retrieve_product_list_response.get('identity', {}).get('created_at', ''), + 'updated_at': entities_retrieve_product_list_response.get('identity', {}).get('updated_at', ''), + } + } + + assert entities_retrieve_product_list_response == expect_results + + +def test_retrieve_entity_product(): + entity_connect_product_id = entities_retrieve_product_list_response.get('connect', {}).get('id', '') + entity_credit_score_product_id = entities_retrieve_product_list_response.get('credit_score', {}).get('id', '') + entity_identity_product_id = entities_retrieve_product_list_response.get('identity', {}).get('id', '') + + entity_connect_product_response = method.entities(entities_create_response['id']).products.retrieve(entity_connect_product_id) + entity_credit_score_product_response = method.entities(entities_create_response['id']).products.retrieve(entity_credit_score_product_id) + entity_identity_product_response = method.entities(entities_create_response['id']).products.retrieve(entity_identity_product_id) + + expect_connect_results: EntityProduct = { + 'id': entity_connect_product_id, + 'name': 'connect', + 'status': 'available', + 'status_error': None, + 'latest_request_id': entity_connect_product_response['latest_request_id'], + 'is_subscribable': True, + 'created_at': entity_connect_product_response['created_at'], + 'updated_at': entity_connect_product_response['updated_at'] + } + + expect_credit_score_results: EntityProduct = { + 'id': entity_credit_score_product_id, + 'name': 'credit_score', + 'status': 'available', + 'status_error': None, + 'latest_request_id': entity_credit_score_product_response['latest_request_id'], + 'is_subscribable': True, + 'created_at': entity_credit_score_product_response['created_at'], + 'updated_at': entity_credit_score_product_response['updated_at'] + } + + expect_identity_results: EntityProduct = { + 'id': entity_identity_product_id, + 'name': 'identity', + 'status': 'available', + 'status_error': None, + 'latest_request_id': entity_identity_product_response['latest_request_id'], + 'is_subscribable': False, + 'created_at': entity_identity_product_response['created_at'], + 'updated_at': entity_identity_product_response['updated_at'] + } + + assert entity_connect_product_response == expect_connect_results + assert entity_credit_score_product_response == expect_credit_score_results + assert entity_identity_product_response == expect_identity_results + +# ENTITY SUBSCRIPTION TESTS + +def test_create_entity_connect_subscription(): + global entities_create_connect_subscription_response + entities_create_connect_subscription_response = method.entities(entities_create_response['id']).subscriptions.create('connect') + + expect_results: EntitySubscription = { + 'id': entities_create_connect_subscription_response['id'], + 'name': 'connect', + 'status': 'active', + 'latest_request_id': None, + 'created_at': entities_create_connect_subscription_response['created_at'], + 'updated_at': entities_create_connect_subscription_response['updated_at'] + } + + assert entities_create_connect_subscription_response == expect_results + + +def test_create_entity_credit_score_subscription(): + global entities_create_credit_score_subscription_response + entities_create_credit_score_subscription_response = method.entities(entities_create_response['id']).subscriptions.create('credit_score') + + expect_results: EntitySubscription = { + 'id': entities_create_credit_score_subscription_response['id'], + 'name': 'credit_score', + 'status': 'active', + 'latest_request_id': None, + 'created_at': entities_create_credit_score_subscription_response['created_at'], + 'updated_at': entities_create_credit_score_subscription_response['updated_at'] + } + + assert entities_create_credit_score_subscription_response == expect_results + + +def test_retrieve_entity_subscription(): + entity_connect_subscription_id = entities_create_connect_subscription_response['id'] + entity_credit_score_subscription_id = entities_create_credit_score_subscription_response['id'] + + entity_connect_subscription_response = method.entities(entities_create_response['id']).subscriptions.retrieve(entity_connect_subscription_id) + entity_credit_score_subscription_response = method.entities(entities_create_response['id']).subscriptions.retrieve(entity_credit_score_subscription_id) + + expect_connect_results: EntitySubscription = { + 'id': entity_connect_subscription_id, + 'name': 'connect', + 'status': 'active', + 'latest_request_id': None, + 'created_at': entity_connect_subscription_response['created_at'], + 'updated_at': entity_connect_subscription_response['updated_at'] + } + + expect_credit_score_results: EntitySubscription = { + 'id': entity_credit_score_subscription_id, + 'name': 'credit_score', + 'status': 'active', + 'latest_request_id': None, + 'created_at': entity_credit_score_subscription_response['created_at'], + 'updated_at': entity_credit_score_subscription_response['updated_at'] + } + + assert entity_connect_subscription_response == expect_connect_results + assert entity_credit_score_subscription_response == expect_credit_score_results + +# ENTITY CONSENT TESTS + +def test_withdraw_entity_consent(): + entity_withdraw_consent_response = method.entities.withdraw_consent(entities_create_response['id']) + + expect_results: Entity = { + 'id': entities_create_response['id'], + 'type': None, + 'individual': None, + 'verification': None, + 'error': { + 'type': 'ENTITY_DISABLED', + 'sub_type': 'ENTITY_CONSENT_WITHDRAWN', + 'code': 12004, + 'message': 'Entity was disabled due to consent withdrawal.' + }, + 'address': {}, + 'status': 'disabled', + 'metadata': None, + 'created_at': entities_update_response['created_at'], + 'updated_at': entity_withdraw_consent_response['updated_at'], + } + + assert entity_withdraw_consent_response == expect_results + \ No newline at end of file diff --git a/test/resources/Merchant_test.py b/test/resources/Merchant_test.py new file mode 100644 index 0000000..3bb84bb --- /dev/null +++ b/test/resources/Merchant_test.py @@ -0,0 +1,75 @@ +import os +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) + +merchant_retrieve_response = None +merchants_list_response = None +amex_mch_id = 'mch_3' +amex_provider_id_plaid = 'ins_10' + + +def test_retrieve_merchant(): + global merchant_retrieve_response + + merchant_retrieve_response = method.merchants.retrieve(amex_mch_id) + + expect_results = { + "id": "mch_3", + "parent_name": "American Express", + "name": "American Express - Credit Cards", + "logo": "https://static.methodfi.com/mch_logos/mch_3.png", + "type": "credit_card", + "provider_ids": { + "plaid": ["ins_10"], + "mx": ["amex"], + "finicity": [], + "dpp": ["120", "18954427", "11859365", "18947131", "16255844"] + }, + "is_temp": False, + "account_number_formats": [] + } + + assert merchant_retrieve_response == expect_results + + +def test_list_merchants(): + global merchants_list_response + + merchants_list_response = method.merchants.list({ 'provider_id.plaid': amex_provider_id_plaid }) + merchant_to_use = merchants_list_response[0] + + expect_results = { + "id": "mch_300485", + "parent_name": "American Express", + "name": "American Express Credit Card", + "logo": "https://static.methodfi.com/mch_logos/mch_300485.png", + "type": "credit_card", + "provider_ids": { + "plaid": ["ins_10"], + "mx": ["amex"], + "finicity": [], + "dpp": [ + '7929257', + '120', + '18391555', + '18954427', + '11859365', + '18947131', + '16255844' + ] + }, + "is_temp": False, + "account_number_formats": [ + '###############' + ] + } + + assert merchants_list_response != None + assert isinstance(merchants_list_response, list) + assert merchant_to_use == expect_results diff --git a/test/resources/Payment_test.py b/test/resources/Payment_test.py new file mode 100644 index 0000000..2f5de4f --- /dev/null +++ b/test/resources/Payment_test.py @@ -0,0 +1,174 @@ +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) + +payments_create_response = None +payments_retrieve_response = None +payments_list_response = None +payments_delete_response = None + +@pytest.fixture(scope='module') +def setup(): + holder_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', + } + }) + + phone_verification_response = method.entities(holder_1_response['id']).verification_sessions.create({ + 'type': 'phone', + 'method': 'byo_sms', + 'byo_sms': { + 'timestamp': '2021-09-01T00:00:00.000Z' + } + }) + + identity_verification_response = method.entities(holder_1_response['id']).verification_sessions.create({ + 'type': 'identity', + 'method': 'kba', + 'kba': {} + }) + + source_1_response = method.accounts.create({ + 'holder_id': holder_1_response['id'], + 'ach': { + 'routing': '062103000', + 'number': '123456789', + 'type': 'checking', + } + }) + + destination_1_response = method.accounts.create({ + 'holder_id': holder_1_response['id'], + 'liability': { + 'mch_id': 'mch_3', + 'account_number': '123456789', + } + }) + + return { + 'holder_1_id': holder_1_response['id'], + 'source_1_id': source_1_response['id'], + 'destination_1_id': destination_1_response['id'], + } + + +def test_create_payment(setup): + global payments_create_response + + payments_create_response = method.payments.create({ + 'amount': 5000, + 'source': setup['source_1_id'], + 'destination': setup['destination_1_id'], + 'description': 'MethodPy' + }) + + expect_results = { + 'id': payments_create_response['id'], + 'source': setup['source_1_id'], + 'destination': setup['destination_1_id'], + 'amount': 5000, + 'description': 'MethodPy', + 'status': 'pending', + 'estimated_completion_date': payments_create_response['estimated_completion_date'], + 'source_trace_id': None, + 'source_settlement_date': payments_create_response['source_settlement_date'], + 'source_status': 'pending', + 'destination_trace_id': None, + 'destination_settlement_date': payments_create_response['destination_settlement_date'], + 'destination_status': 'pending', + 'reversal_id': None, + 'fee': None, + 'type': 'standard', + 'error': None, + 'metadata': None, + 'created_at': payments_create_response['created_at'], + 'updated_at': payments_create_response['updated_at'], + } + + + assert payments_create_response == expect_results + + +def test_retrieve_payment(setup): + global payments_retrieve_response + + payments_retrieve_response = method.payments.retrieve(payments_create_response['id']) + + expect_results = { + 'id': payments_create_response['id'], + 'source': setup['source_1_id'], + 'destination': setup['destination_1_id'], + 'amount': 5000, + 'description': 'MethodPy', + 'status': 'pending', + 'estimated_completion_date': payments_create_response['estimated_completion_date'], + 'source_trace_id': None, + 'source_settlement_date': payments_create_response['source_settlement_date'], + 'source_status': 'pending', + 'destination_trace_id': None, + 'destination_settlement_date': payments_create_response['destination_settlement_date'], + 'destination_status': 'pending', + 'reversal_id': None, + 'fee': None, + 'type': 'standard', + 'error': None, + 'metadata': None, + 'created_at': payments_retrieve_response['created_at'], + 'updated_at': payments_retrieve_response['updated_at'], + 'fund_status': 'pending' + } + + assert payments_retrieve_response == expect_results + + +def test_list_payments(setup): + global payments_list_response + + payments_list_response = method.payments.list({ 'source': setup['source_1_id'] }) + payment_ids = [payment['id'] for payment in payments_list_response] + + assert payments_create_response['id'] in payment_ids + + +def test_delete_payment(setup): + global payments_delete_response + + payments_delete_response = method.payments.delete(payments_create_response['id']) + + expect_results = { + 'id': payments_create_response['id'], + 'source': setup['source_1_id'], + 'destination': setup['destination_1_id'], + 'amount': 5000, + 'description': 'MethodPy', + 'status': 'canceled', + 'estimated_completion_date': payments_create_response['estimated_completion_date'], + 'source_trace_id': None, + 'source_settlement_date': payments_create_response['source_settlement_date'], + 'source_status': 'canceled', + 'destination_trace_id': None, + 'destination_settlement_date': payments_create_response['destination_settlement_date'], + 'destination_status': 'canceled', + 'reversal_id': None, + 'fee': None, + 'type': 'standard', + 'error': None, + 'metadata': None, + 'created_at': payments_delete_response['created_at'], + 'updated_at': payments_delete_response['updated_at'], + } + + assert payments_delete_response == expect_results diff --git a/test/resources/Report_test.py b/test/resources/Report_test.py new file mode 100644 index 0000000..939dab1 --- /dev/null +++ b/test/resources/Report_test.py @@ -0,0 +1,59 @@ +import os +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) + +report_create_response = None +report_retrieve_response = None +report_download_response = None + + +def test_create_report(): + global report_create_response + + report_create_response = method.reports.create({ + 'type': 'payments.created.current', + }) + + expect_results = { + "id": report_create_response["id"], + "type": "payments.created.current", + "url": f"https://dev.methodfi.com/reports/{report_create_response['id']}/download", + "status": "completed", + "metadata": None, + "created_at": report_create_response["created_at"], + "updated_at": report_create_response["updated_at"], + } + + assert report_create_response == expect_results + + +def test_retrieve_report(): + global report_retrieve_response + + report_retrieve_response = method.reports.retrieve(report_create_response['id']) + + expect_results = { + "id": report_create_response["id"], + "type": "payments.created.current", + "url": f"https://dev.methodfi.com/reports/{report_create_response['id']}/download", + "status": "completed", + "metadata": None, + "created_at": report_retrieve_response["created_at"], + "updated_at": report_retrieve_response["updated_at"], + } + + assert report_retrieve_response == expect_results + + +def test_download_report(): + global report_download_response + + report_download_response = method.reports.download(report_create_response['id']) + + assert report_download_response is not None diff --git a/test/resources/Webhook_test.py b/test/resources/Webhook_test.py new file mode 100644 index 0000000..889bf93 --- /dev/null +++ b/test/resources/Webhook_test.py @@ -0,0 +1,68 @@ +import os +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) + +webhooks_create_response = None +webhooks_retrieve_response = None +webhooks_list_response = None +webhooks_delete_response = None + +def test_create_webhooks(): + global webhooks_create_response + + webhooks_create_response = method.webhooks.create({ + 'type': 'payment.create', + 'url': 'https://dev.methodfi.com', + 'auth_token': 'test_auth_token' + }) + + expect_results = { + 'id': webhooks_create_response['id'], + 'type': 'payment.create', + 'url': 'https://dev.methodfi.com', + 'metadata': None, + 'created_at': webhooks_create_response['created_at'], + 'updated_at': webhooks_create_response['updated_at'] + } + + assert webhooks_create_response == expect_results + + +def test_retrieve_webhook(): + global webhooks_retrieve_response + + webhooks_retrieve_response = method.webhooks.retrieve(webhooks_create_response['id']) + + expect_results = { + 'id': webhooks_create_response['id'], + 'type': 'payment.create', + 'url': 'https://dev.methodfi.com', + 'metadata': None, + 'created_at': webhooks_retrieve_response['created_at'], + 'updated_at': webhooks_retrieve_response['updated_at'] + } + + assert webhooks_retrieve_response == expect_results + + +def test_list_webhooks(): + global webhooks_list_response + + webhooks_list_response = method.webhooks.list() + webhook_ids = [webhook['id'] for webhook in webhooks_list_response] + + assert webhooks_create_response['id'] in webhook_ids + + +def test_delete_webhook(): + global webhooks_delete_response + + webhooks_delete_response = method.webhooks.delete(webhooks_create_response['id']) + + assert webhooks_delete_response == None diff --git a/test/resources/utils.py b/test/resources/utils.py new file mode 100644 index 0000000..9695d9b --- /dev/null +++ b/test/resources/utils.py @@ -0,0 +1,20 @@ +import asyncio + +async def sleep(ms: int): + await asyncio.sleep(ms / 1000) # Convert milliseconds to seconds + +async def await_results(fn): + result = None + retries = 10 + while retries > 0: + try: + result = fn() + if result['status'] in ['completed', 'failed']: + break + await sleep(5000) + except Exception as error: + print('Error occurred while retrieving account balances:', error) + raise error # Rethrow the error to fail the test + retries -= 1 + + return result