diff --git a/.cursor/rules/python-sdk.mdc b/.cursor/rules/python-sdk.mdc new file mode 100644 index 00000000..7aad930c --- /dev/null +++ b/.cursor/rules/python-sdk.mdc @@ -0,0 +1,34 @@ +--- +alwaysApply: true +description: Coding conventions for the Unit Python SDK fork +--- + +## Unit Python SDK Conventions + +This is a fork of the Unit.co Python SDK. Follow the same Python style conventions as the API repo (keyword arguments, no docstrings, comments explain why not what). + +### Request Types + +- Every request type must implement `to_json_api()` returning a JSON API payload (`{"data": {"type": "...", "attributes": {...}}}`) +- Use keyword arguments in constructors +- Optional fields should default to `None` and be excluded from the serialized payload when not set + +### DTO Types + +- DTOs are response types instantiated by `DtoDecoder.decode()` from JSON API responses +- Each DTO has an `id` and an `attributes` dict +- DTOs are dataclass-style objects (plain `__init__` with attribute assignment) + +### Codec Registration + +When adding a new Unit API type: +1. Create the DTO class in `unit/models/` +2. Create the request class with `to_json_api()` in the same file +3. Add the JSON API type string -> DTO class mapping to `mappings` in `unit/models/codecs.py` +4. Add resource methods in `unit/api/` if needed + +### Resource Classes + +- All resource classes inherit from `BaseResource` (`unit/api/base_resource.py`) +- Use `self._post()`, `self._get()`, `self._patch()` etc. from BaseResource +- Resource methods return `UnitResponse` wrapper objects diff --git a/.cursor/skills/local-install/SKILL.md b/.cursor/skills/local-install/SKILL.md new file mode 100644 index 00000000..d8634b0b --- /dev/null +++ b/.cursor/skills/local-install/SKILL.md @@ -0,0 +1,14 @@ +--- +name: truss-unit-sdk-local-install +description: Install the local unit-python-sdk fork into the API virtualenv in editable mode so SDK changes are immediately reflected. Use when modifying unit-python-sdk, adding new SDK types, or after any edit to unit-python-sdk/unit/models/ that needs to be picked up by the API. +--- + +# Truss Unit SDK Local Install + +After modifying anything in `unit-python-sdk/`, run this to make the API venv pick up the changes: + +```bash +/Users/averykushner/cursor-projects/truss-fullstack/api/.venv/bin/pip install -e /Users/averykushner/cursor-projects/truss-fullstack/unit-python-sdk +``` + +The `-e` flag installs in editable mode — the venv symlinks to the local source, so subsequent edits to the SDK are reflected immediately without reinstalling. Only run this command again if the venv is recreated or the SDK package metadata changes (e.g. `pyproject.toml`). diff --git a/.cursor/skills/sdk-guide/SKILL.md b/.cursor/skills/sdk-guide/SKILL.md new file mode 100644 index 00000000..8439ce6c --- /dev/null +++ b/.cursor/skills/sdk-guide/SKILL.md @@ -0,0 +1,281 @@ +--- +name: truss-unit-sdk +description: Guide to the Truss Unit Python SDK fork covering the resource-based API pattern, application DTOs and request types, JSON API codec system, and how the API repo interfaces with the SDK via unit_api_call. Use when updating the Unit SDK, adding new SDK types, modifying application submission, working with Unit API responses, or preparing for Unit API version migrations. +--- + +# Truss Unit Python SDK + +## Overview + +Truss maintains a fork of the Unit Python SDK at `unit-python-sdk/`. This is a typed REST client for the [Unit.co](https://www.unit.co) banking API. The Truss API repo (`api/`) imports this SDK and wraps all calls through `unit_api_call()`. + +## Project Structure + +``` +unit-python-sdk/ +├── unit/ +│ ├── __init__.py # Unit client class — exposes resource accessors +│ ├── api/ # Resource classes (REST endpoint wrappers) +│ │ ├── base_resource.py # HTTP methods, auth headers, JSON API encoding +│ │ ├── application_resource.py # applications.create(), .get(), .list(), .update() +│ │ └── [30+ other resources] # payments, accounts, customers, etc. +│ ├── models/ # DTOs, request types, response types +│ │ ├── application.py # Application DTOs + Create/Update request types +│ │ ├── codecs.py # DtoDecoder — JSON API type → DTO class mapping +│ │ └── [30+ other model files] +│ └── utils/ # Utility functions +├── e2e_tests/ +│ └── application_test.py # Application e2e tests +└── setup.py # Package version (currently 0.10.5) +``` + +## SDK Architecture + +### Request → API → Response Flow + +``` +1. Build Request Object (e.g., CreateBusinessApplicationRequest) + │ + ↓ .to_json_api() +2. JSON API Payload {"data": {"type": "businessApplication", "attributes": {...}}} + │ + ↓ BaseResource.post(url, payload) +3. HTTP POST POST {api_url}/applications + │ Headers: Content-Type: application/vnd.api+json + │ Authorization: Bearer {token} + ↓ +4. Raw JSON Response {"data": {"type": "businessApplication", "id": "...", "attributes": {...}}} + │ + ↓ DtoDecoder.decode(response) +5. Typed DTO BusinessApplicationDTO(id=..., attributes=...) + │ + ↓ UnitResponse(data=dto, included=[...]) +6. UnitResponse Wrapper Final response with typed data + included resources +``` + +### BaseResource (`unit/api/base_resource.py`) + +All resource classes inherit from `BaseResource`. It provides: + +- `get(url)`, `post(url, data)`, `patch(url, data)`, `put(url, data)`, `delete(url)` +- Sets `Content-Type: application/vnd.api+json` and `Authorization: Bearer {token}` +- Serializes request objects via `UnitEncoder` (handles `to_json_api()` on request objects) +- Returns raw response dict for decoding + +### Unit Client (`unit/__init__.py`) + +The main `Unit` class instantiates all resource accessors: + +```python +class Unit: + def __init__(self, api_url, token): + self.applications = ApplicationResource(api_url, token) + self.payments = PaymentResource(api_url, token) + self.accounts = AccountResource(api_url, token) + # ... 30+ more resources +``` + +## Application Types + +### DTOs (Response Types) + +| Class | JSON API Type | Description | +|-------|---------------|-------------| +| `IndividualApplicationDTO` | `"individualApplication"` | Individual/sole prop application (includes `sole_proprietorship` flag) | +| `BusinessApplicationDTO` | `"businessApplication"` | Business application | + +Both are dataclass-style objects with an `id` (Unit's application ID) and `attributes` dict containing all application fields. + +### Request Types + +| Class | Creates | Key Fields | +|-------|---------|------------| +| `CreateIndividualApplicationRequest` | Individual application | full_name, dob, address, email, phone, ssn, sole_proprietorship, ip, tags, idempotency_key | +| `CreateBusinessApplicationRequest` | Business application | name, address, phone, ein, entity_type, state_of_incorporation, contact, officer, beneficial_owners, tags, idempotency_key | + +### Sole Prop as Individual + +Sole proprietorships are **not** a separate type. They use `CreateIndividualApplicationRequest` with `sole_proprietorship=True` and additional business-related fields: + +```python +CreateIndividualApplicationRequest( + full_name=FullName(first="John", last="Doe"), + sole_proprietorship=True, + # ... individual fields plus business fields +) +``` + +### `to_json_api()` — Serialization + +Each request type implements `to_json_api()` which returns the JSON API payload: + +```python +def to_json_api(self): + return { + "data": { + "type": "individualApplication", # or "businessApplication" + "attributes": { + # All fields serialized here + } + } + } +``` + +This is called automatically by `UnitEncoder` during serialization in `BaseResource.post()`. + +## Codec System (`unit/models/codecs.py`) + +### Type Mapping + +`DtoDecoder.decode()` uses a `mappings` dict to route JSON API responses to the correct DTO class: + +```python +mappings = { + "individualApplication": IndividualApplicationDTO, + "businessApplication": BusinessApplicationDTO, + # ... 50+ other type mappings +} +``` + +When a response comes back with `"type": "businessApplication"`, the decoder instantiates `BusinessApplicationDTO` with the response data. + +### Adding New Types + +To support a new Unit API type: + +1. Create the DTO class in the appropriate `unit/models/*.py` file +2. Create the request class with `to_json_api()` method +3. Add the type string → DTO class mapping to `mappings` in `unit/models/codecs.py` +4. If needed, add a new resource method or resource class in `unit/api/` + +## Sub-types Used in Applications + +### FullName +```python +FullName(first="John", last="Doe") +``` + +### Phone +```python +Phone(country_code="1", number="5551234567") +``` + +### Address (UnitAddress in Truss API) +```python +Address(street="123 Main St", city="Austin", state="TX", postal_code="78701", country="US") +``` + +### Officer +```python +Officer( + full_name=FullName(...), + date_of_birth=date(...), + address=Address(...), + phone=Phone(...), + email="...", + ssn="...", # or passport/nationality for non-US + title="CEO", +) +``` + +### BeneficialOwner +```python +BeneficialOwner( + full_name=FullName(...), + date_of_birth=date(...), + address=Address(...), + phone=Phone(...), + email="...", + ssn="...", + percentage=50, # 25-100 +) +``` + +### BusinessContact +```python +BusinessContact( + full_name=FullName(...), + phone=Phone(...), + email="...", +) +``` + +## How the API Repo Interfaces with the SDK + +For the `unit_api_call()` wrapper pattern, error mapping, and client initialization details, see the `architecture` skill at `api/.cursor/skills/architecture/SKILL.md`. + +### Model-to-SDK Conversion + +The conversion from Django models to SDK request types happens in `api/src/api/operations/account/onboarding/application/submit_onboarding_application.py`. See the `truss-onboarding-architecture` skill for the complete field mapping. + +## Subagents + +### trace-sdk-application-types: Map SDK application types and fields + +**Type:** `explore` +**Model:** *(default)* +**When:** You need to understand the complete structure of Unit SDK application types — every field, sub-type, and serialization detail — typically before modifying the SDK for a new Unit API version. + +**Prompt:** + +> Map the complete structure of application types in the Truss Unit Python SDK fork at `unit-python-sdk/`. +> +> 1. **Request types**: Read `unit-python-sdk/unit/models/application.py`. For each request class (`CreateIndividualApplicationRequest`, `CreateBusinessApplicationRequest`): +> - Document every field with its type and whether it's required or optional +> - Read the `to_json_api()` method to understand exactly how each field is serialized +> - Note any conditional fields (e.g., sole_proprietorship flag changes which fields are included) +> +> 2. **DTO types**: In the same file, read `IndividualApplicationDTO` and `BusinessApplicationDTO`: +> - Document the response structure (id, type, attributes) +> - Document how the DTO is constructed from JSON API responses +> +> 3. **Sub-types**: Find and read the definitions of `FullName`, `Phone`, `Address`, `Officer`, `BeneficialOwner`, `BusinessContact`. These may be in `unit/models/application.py` or imported from other model files. Document their fields. +> +> 4. **Codec mapping**: Read `unit-python-sdk/unit/models/codecs.py`: +> - Find the `mappings` dict +> - Document which type strings map to which DTO classes for applications +> - Understand how `DtoDecoder.decode()` routes responses +> +> 5. **Resource methods**: Read `unit-python-sdk/unit/api/application_resource.py`: +> - Document each method (create, get, list, update, upload, approve_sb) +> - Note the URL patterns used for each method +> - Document return types +> +> Return: +> - Complete field schema for each request type (field name, type, required/optional, serialized JSON key) +> - Complete field schema for each DTO type +> - Sub-type field schemas +> - Codec type string → class mapping for applications +> - Resource method signatures and URL patterns + +### audit-sdk-for-v2-migration: Identify SDK changes needed for Unit API v2 + +**Type:** `explore` +**Model:** *(default)* +**When:** You are preparing to update the Unit SDK fork to support a new version of the Unit applications API and need to identify what must change. + +**Prompt:** + +> Audit the Truss Unit Python SDK fork for changes needed to support Unit Applications API v2. The SDK is at `unit-python-sdk/`. +> +> 1. **Current application code**: Read these files completely: +> - `unit-python-sdk/unit/models/application.py` — all DTOs and request types +> - `unit-python-sdk/unit/api/application_resource.py` — all API methods +> - `unit-python-sdk/unit/models/codecs.py` — type mappings +> - `unit-python-sdk/unit/__init__.py` — client resource initialization +> +> 2. **SDK patterns for other resources**: Read 2-3 other resource/model pairs to understand how the SDK handles different resource types. This helps identify reusable patterns: +> - Pick resources that have complex nested types (like `payment.py` or `account.py`) +> +> 3. **Test patterns**: Read `unit-python-sdk/e2e_tests/application_test.py` to understand how application tests are structured. +> +> 4. **How the API repo consumes the SDK**: Read these files to understand what the API repo depends on: +> - `api/src/api/operations/account/onboarding/application/submit_onboarding_application.py` +> - `api/src/api/utils/integrations/unit/unit.py` (unit_api_call wrapper) +> +> Return: +> - List of files that need modification +> - For each file, what specifically needs to change (new classes, modified methods, new mappings) +> - Backward compatibility concerns (can v1 and v2 coexist?) +> - Recommended approach: separate v2 classes vs. version parameter vs. extending existing classes +> - API repo callsites that reference SDK types and would need updating diff --git a/.gitignore b/.gitignore index 813da845..09a47dab 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ venv/* dist/ .DS_Store + +build/ \ No newline at end of file diff --git a/unit/__init__.py b/unit/__init__.py index 8076f8c6..70d057fd 100644 --- a/unit/__init__.py +++ b/unit/__init__.py @@ -1,9 +1,15 @@ from unit.api.application_resource import ApplicationResource +from unit.api.check_deposit_resource import CheckDepositResource +from unit.api.batch_release_resource import BatchReleaseResource +from unit.api.check_payment_resource import CheckPaymentResource +from unit.api.check_stop_payment_resource import CheckStopPaymentResource from unit.api.customer_resource import CustomerResource from unit.api.account_resource import AccountResource from unit.api.card_resource import CardResource from unit.api.transaction_resource import TransactionResource from unit.api.payment_resource import PaymentResource +from unit.api.repayment_resource import RepaymentResource +from unit.api.ach_resource import AchResource from unit.api.statement_resource import StatementResource from unit.api.customerToken_resource import CustomerTokenResource from unit.api.counterparty_resource import CounterpartyResource @@ -19,6 +25,11 @@ from unit.api.authorization_resource import AuthorizationResource from unit.api.authorization_request_resource import AuthorizationRequestResource from unit.api.account_end_of_day_resource import AccountEndOfDayResource +from unit.api.accrued_interest_resource import AccruedInterestResource +from unit.api.reward_resource import RewardResource +from unit.api.dispute_resource import DisputeResource +from unit.api.received_payment_resource import ReceivedPaymentResource +from unit.api.check_registered_address import CheckRegisteredAddressResource __all__ = ["api", "models", "utils"] @@ -31,6 +42,9 @@ def __init__(self, api_url, token): self.cards = CardResource(api_url, token) self.transactions = TransactionResource(api_url, token) self.payments = PaymentResource(api_url, token) + self.check_deposits = CheckDepositResource(api_url, token) + self.repayments = RepaymentResource(api_url, token) + self.ach = AchResource(api_url, token) self.statements = StatementResource(api_url, token) self.customerTokens = CustomerTokenResource(api_url, token) self.counterparty = CounterpartyResource(api_url, token) @@ -46,3 +60,11 @@ def __init__(self, api_url, token): self.authorizations = AuthorizationResource(api_url, token) self.authorization_requests = AuthorizationRequestResource(api_url, token) self.account_end_of_day = AccountEndOfDayResource(api_url, token) + self.accrued_interest = AccruedInterestResource(api_url, token) + self.rewards = RewardResource(api_url, token) + self.batchRelease = BatchReleaseResource(api_url, token) + self.check_payments = CheckPaymentResource(api_url, token) + self.check_stop_payments = CheckStopPaymentResource(api_url, token) + self.disputes = DisputeResource(api_url, token) + self.received_payments = ReceivedPaymentResource(api_url, token) + self.check_registered_address = CheckRegisteredAddressResource(api_url, token) diff --git a/unit/api/account_resource.py b/unit/api/account_resource.py index 06fb60bd..26c35056 100644 --- a/unit/api/account_resource.py +++ b/unit/api/account_resource.py @@ -7,7 +7,7 @@ def __init__(self, api_url, token): super().__init__(api_url, token) self.resource = "accounts" - def create(self, request: CreateDepositAccountRequest) -> Union[UnitResponse[AccountDTO], UnitError]: + def create(self, request: CreateAccountRequest) -> Union[UnitResponse[AccountDTO], UnitError]: payload = request.to_json_api() response = super().post(self.resource, payload) if super().is_20x(response.status_code): @@ -33,6 +33,23 @@ def reopen_account(self, account_id: str, reason: str = "ByCustomer") -> Union[U else: return UnitError.from_json_api(response.json()) + def freeze_account(self, request: FreezeAccountRequest) -> Union[UnitResponse[AccountDTO], UnitError]: + payload = request.to_json_api() + response = super().post(f"{self.resource}/{request.account_id}/freeze", payload) + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[AccountDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) + + def unfreeze_account(self, account_id: str) -> Union[UnitResponse[AccountDTO], UnitError]: + response = super().post(f"{self.resource}/{account_id}/unfreeze") + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[AccountDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) + def get(self, account_id: str, include: Optional[str] = "") -> Union[UnitResponse[AccountDTO], UnitError]: response = super().get(f"{self.resource}/{account_id}", {"include": include}) if super().is_20x(response.status_code): @@ -52,7 +69,7 @@ def list(self, params: ListAccountParams = None) -> Union[UnitResponse[List[Acco else: return UnitError.from_json_api(response.json()) - def update(self, request: PatchDepositAccountRequest) -> Union[UnitResponse[AccountDTO], UnitError]: + def update(self, request: PatchAccountRequest) -> Union[UnitResponse[AccountDTO], UnitError]: payload = request.to_json_api() response = super().patch(f"{self.resource}/{request.account_id}", payload) if super().is_20x(response.status_code): diff --git a/unit/api/accrued_interest_resource.py b/unit/api/accrued_interest_resource.py new file mode 100644 index 00000000..218e3cd8 --- /dev/null +++ b/unit/api/accrued_interest_resource.py @@ -0,0 +1,18 @@ +from unit.api.base_resource import BaseResource +from unit.models.accrued_interest import * +from unit.models.codecs import DtoDecoder + + +class AccruedInterestResource(BaseResource): + def __init__(self, api_url, token): + super().__init__(api_url, token) + self.resource = "accrued-interest" + + def total(self, params: GetAccruedInterestTotalParams = None) -> Union[UnitResponse[AccruedInterestTotalDTO], UnitError]: + params = params or GetAccruedInterestTotalParams() + response = super().get(f"{self.resource}/total", params.to_dict()) + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[AccruedInterestTotalDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) diff --git a/unit/api/ach_resource.py b/unit/api/ach_resource.py new file mode 100644 index 00000000..f19835db --- /dev/null +++ b/unit/api/ach_resource.py @@ -0,0 +1,34 @@ +from unit.api.base_resource import BaseResource +from unit.models.payment import * +from unit.models.codecs import DtoDecoder, split_json_api_single_response + + +class AchResource(BaseResource): + def __init__(self, api_url, token): + super().__init__(api_url, token) + self.resource = "ach" + + def simulate_transmit(self, request: SimulateTransmitAchRequest) -> Union[UnitResponse[PaymentDTO], UnitError]: + payload = request.to_json_api() + response = super().post(f"sandbox/{self.resource}/transmit", payload) + if super().is_20x(response.status_code): + data = response.json().get("data") + # TODO Fix dto + _id, _type, attributes, relationships = split_json_api_single_response(data) + print("simulate_transmit") + print("data", data) + print("_id, _type, attributes, relationships", _id, _type, attributes, relationships) + return UnitResponse[SimulateAchPaymentDTO](SimulateAchPaymentDTO.from_json_api(_id, _type, attributes, relationships), None) + else: + return UnitError.from_json_api(response.json()) + + def simulate_clear(self, request: SimulateClearAchRequest) -> Union[UnitResponse[PaymentDTO], UnitError]: + payload = request.to_json_api() + response = super().post(f"sandbox/{self.resource}/clear", payload) + if super().is_20x(response.status_code): + data = response.json().get("data") + # TODO Fix dto + _id, _type, attributes, relationships = split_json_api_single_response(data) + return UnitResponse[SimulateAchPaymentDTO](SimulateAchPaymentDTO.from_json_api(_id, _type, attributes, relationships), None) + else: + return UnitError.from_json_api(response.json()) diff --git a/unit/api/applicationForm_resource.py b/unit/api/applicationForm_resource.py index 3ef83f3e..0aa12c4a 100644 --- a/unit/api/applicationForm_resource.py +++ b/unit/api/applicationForm_resource.py @@ -10,7 +10,10 @@ def __init__(self, api_url, token): def create(self, request: CreateApplicationFormRequest) -> Union[UnitResponse[ApplicationFormDTO], UnitError]: payload = request.to_json_api() - response = super().post(self.resource, payload) + response = super().post( + self.resource, + payload + ) if super().is_20x(response.status_code): data = response.json().get("data") return UnitResponse[ApplicationFormDTO](DtoDecoder.decode(data), None) diff --git a/unit/api/application_resource.py b/unit/api/application_resource.py index 7156c7d3..82d041e7 100644 --- a/unit/api/application_resource.py +++ b/unit/api/application_resource.py @@ -71,3 +71,20 @@ def update(self, request: PatchApplicationRequest) -> Union[UnitResponse[Applica else: return UnitError.from_json_api(response.json()) + def approve_sb(self, request: ApproveApplicationSBRequest): + url = f"sandbox/{self.resource}/{request.application_id}/approve" + + payload = request.to_json_api() + response = super().post(url, payload) + + if response.ok: + data = response.json().get("data") + included = response.json().get("included") + return UnitResponse(data, included) + # TODO need DTOs for this response + # if data["type"] == "individualApplication": + # return UnitResponse[IndividualApplicationDTO](DtoDecoder.decode(data), DtoDecoder.decode(included)) + # else: + # return UnitResponse[BusinessApplicationDTO](DtoDecoder.decode(data), DtoDecoder.decode(included)) + else: + return UnitError.from_json_api(response.json()) diff --git a/unit/api/authorization_request_resource.py b/unit/api/authorization_request_resource.py index 56815ee8..dc8dca7b 100644 --- a/unit/api/authorization_request_resource.py +++ b/unit/api/authorization_request_resource.py @@ -44,3 +44,11 @@ def decline(self, request: DeclineAuthorizationRequest) -> Union[UnitResponse[Pu else: return UnitError.from_json_api(response.json()) + def sandbox_simulate(self, request: SimulateAuthorizationRequest) -> Union[UnitResponse[PurchaseAuthorizationRequestDTO], UnitError]: + payload = request.to_json_api() + response = super().post(f"sandbox/{self.resource}/purchase", payload) + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[PurchaseAuthorizationRequestDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) diff --git a/unit/api/authorization_resource.py b/unit/api/authorization_resource.py index 28a3fec7..3991bce1 100644 --- a/unit/api/authorization_resource.py +++ b/unit/api/authorization_resource.py @@ -25,4 +25,4 @@ def list(self, params: ListAuthorizationParams = None) -> Union[UnitResponse[Lis data = response.json().get("data") return UnitResponse[AuthorizationDTO](DtoDecoder.decode(data), None) else: - return UnitError.from_json_api(response.json()) + return UnitError.from_json_api(response.json()) \ No newline at end of file diff --git a/unit/api/base_resource.py b/unit/api/base_resource.py index b8b06547..2cab48ef 100644 --- a/unit/api/base_resource.py +++ b/unit/api/base_resource.py @@ -14,22 +14,22 @@ def __init__(self, api_url, token): "user-agent": "unit-python-sdk" } - def get(self, resource: str, params: Dict = None, headers: Optional[Dict[str, str]] = None): - return requests.get(f"{self.api_url}/{resource}", params=params, headers=self.__merge_headers(headers)) + def get(self, resource: str, params: Dict = None, headers: Optional[Dict[str, str]] = None, timeout: float = None): + return requests.get(f"{self.api_url}/{resource}", params=params, headers=self.__merge_headers(headers), timeout=timeout) - def post(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None): + def post(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None, timeout: float = None): data = json.dumps(data, cls=UnitEncoder) if data is not None else None - return requests.post(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers)) + return requests.post(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers), timeout=timeout) - def patch(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None): + def patch(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None, timeout: float = None): data = json.dumps(data, cls=UnitEncoder) if data is not None else None - return requests.patch(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers)) + return requests.patch(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers), timeout=timeout) - def delete(self, resource: str, params: Dict = None, headers: Optional[Dict[str, str]] = None): - return requests.delete(f"{self.api_url}/{resource}", params=params, headers=self.__merge_headers(headers)) + def delete(self, resource: str, params: Dict = None, headers: Optional[Dict[str, str]] = None, timeout: float = None): + return requests.delete(f"{self.api_url}/{resource}", params=params, headers=self.__merge_headers(headers), timeout=timeout) - def put(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None): - return requests.put(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers)) + def put(self, resource: str, data: Optional[Dict] = None, headers: Optional[Dict[str, str]] = None, timeout: float = None): + return requests.put(f"{self.api_url}/{resource}", data=data, headers=self.__merge_headers(headers), timeout=timeout) def __merge_headers(self, headers: Optional[Dict[str, str]] = None): if not headers: diff --git a/unit/api/batch_release_resource.py b/unit/api/batch_release_resource.py new file mode 100644 index 00000000..e8928741 --- /dev/null +++ b/unit/api/batch_release_resource.py @@ -0,0 +1,17 @@ +from unit.api.base_resource import BaseResource +from unit.models.batch_release import * +from unit.models.codecs import DtoDecoder + + +class BatchReleaseResource(BaseResource): + def __init__(self, api_url, token): + super().__init__(api_url, token) + self.resource = 'batch-releases' + + def create(self, batch_releases: List[BatchReleaseDTO]) -> Union[UnitResponse[List[BatchReleaseDTO]], UnitError]: + response = super().post(self.resource, {"data": batch_releases}) + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[List[BatchReleaseDTO]](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) diff --git a/unit/api/check_deposit_resource.py b/unit/api/check_deposit_resource.py new file mode 100644 index 00000000..53f8e779 --- /dev/null +++ b/unit/api/check_deposit_resource.py @@ -0,0 +1,31 @@ +from unit.api.base_resource import BaseResource +from unit.models.check_deposit import * +from unit.models.codecs import DtoDecoder +from unit.models.transaction import * + + +class CheckDepositResource(BaseResource): + def __init__(self, api_url, token): + super().__init__(api_url, token) + self.resource = "check-deposits" + + def get(self, check_deposit_id: str) -> Union[UnitResponse[CheckDepositDTO], UnitError]: + params = {} + response = super().get(f"{self.resource}/{check_deposit_id}", params) + if response.status_code == 200: + data = response.json().get("data") + included = response.json().get("included") + return UnitResponse[TransactionDTO](DtoDecoder.decode(data), DtoDecoder.decode(included)) + else: + return UnitError.from_json_api(response.json()) + + def list(self, params: ListTransactionParams = None) -> Union[UnitResponse[List[CheckDepositDTO]], UnitError]: + raise NotImplementedError() + + def get_image(self, check_deposit_id: str, is_back_side: Optional[bool] = False) -> Union[UnitResponse[bytes], UnitError]: + params = {} + response = super().get(f"{self.resource}/{check_deposit_id}/{'back' if is_back_side else 'front'}", params) + if response.status_code == 200: + return UnitResponse[bytes](response.content, None) + else: + return UnitError.from_json_api(response.json()) diff --git a/unit/api/check_payment_resource.py b/unit/api/check_payment_resource.py new file mode 100644 index 00000000..cff6a1ea --- /dev/null +++ b/unit/api/check_payment_resource.py @@ -0,0 +1,54 @@ +from unit.api.base_resource import BaseResource +from unit.models.authorization_request import * +from unit.models.check_payment import ApproveCheckPaymentRequest, CheckPaymentDTO, ReturnCheckPaymentRequest +from unit.models.codecs import DtoDecoder +from unit.models.payment import CreateCheckPaymentRequest + + +class CheckPaymentResource(BaseResource): + def __init__(self, api_url, token): + super().__init__(api_url, token) + self.resource = "check-payments" + + def get(self, check_payment_id: str) -> Union[UnitResponse[CheckPaymentDTO], UnitError]: + response = super().get(f"{self.resource}/{check_payment_id}") + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[check_payment_id](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) + + def approve(self, request: ApproveCheckPaymentRequest) -> Union[UnitResponse[CheckPaymentDTO], UnitError]: + payload = request.to_json_api() + response = super().post(f"{self.resource}/{request.check_payment_id}/approve", payload) + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[CheckPaymentDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) + + def create(self, request: CreateCheckPaymentRequest, timeout: float = None) -> Union[UnitResponse[CheckPaymentDTO], UnitError]: + payload = request.to_json_api() + response = super().post(f"{self.resource}", payload, timeout=timeout) + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[CheckPaymentDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) + + def cancel(self, check_payment_id: str) -> Union[UnitResponse[CheckPaymentDTO], UnitError]: + response = super().post(f"{self.resource}/{check_payment_id}/cancel") + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[CheckPaymentDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) + + def return_check(self, request: ReturnCheckPaymentRequest) -> Union[UnitResponse[CheckPaymentDTO], UnitError]: + payload = request.to_json_api() + response = super().post(f"{self.resource}/{request.check_payment_id}/return", payload) + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[CheckPaymentDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) diff --git a/unit/api/check_registered_address.py b/unit/api/check_registered_address.py new file mode 100644 index 00000000..b6ae299e --- /dev/null +++ b/unit/api/check_registered_address.py @@ -0,0 +1,21 @@ +from unit.api.base_resource import BaseResource +from unit.models import * +from unit.models.check_registered_address import CheckRegisteredAddressRequest, CheckRegisteredAddressResponse +from unit.models.codecs import DtoDecoder + + +class CheckRegisteredAddressResource(BaseResource): + def __init__(self, api_url, token): + super().__init__(api_url, token) + self.resource = "applications/check-registered-agent-address" + + def create(self, request: CheckRegisteredAddressRequest) -> Union[UnitResponse[CheckRegisteredAddressResponse], UnitError]: + payload = request.to_json_api() + response = super().post(self.resource, payload) + + if response.ok: + data = response.json().get("data") + return UnitResponse[CheckRegisteredAddressResponse](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) + diff --git a/unit/api/check_stop_payment_resource.py b/unit/api/check_stop_payment_resource.py new file mode 100644 index 00000000..203fb7ef --- /dev/null +++ b/unit/api/check_stop_payment_resource.py @@ -0,0 +1,20 @@ +from unit.api.base_resource import BaseResource +from unit.models.authorization_request import * +from unit.models.check_stop_payment import CheckStopPaymentDTO +from unit.models.codecs import DtoDecoder +from unit.models.payment import CreateCheckStopPaymentRequest + + +class CheckStopPaymentResource(BaseResource): + def __init__(self, api_url, token): + super().__init__(api_url, token) + self.resource = "stop-payments" + + def create(self, request: CreateCheckStopPaymentRequest) -> Union[UnitResponse[CheckStopPaymentDTO], UnitError]: + payload = request.to_json_api() + response = super().post(f"{self.resource}", payload) + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[CheckStopPaymentDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) diff --git a/unit/api/customer_resource.py b/unit/api/customer_resource.py index d164452c..bbcbfc05 100644 --- a/unit/api/customer_resource.py +++ b/unit/api/customer_resource.py @@ -38,3 +38,12 @@ def list(self, params: ListCustomerParams = None) -> Union[UnitResponse[List[Cus return UnitResponse[CustomerDTO](DtoDecoder.decode(data), None) else: return UnitError.from_json_api(response.json()) + + def archive(self, request: ArchiveCustomerRequest) -> Union[UnitResponse[CustomerDTO], UnitError]: + payload = request.to_json_api() + response = super().post(f"{self.resource}/{request.customer_id}/archive", payload) + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[CustomerDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) diff --git a/unit/api/dispute_resource.py b/unit/api/dispute_resource.py new file mode 100644 index 00000000..04d7124b --- /dev/null +++ b/unit/api/dispute_resource.py @@ -0,0 +1,18 @@ +from unit.api.base_resource import BaseResource +from unit.models.codecs import DtoDecoder +from unit.models.dispute import * + + + +class DisputeResource(BaseResource): + def __init__(self, api_url, token): + super().__init__(api_url, token) + self.resource = "disputes" + + def get(self, dispute_id: str) -> Union[UnitResponse[DisputeDTO], UnitError]: + response = super().get(f"{self.resource}/{dispute_id}") + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[DisputeDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) diff --git a/unit/api/payment_resource.py b/unit/api/payment_resource.py index 6ef30a43..241ce540 100644 --- a/unit/api/payment_resource.py +++ b/unit/api/payment_resource.py @@ -1,6 +1,6 @@ from unit.api.base_resource import BaseResource from unit.models.payment import * -from unit.models.codecs import DtoDecoder +from unit.models.codecs import DtoDecoder, split_json_api_single_response class PaymentResource(BaseResource): @@ -8,9 +8,9 @@ def __init__(self, api_url, token): super().__init__(api_url, token) self.resource = "payments" - def create(self, request: CreatePaymentRequest) -> Union[UnitResponse[PaymentDTO], UnitError]: + def create(self, request: CreatePaymentRequest, timeout: float = None) -> Union[UnitResponse[PaymentDTO], UnitError]: payload = request.to_json_api() - response = super().post(self.resource, payload) + response = super().post(self.resource, payload, timeout=timeout) if super().is_20x(response.status_code): data = response.json().get("data") return UnitResponse[PaymentDTO](DtoDecoder.decode(data), None) @@ -45,3 +45,21 @@ def list(self, params: ListPaymentParams = None) -> Union[UnitResponse[List[Paym else: return UnitError.from_json_api(response.json()) + def simulate_incoming_ach(self, request: SimulateIncomingAchRequest) -> Union[UnitResponse[PaymentDTO], UnitError]: + payload = request.to_json_api() + response = super().post(f"sandbox/{self.resource}", payload) + if super().is_20x(response.status_code): + data = response.json().get("data") + # TODO Fix dto + _id, _type, attributes, relationships = split_json_api_single_response(data) + return UnitResponse[SimulateIncomingAchPaymentDTO](SimulateIncomingAchPaymentDTO.from_json_api(_id, _type, attributes, relationships), None) + else: + return UnitError.from_json_api(response.json()) + + def cancel(self, payment_id: str) -> Union[UnitResponse[PaymentDTO], UnitError]: + response = super().post(f"{self.resource}/{payment_id}/cancel") + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[PaymentDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) diff --git a/unit/api/received_payment_resource.py b/unit/api/received_payment_resource.py index b453d6ac..888fd891 100644 --- a/unit/api/received_payment_resource.py +++ b/unit/api/received_payment_resource.py @@ -38,6 +38,14 @@ def list(self, params: ListReceivedPaymentParams = None) -> Union[UnitResponse[L def advance(self, payment_id: str) -> Union[UnitResponse[AchReceivedPaymentDTO], UnitError]: response = super().post(f"{self.resource}/{payment_id}/advance") + if response.status_code == 200: + data = response.json().get("data") + return UnitResponse[AchReceivedPaymentDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) + + def reprocess(self, payment_id: str) -> Union[UnitResponse[AchReceivedPaymentDTO], UnitError]: + response = super().post(f"{self.resource}/{payment_id}/reprocess") if response.status_code == 200: data = response.json().get("data") return UnitResponse[AchReceivedPaymentDTO](DtoDecoder.decode(data), None) diff --git a/unit/api/repayment_resource.py b/unit/api/repayment_resource.py new file mode 100644 index 00000000..74276816 --- /dev/null +++ b/unit/api/repayment_resource.py @@ -0,0 +1,46 @@ +from typing import Union, List, Optional + +from unit.api.base_resource import BaseResource +from unit.models import UnitResponse, UnitError +from unit.models.codecs import DtoDecoder +from unit.models.repayment import ( + RepaymentDTO, + CreateRepaymentRequest, + ListRepaymentParams, +) + + +class RepaymentResource(BaseResource): + def __init__(self, api_url, token): + super().__init__(api_url, token) + self.resource = "repayments" + + def create( + self, request: CreateRepaymentRequest + ) -> Union[UnitResponse[RepaymentDTO], UnitError]: + payload = request.to_json_api() + response = super().post(self.resource, payload) + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[RepaymentDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) + + def get(self, repayment_id: str) -> Union[UnitResponse[RepaymentDTO], UnitError]: + response = super().get(f"{self.resource}/{repayment_id}") + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[RepaymentDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) + + def list( + self, params: Optional[ListRepaymentParams] = None + ) -> Union[UnitResponse[List[RepaymentDTO]], UnitError]: + params = params or ListRepaymentParams() + response = super().get(self.resource, params.to_dict()) + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[List[RepaymentDTO]](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) diff --git a/unit/api/reward_resource.py b/unit/api/reward_resource.py new file mode 100644 index 00000000..8cafa463 --- /dev/null +++ b/unit/api/reward_resource.py @@ -0,0 +1,39 @@ +from typing import Union, List + +from unit.api.base_resource import BaseResource +from unit.models import UnitResponse, UnitError +from unit.models.reward import RewardDTO, ListRewardsParams, CreateRewardRequest + +from unit.models.codecs import DtoDecoder + + +class RewardResource(BaseResource): + def __init__(self, api_url, token): + super().__init__(api_url, token) + self.resource = "rewards" + + def create(self, request: CreateRewardRequest) -> Union[UnitResponse[RewardDTO], UnitError]: + payload = request.to_json_api() + response = super().post(self.resource, payload) + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[RewardDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) + + def get(self, reward_id: str) -> Union[UnitResponse[RewardDTO], UnitError]: + response = super().get(f"{self.resource}/{reward_id}") + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[RewardDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) + + def list(self, params: ListRewardsParams = None) -> Union[UnitResponse[List[RewardDTO]], UnitError]: + params = params or ListRewardsParams() + response = super().get(self.resource, params.to_dict()) + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[List[RewardDTO]](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) diff --git a/unit/api/statement_resource.py b/unit/api/statement_resource.py index 8d0f47b1..791acb7f 100644 --- a/unit/api/statement_resource.py +++ b/unit/api/statement_resource.py @@ -15,7 +15,7 @@ def get(self, params: GetStatementParams) -> Union[UnitResponse[str], UnitError] response = super().get(f"{self.resource}/{params.statement_id}/{params.output_type}", parameters) if response.status_code == 200: - return UnitResponse[str](response.text, None) + return UnitResponse[bytes](response.content, None) else: return UnitError.from_json_api(response.json()) @@ -23,7 +23,7 @@ def get_bank_verification(self, account_id: str, include_proof_of_funds: Optiona response = super().get(f"{self.resource}/{account_id}/bank/pdf", {"includeProofOfFunds": include_proof_of_funds}) if response.status_code == 200: - return UnitResponse[str](response.text, None) + return UnitResponse[bytes](response.content, None) else: return UnitError.from_json_api(response.json()) diff --git a/unit/api/transaction_resource.py b/unit/api/transaction_resource.py index 2ab9d369..d9291033 100644 --- a/unit/api/transaction_resource.py +++ b/unit/api/transaction_resource.py @@ -9,8 +9,9 @@ def __init__(self, api_url, token): super().__init__(api_url, token) self.resource = "transactions" - def get(self, transaction_id: str, include: Optional[str] = "") -> Union[UnitResponse[TransactionDTO], UnitError]: - response = super().get(f"{self.resource}/{transaction_id}", {"include": include}) + def get(self, transaction_id: str, account_id: str, include: Optional[str] = "") -> Union[UnitResponse[TransactionDTO], UnitError]: + params = {"filter[accountId]": account_id, "include": include} + response = super().get(f"{self.resource}/{transaction_id}", params) if response.status_code == 200: data = response.json().get("data") included = response.json().get("included") @@ -38,3 +39,20 @@ def update(self, request: PatchTransactionRequest) -> Union[UnitResponse[Transac else: return UnitError.from_json_api(response.json()) + def sandbox_simulate_purchase_transaction(self, request: SimulatePurchaseTransaction) -> Union[UnitResponse[PurchaseTransactionDTO], UnitError]: + payload = request.to_json_api() + response = super().post(f"sandbox/purchases", payload) + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[PurchaseTransactionDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) + + def sandbox_simulate_card_transaction(self, request: SimulateCardTransaction) -> Union[UnitResponse[CardTransactionDTO], UnitError]: + payload = request.to_json_api() + response = super().post(f"sandbox/card-transactions", payload) + if super().is_20x(response.status_code): + data = response.json().get("data") + return UnitResponse[CardTransactionDTO](DtoDecoder.decode(data), None) + else: + return UnitError.from_json_api(response.json()) diff --git a/unit/models/__init__.py b/unit/models/__init__.py index e7fe6e81..88e69d0f 100644 --- a/unit/models/__init__.py +++ b/unit/models/__init__.py @@ -3,6 +3,33 @@ from datetime import datetime, date +def to_camel_case(snake_str): + components = snake_str.lstrip("_").split("_") + # We capitalize the first letter of each component except the first one + # with the 'title' method and join them together. + return components[0] + "".join(x.title() for x in components[1:]) + + +def extract_attributes(list_of_attributes, attributes): + extracted_attributes = {} + for a in list_of_attributes: + if a in attributes: + extracted_attributes[a] = attributes[a] + + return extracted_attributes + + +class UnitDTO(object): + def to_dict(self): + if type(self) is dict: + return self + else: + v = vars(self) + return dict( + (to_camel_case(k), val) for k, val in v.items() if val is not None + ) + + class Relationship(object): def __init__(self, _type: str, _id: str): self.type = _type @@ -12,7 +39,8 @@ def to_dict(self): return {"type": self.type, "id": self.id} -T = TypeVar('T') +T = TypeVar("T") + class RelationshipArray(Generic[T]): def __init__(self, l: List[T]): @@ -33,10 +61,41 @@ class UnitRequest(object): def to_json_api(self) -> Dict: pass + def vars_to_attributes_dict(self, ignore: List[str] = []) -> Dict: + attributes = {} + + for k in self.__dict__: + if k != "relationships" and k not in ignore: + v = getattr(self, k) + if v: + attributes[to_camel_case(k)] = v + + return attributes + + def to_payload( + self, + _type: str, + relationships: Dict[str, Relationship] = None, + ignore: List[str] = [], + ) -> Dict: + payload = { + "data": { + "type": _type, + "attributes": self.vars_to_attributes_dict(ignore), + } + } + + if relationships: + payload["data"]["relationships"] = relationships + + return payload + + class UnitParams(object): def to_dict(self) -> Dict: pass + class RawUnitObject(object): def __init__(self, _id, _type, attributes, relationships): self.id = _id @@ -44,9 +103,16 @@ def __init__(self, _id, _type, attributes, relationships): self.attributes = attributes self.relationships = relationships + class UnitErrorPayload(object): - def __init__(self, title: str, status: str, detail: Optional[str] = None, details: Optional[str] = None, - source: Optional[Dict] = None): + def __init__( + self, + title: str, + status: str, + detail: Optional[str] = None, + details: Optional[str] = None, + source: Optional[Dict] = None, + ): self.title = title self.status = status self.detail = detail @@ -66,35 +132,77 @@ def from_json_api(data: Dict): errors = [] for err in data["errors"]: errors.append( - UnitErrorPayload(err.get("title"), err.get("status"), err.get("detail", None), - err.get("details", None), err.get("source", None)) + UnitErrorPayload( + err.get("title"), + err.get("status"), + err.get("detail", None), + err.get("details", None), + err.get("source", None), + ) ) return UnitError(errors) def __str__(self): - return json.dumps({"errors": [{"title": err.title, "status": err.status, "detail": err.detail, - "details": err.details, "source": err.source} for err in self.errors]}) - - + return json.dumps( + { + "errors": [ + { + "title": err.title, + "status": err.status, + "detail": err.detail, + "details": err.details, + "source": err.source, + } + for err in self.errors + ] + } + ) + + +Occupation = Literal["ArchitectOrEngineer", "BusinessAnalystAccountantOrFinancialAdvisor", + "CommunityAndSocialServicesWorker", "ConstructionMechanicOrMaintenanceWorker", "Doctor", + "Educator", "EntertainmentSportsArtsOrMedia", "ExecutiveOrManager", "FarmerFishermanForester", + "FoodServiceWorker", "GigWorker", "HospitalityOfficeOrAdministrativeSupportWorker", + "HouseholdManager", "JanitorHousekeeperLandscaper", "Lawyer", "ManufacturingOrProductionWorker", + "MilitaryOrPublicSafety", "NurseHealthcareTechnicianOrHealthcareSupport", + "PersonalCareOrServiceWorker", "PilotDriverOperator", "SalesRepresentativeBrokerAgent", + "ScientistOrTechnologist", "Student"] +AnnualIncome = Literal["UpTo10k", "Between10kAnd25k", "Between25kAnd50k", "Between50kAnd100k", "Between100kAnd250k", + "Over250k"] +SourceOfIncome = Literal["EmploymentOrPayrollIncome", "PartTimeOrContractorIncome", "InheritancesAndGifts", + "PersonalInvestments", "BusinessOwnershipInterests", "GovernmentBenefits"] Status = Literal["Approved", "Denied", "PendingReview"] -Title = Literal["CEO", "COO", "CFO", "President"] -EntityType = Literal["Corporation", "LLC", "Partnership"] +Title = Literal["CEO", "COO", "CFO", "President", "BenefitsAdministrationOfficer", "CIO", "VP", "AVP", "Treasurer", + "Secretary", "Controller", "Manager", "Partner", "Member"] +EntityType = Literal["Corporation", "LLC", "Partnership", "PubliclyTradedCorporation", "PrivatelyHeldCorporation", + "NotForProfitOrganization"] + -class FullName(object): +class FullName(UnitDTO): def __init__(self, first: str, last: str): self.first = first self.last = last + def __str__(self): + return f"{self.first} {self.last}" + @staticmethod def from_json_api(data: Dict): return FullName(data.get("first"), data.get("last")) # todo: Alex - use typing.Literal for multi accepted values (e.g country) -class Address(object): - def __init__(self, street: str, city: str, state: str, postal_code: str, country: str, - street2: Optional[str] = None): +class Address(UnitDTO): + def __init__( + self, + street: str, + city: str, + state: str, + postal_code: str, + country: str, + street2: Optional[str] = None, + ): self.street = street self.street2 = street2 self.city = city @@ -104,11 +212,17 @@ def __init__(self, street: str, city: str, state: str, postal_code: str, country @staticmethod def from_json_api(data: Dict): - return Address(data.get("street"), data.get("city"), data.get("state"), - data.get("postalCode"), data.get("country"), data.get("street2", None)) + return Address( + data.get("street"), + data.get("city"), + data.get("state"), + data.get("postalCode"), + data.get("country"), + data.get("street2", None), + ) -class Phone(object): +class Phone(UnitDTO): def __init__(self, country_code: str, number: str): self.country_code = country_code self.number = number @@ -118,7 +232,7 @@ def from_json_api(data: Dict): return Phone(data.get("countryCode"), data.get("number")) -class BusinessContact(object): +class BusinessContact(UnitDTO): def __init__(self, full_name: FullName, email: str, phone: Phone): self.full_name = full_name self.email = email @@ -126,13 +240,19 @@ def __init__(self, full_name: FullName, email: str, phone: Phone): @staticmethod def from_json_api(data: Dict): - return BusinessContact(FullName.from_json_api(data.get("fullName")), data.get("email"), Phone.from_json_api(data.get("phone"))) + return BusinessContact( + FullName.from_json_api(data.get("fullName")), + data.get("email"), + Phone.from_json_api(data.get("phone")), + ) -class Officer(object): +class Officer(UnitDTO): def __init__(self, full_name: FullName, date_of_birth: date, address: Address, phone: Phone, email: str, status: Optional[Status] = None, title: Optional[Title] = None, ssn: Optional[str] = None, - passport: Optional[str] = None, nationality: Optional[str] = None): + passport: Optional[str] = None, nationality: Optional[str] = None, id_theft_score: Optional[str] = None, + occupation: Optional[Occupation] = None, annual_income: Optional[AnnualIncome] = None, + source_of_income: Optional[SourceOfIncome] = None): self.full_name = full_name self.date_of_birth = date_of_birth self.address = address @@ -143,18 +263,36 @@ def __init__(self, full_name: FullName, date_of_birth: date, address: Address, p self.ssn = ssn self.passport = passport self.nationality = nationality + self.id_theft_score = id_theft_score + self.occupation = occupation + self.annual_income = annual_income + self.source_of_income = source_of_income @staticmethod def from_json_api(data: Dict): return Officer(data.get("fullName"), data.get("dateOfBirth"), data.get("address"), data.get("phone"), - data.get("email"), data.get("status"), data.get("title"), data.get("ssn"), data.get("passport"), - data.get("nationality")) - - -class BeneficialOwner(object): - def __init__(self, full_name: FullName, date_of_birth: date, address: Address, phone: Phone, email: str, - status: Optional[Status] = None, ssn: Optional[str] = None, passport: Optional[str] = None, - nationality: Optional[str] = None, percentage: Optional[int] = None): + data.get("email"), data.get("status"), data.get("title"), data.get("ssn"), data.get("passport"), + data.get("nationality"), data.get("idTheftScore"), data.get("occupation"), data.get("annualIncome"), + data.get("sourceOfIncome")) + + +class BeneficialOwner(UnitDTO): + def __init__( + self, + full_name: FullName, + date_of_birth: date, + address: Address, + phone: Phone, + email: str, + status: Optional[Status] = None, + ssn: Optional[str] = None, + passport: Optional[str] = None, + nationality: Optional[str] = None, + percentage: Optional[int] = None, + occupation: Optional[Occupation] = None, + annual_income: Optional[AnnualIncome] = None, + source_of_income: Optional[SourceOfIncome] = None + ): self.full_name = full_name self.date_of_birth = date_of_birth self.address = address @@ -165,32 +303,57 @@ def __init__(self, full_name: FullName, date_of_birth: date, address: Address, p self.passport = passport self.nationality = nationality self.percentage = percentage + self.occupation = occupation + self.annual_income = annual_income + self.source_of_income = source_of_income @staticmethod def from_json_api(l: List): beneficial_owners = [] for data in l: - beneficial_owners.append(BeneficialOwner(data.get("fullName"), data.get("dateOfBirth"), data.get("address"), - data.get("phone"), data.get("email"), data.get("status"), data.get("ssn"), - data.get("passport"), data.get("nationality"), data.get("percentage"))) + beneficial_owners.append( + BeneficialOwner( + data.get("fullName"), + data.get("dateOfBirth"), + data.get("address"), + data.get("phone"), + data.get("email"), + data.get("status"), + data.get("ssn"), + data.get("passport"), + data.get("nationality"), + data.get("percentage"), + data.get("occupation"), + data.get("annualIncome"), + data.get("sourceOfIncome") + ) + ) return beneficial_owners -class AuthorizedUser(object): - def __init__(self, full_name: FullName, email: str, phone: Phone): +class AuthorizedUser(UnitDTO): + def __init__(self, full_name: FullName, email: str, phone: Phone, jwt_subject: Optional[str] = None): self.full_name = full_name self.email = email self.phone = phone + self.jwt_subject = jwt_subject @staticmethod - def from_json_api(l: List): + def from_json_api(l: List) -> List: authorized_users = [] for data in l: - authorized_users.append(AuthorizedUser(data.get("fullName"), data.get("email"), data.get("phone"))) + authorized_users.append( + AuthorizedUser( + data.get("fullName"), data.get("email"), data.get("phone"), data.get("jwtSubject") + ) + ) return authorized_users -class WireCounterparty(object): - def __init__(self, routing_number: str, account_number: str, name: str, address: Address): + +class WireCounterparty(UnitDTO): + def __init__( + self, routing_number: str, account_number: str, name: str, address: Address + ): self.routing_number = routing_number self.account_number = account_number self.name = name @@ -198,11 +361,18 @@ def __init__(self, routing_number: str, account_number: str, name: str, address: @staticmethod def from_json_api(data: Dict): - return WireCounterparty(data["routingNumber"], data["accountNumber"], data["name"], - Address.from_json_api(data["address"])) - -class Counterparty(object): - def __init__(self, routing_number: str, account_number: str, account_type: str, name: str): + return WireCounterparty( + data["routingNumber"], + data["accountNumber"], + data["name"], + Address.from_json_api(data["address"]), + ) + + +class Counterparty(UnitDTO): + def __init__( + self, routing_number: str, account_number: str, account_type: str, name: str + ): self.routing_number = routing_number self.account_number = account_number self.account_type = account_type @@ -210,31 +380,87 @@ def __init__(self, routing_number: str, account_number: str, account_type: str, @staticmethod def from_json_api(data: Dict): - return Counterparty(data["routingNumber"], data["accountNumber"], data["accountType"], data["name"]) + return Counterparty( + data["routingNumber"], + data["accountNumber"], + data["accountType"], + data["name"], + ) + + +class CheckCounterparty(UnitDTO): + def __init__( + self, routing_number: str, account_number: str, name: str + ): + self.routing_number = routing_number + self.account_number = account_number + self.name = name -class Coordinates(object): + @staticmethod + def from_json_api(data: Dict): + return CheckCounterparty( + data["routingNumber"], + data["accountNumber"], + data["name"], + ) + + +class CheckPaymentCounterparty(UnitDTO): + def __init__( + self, name: str, address: Address + ): + self.name = name + self.address = address + + @staticmethod + def from_json_api(data: Dict): + return CheckPaymentCounterparty( + data["name"], + data["address"], + ) + + +class Coordinates(UnitDTO): def __init__(self, longitude: int, latitude: int): self.longitude = longitude self.latitude = latitude @staticmethod def from_json_api(data: Dict): - return Coordinates(data["longitude"], data["latitude"]) + if data: + return Coordinates(data["longitude"], data["latitude"]) + else: + return None -class Merchant(object): - def __init__(self, name: str, type: int, category: str, location: Optional[str]): +class Merchant(UnitDTO): + def __init__( + self, name: str, type: int, category: Optional[str] = None, location: Optional[str] = None, _id: Optional[str] = None, + ): self.name = name self.type = type self.category = category self.location = location + self.id = _id @staticmethod - def from_json_api(data: Dict): - return Merchant(data["name"], data["type"], data["category"], data.get("location")) - -class CardLevelLimits(object): - def __init__(self, daily_withdrawal: int, daily_purchase: int, monthly_withdrawal: int, monthly_purchase: int): + def from_json_api(data: Dict = None): + if data is None: + return None + + return Merchant( + data["name"], data["type"], data.get("category"), data.get("location"), data.get("id") + ) + + +class CardLevelLimits(UnitDTO): + def __init__( + self, + daily_withdrawal: int, + daily_purchase: int, + monthly_withdrawal: int, + monthly_purchase: int, + ): self.daily_withdrawal = daily_withdrawal self.daily_purchase = daily_purchase self.monthly_withdrawal = monthly_withdrawal @@ -242,10 +468,15 @@ def __init__(self, daily_withdrawal: int, daily_purchase: int, monthly_withdrawa @staticmethod def from_json_api(data: Dict): - return CardLevelLimits(data["dailyWithdrawal"], data["dailyPurchase"], data["monthlyWithdrawal"], - data["monthlyPurchase"]) + return CardLevelLimits( + data["dailyWithdrawal"], + data["dailyPurchase"], + data["monthlyWithdrawal"], + data["monthlyPurchase"], + ) + -class CardTotals(object): +class CardTotals(UnitDTO): def __init__(self, withdrawals: int, deposits: int, purchases: int): self.withdrawals = withdrawals self.deposits = deposits @@ -256,7 +487,7 @@ def from_json_api(data: Dict): return CardTotals(data["withdrawals"], data["deposits"], data["purchases"]) -class DeviceFingerprint(object): +class DeviceFingerprint(UnitDTO): def __init__(self, value: str, provider: str = "iovation"): self.value = value self.provider = provider @@ -270,3 +501,91 @@ def to_json_api(self): @classmethod def from_json_api(cls, data: Dict): return cls(value=data["value"], provider=data["provider"]) + + +class CurrencyConversion(UnitDTO): + def __init__(self, original_currency: str, amount_in_original_currency: int, fx_rate: Optional[str]): + self.original_currency = original_currency + self.amount_in_original_currency = amount_in_original_currency + self.fx_rate = fx_rate + + @staticmethod + def from_json_api(data: Dict): + if not data: + return None + + return CurrencyConversion(data["originalCurrency"], data["amountInOriginalCurrency"], data.get("fxRate")) + + +class RichMerchantDataFacilitator(object): + def __init__(self, name: str, _type: Optional[str], logo: Optional[str]): + self.name = name + self.type = _type + self.logo = logo + + @staticmethod + def from_json_api(data: Dict): + if not data: + return None + + arr = [] + for c in data: + arr.append(RichMerchantDataFacilitator(c["name"], c.get("type"), c.get("logo"))) + + return arr + + +class RichMerchantDataCategory(object): + def __init__(self, name: str, icon: str): + self.name = name + self.icon = icon + + @staticmethod + def from_json_api(data: Dict): + if not data: + return None + + arr = [] + for c in data: + arr.append(RichMerchantDataCategory(c["name"], c["icon"])) + + return arr + + +class RichMerchantDataAddress(object): + def __init__(self, city: str, state: str, country: str, street: Optional[str]): + self.city = city + self.state = state + self.country = country + self.street = street + + @staticmethod + def from_json_api(data: Dict): + if not data: + return None + + return RichMerchantDataAddress(data["city"], data["state"], data["country"], data.get("street")) + + +class RichMerchantData(UnitDTO): + def __init__(self, name: str, website: Optional[str], logo: Optional[str], phone: Optional[str], + categories: Optional[List[RichMerchantDataCategory]], address: Optional[RichMerchantDataAddress], + coordinates: Optional[Coordinates], facilitators: Optional[List[RichMerchantDataFacilitator]]): + self.name = name + self.website = website + self.logo = logo + self.phone = phone + self.categories = categories + self.address = address + self.coordinates = coordinates + self.facilitators = facilitators + + @staticmethod + def from_json_api(data: Dict): + if not data: + return None + + return RichMerchantData(data["name"], data.get("website"), data.get("logo"), data.get("phone"), + RichMerchantDataCategory.from_json_api(data.get("categories")), data.get("address"), + Coordinates.from_json_api(data.get("coordinates")), + RichMerchantDataFacilitator.from_json_api(data.get("facilitators"))) diff --git a/unit/models/account.py b/unit/models/account.py index 9768faf9..41bf2558 100644 --- a/unit/models/account.py +++ b/unit/models/account.py @@ -2,9 +2,14 @@ from unit.models import * -AccountStatus = Literal["Open", "Closed"] +AccountStatus = Literal["Open", "Frozen", "Closed"] CloseReason = Literal["ByCustomer", "Fraud"] +FraudReason = Literal["ACHActivity", "CardActivity", "CheckActivity", "ApplicationHistory", "AccountActivity", + "ClientIdentified", "IdentityTheft", "LinkedToFraudulentCustomer"] +CreditAccountType = "creditAccount" +DepositAccountType = "depositAccount" +AccountTypes = Literal[CreditAccountType, DepositAccountType] class DepositAccountDTO(object): def __init__(self, id: str, created_at: datetime, name: str, deposit_product: str, routing_number: str, @@ -29,7 +34,33 @@ def from_json_api(_id, _type, attributes, relationships): ) -AccountDTO = Union[DepositAccountDTO] +class CreditAccountDTO(object): + def __init__(self, _id: str, created_at: datetime, updated_at: Optional[datetime], name: str, credit_terms: str, + currency: str, credit_limit: int, balance: int, hold: int, available: int, + tags: Optional[Dict[str, str]], status: AccountStatus, freeze_reason: Optional[str], + close_reason: Optional[str], close_reason_text: Optional[str], fraud_reason: Optional[FraudReason], + relationships: Optional[Dict[str, Relationship]]): + self.id = _id + self.type = CreditAccountType + self.attributes = {"createdAt": created_at, "updatedAt": updated_at, "name": name, "status": status, + "creditTerms": credit_terms, "currency": currency, "creditLimit": credit_limit, + "balance": balance, "hold": hold, "available": available, "tags": tags, + "freezeReason": freeze_reason, "closeReason": close_reason, + "closeReasonText": close_reason_text, "fraudReason": fraud_reason} + self.relationships = relationships + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CreditAccountDTO(_id, date_utils.to_datetime(attributes["createdAt"]), + date_utils.to_datetime(attributes.get("updatedAt")), attributes["name"], + attributes["creditTerms"], attributes["currency"], attributes["creditLimit"], + attributes["balance"], attributes["hold"], attributes["available"], + attributes.get("tags"), attributes["status"], attributes.get("freezeReason"), + attributes.get("closeReason"), attributes.get("closeReasonText"), + attributes.get("fraudReason"), relationships) + + +AccountDTO = Union[DepositAccountDTO, CreditAccountDTO] class CreateDepositAccountRequest(UnitRequest): @@ -62,6 +93,39 @@ def to_json_api(self) -> Dict: def __repr__(self): json.dumps(self.to_json_api()) +class CreateCreditAccountRequest(UnitRequest): + def __init__(self, credit_terms: str, credit_limit: int, relationships: Dict[str, Relationship], + tags: Optional[Dict[str, str]] = None, idempotency_key: Optional[str] = None): + self.credit_terms = credit_terms + self.credit_limit = credit_limit + self.tags = tags + self.idempotency_key = idempotency_key + self.relationships = relationships + + def to_json_api(self) -> Dict: + payload = { + "data": { + "type": "creditAccount", + "attributes": { + "creditTerms": self.credit_terms, + "creditLimit": self.credit_limit + }, + "relationships": self.relationships + } + } + + if self.tags: + payload["data"]["attributes"]["tags"] = self.tags + + if self.idempotency_key: + payload["data"]["attributes"]["idempotencyKey"] = self.idempotency_key + + return payload + + def __repr__(self): + return json.dumps(self.to_json_api()) + +CreateAccountRequest = Union[CreateDepositAccountRequest, CreateCreditAccountRequest] class PatchDepositAccountRequest(UnitRequest): def __init__(self, account_id: str, deposit_product: Optional[str] = None, tags: Optional[Dict[str, str]] = None): @@ -88,6 +152,33 @@ def to_json_api(self) -> Dict: def __repr__(self): json.dumps(self.to_json_api()) +class PatchCreditAccountRequest(UnitRequest): + def __init__(self, account_id: str, tags: Optional[Dict[str, str]] = None, credit_limit: Optional[int] = None): + self.account_id = account_id + self.tags = tags + self.credit_limit = credit_limit + + def to_json_api(self) -> Dict: + payload = { + "data": { + "type": CreditAccountType, + "attributes": {} + } + } + + if self.tags: + payload["data"]["attributes"]["tags"] = self.tags + + if self.credit_limit: + payload["data"]["attributes"]["creditLimit"] = self.credit_limit + + return payload + + def __repr__(self): + return json.dumps(self.to_json_api()) + +PatchAccountRequest = Union[PatchDepositAccountRequest, PatchCreditAccountRequest] + class AchTotals(object): def __init__(self, debits: int, credits: int): @@ -197,15 +288,20 @@ def from_json_api(attributes): CheckDepositAccountLimits.from_json_api(attributes["checkDeposit"])) +AccountCloseType = Literal["depositAccountClose", "creditAccountClose"] + + class CloseAccountRequest(UnitRequest): - def __init__(self, account_id: str, reason: Optional[Literal["ByCustomer", "Fraud"]] = "ByCustomer"): + def __init__(self, account_id: str, reason: Optional[Literal["ByCustomer", "Fraud"]] = "ByCustomer", + _type: AccountCloseType = "depositAccountClose"): self.account_id = account_id self.reason = reason + self._type = _type def to_json_api(self) -> Dict: payload = { "data": { - "type": "accountClose", + "type": self._type, "attributes": { "reason": self.reason, } @@ -215,24 +311,74 @@ def to_json_api(self) -> Dict: return payload def __repr__(self): - json.dumps(self.to_json_api()) + return json.dumps(self.to_json_api()) + + + +AccountFreezeReasons = Literal["Other", "Fraud"] +AccountFreezeTypes = Literal["accountFreeze", "creditAccountFreeze"] + + +class FreezeAccountRequest(UnitRequest): + def __init__( + self, + account_id: str, + reason: AccountFreezeReasons, + reason_text: Optional[str], + type: Optional[str] = "accountFreeze", + ): + self.account_id = account_id + self.reason = reason + self.reason_text = reason_text + self._type = type + + def to_json_api(self) -> Dict: + payload = { + "data": { + "type": self._type, + "attributes": { + "reason": self.reason, + "reasonText": self.reason_text, + } + } + } + + return payload + + def __repr__(self): + return json.dumps(self.to_json_api()) class ListAccountParams(UnitParams): def __init__(self, offset: int = 0, limit: int = 100, customer_id: Optional[str] = None, - tags: Optional[object] = None, include: Optional[str] = None): + tags: Optional[Dict[str, str]] = None, include: Optional[str] = None, + status: Optional[AccountStatus] = None, from_balance: Optional[int] = None, + to_balance: Optional[int] = None, _type: Optional[AccountTypes] = None): self.offset = offset self.limit = limit self.customer_id = customer_id self.tags = tags self.include = include + self.status = status + self.from_balance = from_balance + self.to_balance = to_balance + self._type = _type def to_dict(self) -> Dict: parameters = {"page[limit]": self.limit, "page[offset]": self.offset} if self.customer_id: parameters["filter[customerId]"] = self.customer_id if self.tags: - parameters["filter[tags]"] = self.tags + parameters["filter[tags]"] = json.dumps(self.tags) if self.include: parameters["include"] = self.include + if self.status: + for idx, status_filter in enumerate(self.status): + parameters[f"filter[status][{idx}]"] = status_filter + if self._type: + parameters[f"filter[type]"] = self._type + if self.from_balance: + parameters["filter[fromBalance]"] = self.from_balance + if self.to_balance: + parameters["filter[toBalance]"] = self.to_balance return parameters diff --git a/unit/models/accrued_interest.py b/unit/models/accrued_interest.py new file mode 100644 index 00000000..7a43628c --- /dev/null +++ b/unit/models/accrued_interest.py @@ -0,0 +1,41 @@ +from typing import Optional +from unit.models import * + + +class AccruedInterestTotalDTO(object): + def __init__(self, id: str, amount: int, + relationships: Optional[Dict[str, Relationship]]): + self.id = id + self.type = "accruedInterestTotal" + self.attributes = {"amount": amount} + self.relationships = relationships + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return AccruedInterestTotalDTO(_id, attributes["amount"], relationships) + + +class GetAccruedInterestTotalParams(UnitParams): + def __init__(self, account_id: Optional[str] = None, + since: Optional[str] = None, until: Optional[str] = None, + since_interest_month: Optional[str] = None, + until_interest_month: Optional[str] = None): + self.account_id = account_id + self.since = since + self.until = until + self.since_interest_month = since_interest_month + self.until_interest_month = until_interest_month + + def to_dict(self) -> Dict: + parameters = {} + if self.account_id: + parameters["filter[accountId]"] = self.account_id + if self.since: + parameters["filter[since]"] = self.since + if self.until: + parameters["filter[until]"] = self.until + if self.since_interest_month: + parameters["filter[sinceInterestMonth]"] = self.since_interest_month + if self.until_interest_month: + parameters["filter[untilInterestMonth]"] = self.until_interest_month + return parameters diff --git a/unit/models/application.py b/unit/models/application.py index d8edfec6..06516cf0 100644 --- a/unit/models/application.py +++ b/unit/models/application.py @@ -4,76 +4,307 @@ ApplicationStatus = Literal["Approved", "Denied", "Pending", "PendingReview"] -DocumentType = Literal["IdDocument", "Passport", "AddressVerification", "CertificateOfIncorporation", - "EmployerIdentificationNumberConfirmation"] +DocumentType = Literal[ + "IdDocument", + "Passport", + "AddressVerification", + "CertificateOfIncorporation", + "EmployerIdentificationNumberConfirmation", + "SocialSecurityCard", + "ClientRequested", + "SelfieVerification", +] + +ApplicationDocumentStatus = Literal[ + "Required", + "ReceivedBack", + "ReceivedFront", + "Invalid", + "Approved", + "PendingReview" +] + +ReasonCode = Literal[ + "PoorQuality", + "NameMismatch", + "SSNMismatch", + "AddressMismatch", + "DOBMismatch", + "ExpiredId", + "EINMismatch", + "StateMismatch", + "Other", +] + +ApplicationTypes = Literal[ + "individualApplication", "businessApplication", "trustApplication" +] + + +Industry = Literal[ + "Retail", + "Wholesale", + "Restaurants", + "Hospitals", + "Construction", + "Insurance", + "Unions", + "RealEstate", + "FreelanceProfessional", + "OtherProfessionalServices", + "OnlineRetailer", + "OtherEducationServices", +] + +AnnualRevenue = Literal[ + "UpTo250k", + "Between250kAnd500k", + "Between500kAnd1m", + "Between1mAnd5m", + "Over5m", + "UpTo50k", + "Between50kAnd100k", + "Between100kAnd200k", + "Between200kAnd500k", + "Over500k", +] + +NumberOfEmployees = Literal[ + "One", + "Between2And5", + "Between5And10", + "Over10", + "UpTo10", + "Between10And50", + "Between50And100", + "Between100And500", + "Over500", +] + +CashFlow = Literal["Unpredictable", "Predictable"] + +BusinessVertical = Literal[ + "AdultEntertainmentDatingOrEscortServices", + "AgricultureForestryFishingOrHunting", + "ArtsEntertainmentAndRecreation", + "BusinessSupportOrBuildingServices", + "Cannabis", + "Construction", + "DirectMarketingOrTelemarketing", + "EducationalServices", + "FinancialServicesCryptocurrency", + "FinancialServicesDebitCollectionOrConsolidation", + "FinancialServicesMoneyServicesBusinessOrCurrencyExchange", + "FinancialServicesOther", + "FinancialServicesPaydayLending", + "GamingOrGambling", + "HealthCareAndSocialAssistance", + "HospitalityAccommodationOrFoodServices", + "LegalAccountingConsultingOrComputerProgramming", + "Manufacturing", + "Mining", + "Nutraceuticals", + "PersonalCareServices", + "PublicAdministration", + "RealEstate", + "ReligiousCivicAndSocialOrganizations", + "RepairAndMaintenance", + "RetailTrade", + "TechnologyMediaOrTelecom", + "TransportationOrWarehousing", + "Utilities", + "WholesaleTrade", +] + +Revocability = Literal["Revocable", "Irrevocable"] +SourceOfFunds = Literal[ + "Inheritance", "Salary", "Savings", "InvestmentReturns", "Gifts" +] -ReasonCode = Literal["PoorQuality", "NameMismatch", "SSNMismatch", "AddressMismatch", "DOBMismatch", "ExpiredId", - "EINMismatch", "StateMismatch", "Other"] - -ApplicationTypes = Literal["individualApplication", "businessApplication"] class IndividualApplicationDTO(object): - def __init__(self, id: str, created_at: datetime, full_name: FullName, address: Address, date_of_birth: date, - email: str, phone: Phone, status: ApplicationStatus, ssn: Optional[str], message: Optional[str], - ip: Optional[str], ein: Optional[str], dba: Optional[str], - sole_proprietorship: Optional[bool], tags: Optional[Dict[str, str]], - relationships: Optional[Dict[str, Relationship]]): + def __init__( + self, + id: str, + created_at: datetime, + full_name: FullName, + address: Address, + date_of_birth: date, + email: str, + phone: Phone, + status: ApplicationStatus, + ssn: Optional[str], + message: Optional[str], + ip: Optional[str], + ein: Optional[str], + dba: Optional[str], + sole_proprietorship: Optional[bool], + business_vertical: Optional[BusinessVertical], + tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]], + ): self.id = id self.type = "individualApplication" - self.attributes = {"createdAt": created_at, "fullName": full_name, "address": address, - "dateOfBirth": date_of_birth, "email": email, "phone": phone, "status": status, "ssn": ssn, - "message": message, "ip": ip, "ein": ein, "dba": dba, - "soleProprietorship": sole_proprietorship, "tags": tags} + self.attributes = { + "createdAt": created_at, + "fullName": full_name, + "address": address, + "dateOfBirth": date_of_birth, + "email": email, + "phone": phone, + "status": status, + "ssn": ssn, + "message": message, + "ip": ip, + "ein": ein, + "dba": dba, + "soleProprietorship": sole_proprietorship, + "businessVertical": business_vertical, + "tags": tags, + } self.relationships = relationships @staticmethod def from_json_api(_id, _type, attributes, relationships): return IndividualApplicationDTO( - _id, date_utils.to_datetime(attributes["createdAt"]), - FullName.from_json_api(attributes["fullName"]), Address.from_json_api(attributes["address"]), + _id, + date_utils.to_datetime(attributes["createdAt"]), + FullName.from_json_api(attributes["fullName"]), + Address.from_json_api(attributes["address"]), date_utils.to_date(attributes["dateOfBirth"]), - attributes["email"], Phone.from_json_api(attributes["phone"]), attributes["status"], - attributes.get("ssn"), attributes.get("message"), attributes.get("ip"), - attributes.get("ein"), attributes.get("dba"), attributes.get("soleProprietorship"), - attributes.get("tags"), relationships + attributes["email"], + Phone.from_json_api(attributes["phone"]), + attributes["status"], + attributes.get("ssn"), + attributes.get("message"), + attributes.get("ip"), + attributes.get("ein"), + attributes.get("dba"), + attributes.get("soleProprietorship"), + attributes.get("businessVertical"), + attributes.get("tags"), + relationships, ) class BusinessApplicationDTO(object): - def __init__(self, id: str, created_at: datetime, name: str, address: Address, phone: Phone, - status: ApplicationStatus, state_of_incorporation: str, entity_type: EntityType, - contact: BusinessContact, officer: Officer, beneficial_owners: [BeneficialOwner], ssn: Optional[str], - message: Optional[str], ip: Optional[str], ein: Optional[str], dba: Optional[str], - tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + def __init__( + self, + id: str, + created_at: datetime, + name: str, + address: Address, + phone: Phone, + status: ApplicationStatus, + state_of_incorporation: str, + entity_type: EntityType, + contact: BusinessContact, + officer: Officer, + beneficial_owners: [BeneficialOwner], + ssn: Optional[str], + message: Optional[str], + ip: Optional[str], + ein: Optional[str], + dba: Optional[str], + website: Optional[str], + year_of_incorporation: Optional[str], + business_vertical: Optional[BusinessVertical], + annual_revenue: Optional[AnnualRevenue], + number_of_employees: Optional[NumberOfEmployees], + cash_flow: Optional[CashFlow], + countries_of_operation: Optional[List[str]], + operating_address: Optional[Address], + tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]], + ): self.id = id self.type = "businessApplication" - self.attributes = {"createdAt": created_at, "name": name, "address": address, "phone": phone, - "status": status, "ssn": ssn, "stateOfIncorporation": state_of_incorporation, "ssn": ssn, - "message": message, "ip": ip, "ein": ein, "entityType": entity_type, "dba": dba, - "contact": contact, "officer": officer, "beneficialOwners":beneficial_owners, "tags": tags} + self.attributes = { + "createdAt": created_at, + "name": name, + "address": address, + "phone": phone, + "status": status, + "ssn": ssn, + "stateOfIncorporation": state_of_incorporation, + "message": message, + "ip": ip, + "ein": ein, + "entityType": entity_type, + "dba": dba, + "website": website, + "yearOfIncorporation": year_of_incorporation, + "businessVertical": business_vertical, + "annualRevenue": annual_revenue, + "numberOfEmployees": number_of_employees, + "cashFlow": cash_flow, + "countriesOfOperation": countries_of_operation, + "contact": contact, + "officer": officer, + "beneficialOwners": beneficial_owners, + "operatingAddress": operating_address, + "tags": tags, + } self.relationships = relationships - @staticmethod def from_json_api(_id, _type, attributes, relationships): return BusinessApplicationDTO( - _id, date_utils.to_datetime(attributes["createdAt"]), attributes.get("name"), - Address.from_json_api(attributes["address"]), Phone.from_json_api(attributes["phone"]), - attributes["status"], attributes.get("stateOfIncorporation"), attributes.get("entityType"), - BusinessContact.from_json_api(attributes["contact"]), Officer.from_json_api(attributes["officer"]), - BeneficialOwner.from_json_api(attributes["beneficialOwners"]), attributes.get("ssn"), - attributes.get("message"), attributes.get("ip"), attributes.get("ein"), attributes.get("dba"), - attributes.get("tags"), relationships + _id, + date_utils.to_datetime(attributes["createdAt"]), + attributes.get("name"), + Address.from_json_api(attributes["address"]), + Phone.from_json_api(attributes["phone"]), + attributes["status"], + attributes.get("stateOfIncorporation"), + attributes.get("entityType"), + BusinessContact.from_json_api(attributes["contact"]), + Officer.from_json_api(attributes["officer"]), + BeneficialOwner.from_json_api(attributes["beneficialOwners"]), + attributes.get("ssn"), + attributes.get("message"), + attributes.get("ip"), + attributes.get("ein"), + attributes.get("dba"), + attributes.get("website"), + attributes.get("year_of_incorporation"), + attributes.get("business_vertical"), + attributes.get("annual_revenue"), + attributes.get("number_of_employees"), + attributes.get("cash_flow"), + attributes.get("countries_of_operation"), + attributes.get("operating_address"), + attributes.get("tags"), + relationships, ) + ApplicationDTO = Union[IndividualApplicationDTO, BusinessApplicationDTO] + class CreateIndividualApplicationRequest(UnitRequest): - def __init__(self, full_name: FullName, date_of_birth: date, address: Address, email: str, phone: Phone, - ip: str = None, ein: str = None, dba: str = None, sole_proprietorship: bool = None, - passport: str = None, nationality: str = None, ssn = None, - device_fingerprints: Optional[List[DeviceFingerprint]] = None, idempotency_key: str = None, - tags: Optional[Dict[str, str]] = None): + def __init__( + self, + full_name: FullName, + date_of_birth: date, + address: Address, + email: str, + phone: Phone, + ip: str = None, + ein: str = None, + dba: str = None, + sole_proprietorship: bool = None, + passport: str = None, + nationality: str = None, + ssn=None, + device_fingerprints: Optional[List[DeviceFingerprint]] = None, + idempotency_key: str = None, + tags: Optional[Dict[str, str]] = None, + website: str = None, + annual_revenue: Optional[AnnualRevenue] = None, + number_of_employees: Optional[NumberOfEmployees] = None, + business_vertical: Optional[BusinessVertical] = None, + ): self.full_name = full_name self.date_of_birth = date_of_birth self.address = address @@ -89,6 +320,10 @@ def __init__(self, full_name: FullName, date_of_birth: date, address: Address, e self.device_fingerprints = device_fingerprints self.idempotency_key = idempotency_key self.tags = tags + self.annual_revenue = annual_revenue + self.number_of_employees = number_of_employees + self.business_vertical = business_vertical + self.website = website def to_json_api(self) -> Dict: payload = { @@ -100,7 +335,7 @@ def to_json_api(self) -> Dict: "address": self.address, "email": self.email, "phone": self.phone, - } + }, } } @@ -114,7 +349,9 @@ def to_json_api(self) -> Dict: payload["data"]["attributes"]["dba"] = self.dba if self.sole_proprietorship: - payload["data"]["attributes"]["soleProprietorship"] = self.sole_proprietorship + payload["data"]["attributes"][ + "soleProprietorship" + ] = self.sole_proprietorship if self.ssn: payload["data"]["attributes"]["ssn"] = self.ssn @@ -129,11 +366,25 @@ def to_json_api(self) -> Dict: payload["data"]["attributes"]["idempotencyKey"] = self.idempotency_key if self.device_fingerprints: - payload["data"]["attributes"]["deviceFingerprints"] = [e.to_json_api() for e in self.device_fingerprints] + payload["data"]["attributes"]["deviceFingerprints"] = [ + e.to_json_api() for e in self.device_fingerprints + ] if self.tags: payload["data"]["attributes"]["tags"] = self.tags + if self.website: + payload["data"]["attributes"]["website"] = self.website + + if self.annual_revenue: + payload["data"]["attributes"]["annualRevenue"] = self.annual_revenue + + if self.number_of_employees: + payload["data"]["attributes"]["numberOfEmployees"] = self.number_of_employees + + if self.business_vertical: + payload["data"]["attributes"]["businessVertical"] = self.business_vertical + return payload def __repr__(self): @@ -141,9 +392,33 @@ def __repr__(self): class CreateBusinessApplicationRequest(UnitRequest): - def __init__(self, name: str, address: Address, phone: Phone, state_of_incorporation: str, ein: str, - contact: BusinessContact, officer: Officer, beneficial_owners: [BeneficialOwner], - entity_type: EntityType, dba: str = None, ip: str = None, website: str = None): + def __init__( + self, + name: str, + address: Address, + phone: Phone, + state_of_incorporation: str, + ein: str, + contact: BusinessContact, + officer: Officer, + beneficial_owners: [BeneficialOwner], + entity_type: EntityType, + tags: Optional[Dict[str, str]] = None, + dba: str = None, + ip: str = None, + website: str = None, + industry: Optional[Industry] = None, + annual_revenue: Optional[AnnualRevenue] = None, + number_of_employees: Optional[NumberOfEmployees] = None, + cash_flow: Optional[CashFlow] = None, + year_of_incorporation: Optional[str] = None, + countries_of_operation: Optional[List[str]] = None, + stock_symbol: Optional[str] = None, + business_vertical: Optional[BusinessVertical] = None, + device_fingerprints: Optional[List[DeviceFingerprint]] = None, + operating_address: Optional[Address] = None, + idempotency_key: Optional[str] = None, + ): self.name = name self.address = address self.phone = phone @@ -156,8 +431,20 @@ def __init__(self, name: str, address: Address, phone: Phone, state_of_incorpora self.dba = dba self.ip = ip self.website = website + self.tags = tags + self.industry = industry + self.annual_revenue = annual_revenue + self.number_of_employees = number_of_employees + self.cash_flow = cash_flow + self.year_of_incorporation = year_of_incorporation + self.countries_of_operation = countries_of_operation + self.stock_symbol = stock_symbol + self.business_vertical = business_vertical + self.device_fingerprints = device_fingerprints + self.operating_address = operating_address + self.idempotency_key = idempotency_key - def to_json_api(self) -> Dict: + def to_json_api(self) -> dict: payload = { "data": { "type": "businessApplication", @@ -170,20 +457,49 @@ def to_json_api(self) -> Dict: "contact": self.contact, "officer": self.officer, "beneficialOwners": self.beneficial_owners, - "entityType": self.entity_type - } + "entityType": self.entity_type, + "yearOfIncorporation": self.year_of_incorporation, + "businessVertical": self.business_vertical, + }, } } if self.dba: payload["data"]["attributes"]["dba"] = self.dba + if self.tags: + payload["data"]["attributes"]["tags"] = self.tags + if self.ip: payload["data"]["attributes"]["ip"] = self.ip if self.website: payload["data"]["attributes"]["website"] = self.website + if self.industry: + payload["data"]["attributes"]["industry"] = self.industry + + if self.annual_revenue: + payload["data"]["attributes"]["annualRevenue"] = self.annual_revenue + + if self.number_of_employees: + payload["data"]["attributes"]["numberOfEmployees"] = self.number_of_employees + + if self.cash_flow: + payload["data"]["attributes"]["cashFlow"] = self.cash_flow + + if self.countries_of_operation: + payload["data"]["attributes"]["countriesOfOperation"] = self.countries_of_operation + + if self.stock_symbol: + payload["data"]["attributes"]["stockSymbol"] = self.stock_symbol + + if self.operating_address: + payload["data"]["attributes"]["operatingAddress"] = self.operating_address + + if self.idempotency_key: + payload["data"]["attributes"]["idempotencyKey"] = self.idempotency_key + return payload def __repr__(self): @@ -191,30 +507,69 @@ def __repr__(self): class ApplicationDocumentDTO(object): - def __init__(self, id: str, status: ApplicationStatus, document_type: DocumentType, description: str, name: str, - address: Optional[Address], date_of_birth: Optional[date], passport: Optional[str], ein: Optional[str], - reason_code: Optional[ReasonCode], reason: Optional[str]): + def __init__( + self, + id: str, + status: ApplicationDocumentStatus, + document_type: DocumentType, + description: str, + name: str, + address: Optional[Address], + date_of_birth: Optional[date], + passport: Optional[str], + ein: Optional[str], + reason_code: Optional[ReasonCode], + reason: Optional[str], + ): self.id = id self.type = "document" - self.attributes = {"status": status, "documentType": document_type, "description": description, "name": name, - "address": address, "dateOfBirth": date_of_birth, "passport": passport, "ein": ein, - "reasonCode": reason_code, "reason": reason} + self.attributes = { + "status": status, + "documentType": document_type, + "description": description, + "name": name, + "address": address, + "dateOfBirth": date_of_birth, + "passport": passport, + "ein": ein, + "reasonCode": reason_code, + "reason": reason, + } @staticmethod def from_json_api(_id, _type, attributes): - address = Address.from_json_api(attributes.get("address")) if attributes.get("address") else None + address = ( + Address.from_json_api(attributes.get("address")) + if attributes.get("address") + else None + ) return ApplicationDocumentDTO( - _id, attributes["status"], attributes["documentType"], attributes["description"], attributes["name"], - address, attributes.get("dateOfBirth"), attributes.get("passport"), - attributes.get("ein"), attributes.get("reasonCode"), attributes.get("reason") + _id, + attributes["status"], + attributes["documentType"], + attributes["description"], + attributes["name"], + address, + attributes.get("dateOfBirth"), + attributes.get("passport"), + attributes.get("ein"), + attributes.get("reasonCode"), + attributes.get("reason"), ) + FileType = Literal["jpeg", "png", "pdf"] class UploadDocumentRequest(object): - def __init__(self, application_id: str, document_id: str, file: IO, file_type: FileType, - is_back_side: Optional[bool] = False): + def __init__( + self, + application_id: str, + document_id: str, + file: IO, + file_type: FileType, + is_back_side: Optional[bool] = False, + ): self.application_id = application_id self.document_id = document_id self.file = file @@ -223,9 +578,15 @@ def __init__(self, application_id: str, document_id: str, file: IO, file_type: F class ListApplicationParams(UnitParams): - def __init__(self, offset: int = 0, limit: int = 100, email: Optional[str] = None, - tags: Optional[object] = None, query: Optional[str] = None, - sort: Optional[Literal["createdAt", "-createdAt"]] = None): + def __init__( + self, + offset: int = 0, + limit: int = 100, + email: Optional[str] = None, + tags: Optional[object] = None, + query: Optional[str] = None, + sort: Optional[Literal["createdAt", "-createdAt"]] = None, + ): self.offset = offset self.limit = limit self.email = email @@ -245,20 +606,20 @@ def to_dict(self) -> Dict: parameters["sort"] = self.sort return parameters + class PatchApplicationRequest(UnitRequest): - def __init__(self, application_id: str, type: ApplicationTypes = "individualApplication", - tags: Optional[Dict[str, str]] = None): + def __init__( + self, + application_id: str, + type: ApplicationTypes = "individualApplication", + tags: Optional[Dict[str, str]] = None, + ): self.application_id = application_id self.type = type self.tags = tags def to_json_api(self) -> Dict: - payload = { - "data": { - "type": self.type, - "attributes": {} - } - } + payload = {"data": {"type": self.type, "attributes": {}}} if self.tags: payload["data"]["attributes"]["tags"] = self.tags @@ -268,3 +629,16 @@ def to_json_api(self) -> Dict: def __repr__(self): json.dumps(self.to_json_api()) + +class ApproveApplicationSBRequest(UnitRequest): + def __init__(self, application_id: str): + self.application_id = application_id + + def to_json_api(self) -> Dict: + payload = { + "data": {"type": "applicationApprove", "attributes": {"reason": "sandbox"}} + } + return payload + + def __repr__(self): + json.dumps(self.to_json_api()) diff --git a/unit/models/authorization.py b/unit/models/authorization.py index a737cb2d..9f6de45b 100644 --- a/unit/models/authorization.py +++ b/unit/models/authorization.py @@ -7,23 +7,20 @@ class AuthorizationDTO(object): def __init__(self, id: str, created_at: datetime, amount: int, card_last_4_digits: str, status: AuthorizationStatus, - merchant_name: str, - merchant_type: int, merchant_category: str, merchant_location: Optional[str], recurring: bool, - tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + merchant: Merchant, recurring: bool, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): self.id = id self.type = "authorization" self.attributes = {"createdAt": created_at, "amount": amount, "cardLast4Digits": card_last_4_digits, - "status": status, "merchant": {"name": merchant_name, "type": merchant_type, - "category": merchant_category, "location": merchant_location}, + "status": status, "merchant": merchant, "recurring": recurring, "tags": tags} self.relationships = relationships @staticmethod def from_json_api(_id, _type, attributes, relationships): return AuthorizationDTO(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["amount"], - attributes["cardLast4Digits"], attributes["status"], attributes["merchant"]["name"], - attributes["merchant"]["type"], attributes["merchant"]["category"], - attributes["merchant"].get("location"), attributes["recurring"], + attributes["cardLast4Digits"], attributes["status"], + Merchant.from_json_api(attributes["merchant"]), attributes["recurring"], attributes.get("tags"), relationships) diff --git a/unit/models/authorization_request.py b/unit/models/authorization_request.py index 6eed39a4..116047de 100644 --- a/unit/models/authorization_request.py +++ b/unit/models/authorization_request.py @@ -29,12 +29,12 @@ def from_json_api(_id, _type, attributes, relationships): attributes.get("partialApprovalAllowed"), attributes.get("approvedAmount"), attributes.get("declineReason"), attributes["merchant"]["name"], attributes["merchant"]["type"], - attributes["merchant"]["category"], + attributes["merchant"].get("category"), attributes["merchant"].get("location"), attributes["recurring"], attributes.get("tags"), relationships) -class ListPurchaseAuthorizationRequestParams(object): +class ListPurchaseAuthorizationRequestParams(UnitParams): def __init__(self, limit: int = 100, offset: int = 0, account_id: Optional[str] = None, customer_id: Optional[str] = None): self.limit = limit @@ -51,7 +51,7 @@ def to_dict(self) -> Dict: return parameters -class ApproveAuthorizationRequest(object): +class ApproveAuthorizationRequest(UnitRequest): def __init__(self, authorization_id: str, amount: Optional[int] = None, tags: Optional[Dict[str, str]] = None): self.authorization_id = authorization_id self.amount = amount @@ -77,7 +77,7 @@ def __repr__(self): json.dumps(self.to_json_api()) -class DeclineAuthorizationRequest(object): +class DeclineAuthorizationRequest(UnitRequest): def __init__(self, authorization_id: str, reason: DeclineReason): self.authorization_id = authorization_id self.reason = reason @@ -96,3 +96,40 @@ def to_json_api(self) -> Dict: def __repr__(self): json.dumps(self.to_json_api()) + + +class SimulateAuthorizationRequest(UnitRequest): + def __init__(self, amount: int, card_id: str, merchant_name: str, merchant_type: int, merchant_location: str): + self.amount = amount + self.card_id = card_id + self.merchant_name = merchant_name + self.merchant_type = merchant_type + self.merchant_location = merchant_location + + def to_json_api(self) -> Dict: + payload = { + "data": { + "type": "purchaseAuthorizationRequest", + "attributes": { + "amount": self.amount, + "merchantName": self.merchant_name, + "merchantType": self.merchant_type, + "merchantLocation": self.merchant_location, + "recurring": False + }, + "relationships": { + "card": { + "data": { + "type": "card", + "id": self.card_id + } + } + } + + } + } + + return payload + + def __repr__(self): + json.dumps(self.to_json_api()) diff --git a/unit/models/batch_release.py b/unit/models/batch_release.py new file mode 100644 index 00000000..4e65af13 --- /dev/null +++ b/unit/models/batch_release.py @@ -0,0 +1,60 @@ +from unit.models import * + + +class BatchReleaseDTO(object): + def __init__(self, id: str, amount: int, description: str, sender_name: str, sender_address: Address, + sender_account_number: str, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + self.id = id + self.type = "batchRelease" + self.attributes = { + "amount": amount, + "description": description, + "senderName": sender_name, + "senderAccountNumber": sender_account_number, + "senderAddress": sender_address, + "tags": tags, + } + self.relationships = relationships + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return BatchReleaseDTO(_id, attributes["amount"], attributes["description"], attributes["senderName"], Address.from_json_api(attributes["senderAddress"]), attributes["senderAccountNumber"], attributes.get("tags"), relationships) + + +class CreateBatchRelease(UnitRequest): + def __init__(self, amount: int, description: str, sender_name: str, sender_address: Address, + sender_account_number: str, relationships: Optional[Dict[str, Relationship]], tags: Optional[Dict[str, str]] = None, + idempotency_key: Optional[str] = None): + self.amount = amount + self.description = description + self.sender_name = sender_name + self.sender_address = sender_address + self.sender_account_number = sender_account_number + self.tags = tags + self.idempotency_key = idempotency_key + self.relationships = relationships + + def to_json_api(self) -> Dict: + payload = { + "type": "batchRelease", + "attributes": { + "amount": self.amount, + "description": self.description, + "senderName": self.sender_name, + "senderAccountNumber": self.sender_account_number, + "senderAddress": self.sender_address + }, + "relationships": self.relationships + } + + if self.idempotency_key: + payload["attributes"]["idempotencyKey"] = self.idempotency_key + + if self.tags: + payload["attributes"]["tags"] = self.tags + + return payload + + def __repr__(self): + return json.dumps(self.to_json_api()) diff --git a/unit/models/benificial_owner.py b/unit/models/benificial_owner.py new file mode 100644 index 00000000..3b3fcd12 --- /dev/null +++ b/unit/models/benificial_owner.py @@ -0,0 +1,26 @@ +import json +from typing import Optional +from unit.models import * +from unit.utils import date_utils + + +class BenificialOwnerDTO(object): + def __init__(self, full_name: FullName, date_of_birth: date, address: Address, phone: Phone, email: str, + status: Optional[Status] = None, ssn: Optional[str] = None, passport: Optional[str] = None, + nationality: Optional[str] = None, percentage: Optional[int] = None): + self.full_name = full_name + self.date_of_birth = date_of_birth + self.address = address + self.phone = phone + self.email = email + self.status = status + self.ssn = ssn + self.passport = passport + self.nationality = nationality + self.percentage = percentage + + @staticmethod + def from_json_api(attributes): + return BenificialOwnerDTO(attributes.get("fullName"), attributes.get("dateOfBirth"), attributes.get("address"), + attributes.get("phone"), attributes.get("email"), attributes.get("status"), attributes.get("ssn"), + attributes.get("passport"), attributes.get("nationality"), attributes.get("percentage")) diff --git a/unit/models/card.py b/unit/models/card.py index 5e7b7699..c8ed53f8 100644 --- a/unit/models/card.py +++ b/unit/models/card.py @@ -24,31 +24,55 @@ def from_json_api(_id, _type, attributes, relationships): ) -class BusinessDebitCardDTO(object): - def __init__(self, id: str, created_at: datetime, last_4_digits: str, expiration_date: str, ssn: str, - full_name: FullName, date_of_birth: date, address: Address, phone: Phone, email: str, - status: CardStatus, passport: Optional[str], nationality: Optional[str], +class BusinessCardDTO(object): + def __init__(self, _id: str, _type: str, created_at: datetime, last_4_digits: str, expiration_date: str, + ssn: Optional[str], full_name: FullName, date_of_birth: date, address: Address, phone: Phone, + email: str, status: CardStatus, passport: Optional[str], nationality: Optional[str], shipping_address: Optional[Address], design: Optional[str], - relationships: Optional[Dict[str, Relationship]]): - self.id = id - self.type = "businessDebitCard" + relationships: Optional[Dict[str, Relationship]], tags: Optional[Dict[str, str]]): + self.id = _id + self.type = _type self.attributes = {"createdAt": created_at, "last4Digits": last_4_digits, "expirationDate": expiration_date, "ssn": ssn, "fullName": full_name, "dateOfBirth": date_of_birth, "address": address, "phone": phone, "email": email, "status": status, "passport": passport, - "nationality": nationality, "shippingAddress": shipping_address, "design": design} + "nationality": nationality, "shippingAddress": shipping_address, "design": design, + "tags": tags} self.relationships = relationships + @staticmethod def from_json_api(_id, _type, attributes, relationships): - shipping_address = Address.from_json_api(attributes.get("shippingAddress")) if attributes.get("shippingAddress") else None - return BusinessDebitCardDTO( - _id, date_utils.to_datetime(attributes["createdAt"]), attributes["last4Digits"], - attributes["expirationDate"], attributes["ssn"], FullName.from_json_api(attributes["fullName"]), + return BusinessCardDTO( + _id, _type, date_utils.to_datetime(attributes["createdAt"]), attributes["last4Digits"], + attributes["expirationDate"], attributes.get("ssn"), FullName.from_json_api(attributes["fullName"]), attributes["dateOfBirth"], Address.from_json_api(attributes["address"]), Phone.from_json_api(attributes["phone"]), attributes["email"], attributes["status"], attributes.get("passport"), attributes.get("nationality"), - shipping_address, attributes.get("design"), relationships + Address.from_json_api(attributes.get("shippingAddress")), attributes.get("design"), relationships, + attributes.get("tags") ) +class BusinessDebitCardDTO(BusinessCardDTO): + def __init__(self, card: BusinessCardDTO): + self.id = card.id + self.type = card.type + self.attributes = card.attributes + self.relationships = card.relationships + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return BusinessDebitCardDTO(BusinessCardDTO.from_json_api(_id, _type, attributes, relationships)) + + +class BusinessCreditCardDTO(BusinessCardDTO): + def __init__(self, card: BusinessCardDTO): + self.id = card.id + self.type = card.type + self.attributes = card.attributes + self.relationships = card.relationships + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return BusinessCreditCardDTO(BusinessCardDTO.from_json_api(_id, _type, attributes, relationships)) class IndividualVirtualDebitCardDTO(object): def __init__(self, id: str, created_at: datetime, last_4_digits: str, expiration_date: str, status: CardStatus, @@ -68,37 +92,71 @@ def from_json_api(_id, _type, attributes, relationships): class BusinessVirtualDebitCardDTO(object): - def __init__(self, id: str, created_at: datetime, last_4_digits: str, expiration_date: str, ssn: str, + def __init__(self, id: str, created_at: datetime, last_4_digits: str, expiration_date: str, full_name: FullName, date_of_birth: date, address: Address, phone: Phone, email: str, - status: CardStatus, passport: Optional[str], nationality: Optional[str], - relationships: Optional[Dict[str, Relationship]]): + status: CardStatus, passport: Optional[str] = None, nationality: Optional[str] = None, + relationships: Optional[Dict[str, Relationship]] = None): self.id = id self.type = "businessVirtualDebitCard" self.attributes = {"createdAt": created_at, "last4Digits": last_4_digits, "expirationDate": expiration_date, - "ssn": ssn, "fullName": full_name, "dateOfBirth": date_of_birth, "address": address, + "fullName": full_name, "dateOfBirth": date_of_birth, "address": address, "phone": phone, "email": email, "status": status, "passport": passport, "nationality": nationality} - self.relationships = relationships + self.relationships = relationships or {} def from_json_api(_id, _type, attributes, relationships): return BusinessVirtualDebitCardDTO( _id, date_utils.to_datetime(attributes["createdAt"]), attributes["last4Digits"], - attributes["expirationDate"], attributes["ssn"], FullName.from_json_api(attributes["fullName"]), + attributes["expirationDate"], FullName.from_json_api(attributes["fullName"]), attributes["dateOfBirth"], Address.from_json_api(attributes["address"]), Phone.from_json_api(attributes["phone"]), attributes["email"], attributes["status"], attributes.get("passport"), attributes.get("nationality"), relationships ) +class BusinessVirtualCardDTO(object): + def __init__(self, _id: str, _type: str, created_at: datetime, last_4_digits: str, expiration_date: str, + ssn: Optional[str], full_name: FullName, date_of_birth: date, address: Address, phone: Phone, + email: str, status: CardStatus, passport: Optional[str], nationality: Optional[str], + relationships: Optional[Dict[str, Relationship]], tags: Optional[Dict[str, str]]): + self.id = _id + self.type = _type + self.attributes = {"createdAt": created_at, "last4Digits": last_4_digits, "expirationDate": expiration_date, + "ssn": ssn, "fullName": full_name, "dateOfBirth": date_of_birth, "address": address, + "phone": phone, "email": email, "status": status, "passport": passport, + "nationality": nationality, "tags": tags} + self.relationships = relationships + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return BusinessVirtualCardDTO( + _id, _type, date_utils.to_datetime(attributes["createdAt"]), attributes["last4Digits"], + attributes["expirationDate"], attributes.get("ssn"), FullName.from_json_api(attributes["fullName"]), + attributes["dateOfBirth"], Address.from_json_api(attributes["address"]), + Phone.from_json_api(attributes["phone"]), attributes["email"], attributes["status"], + attributes.get("passport"), attributes.get("nationality"), relationships, attributes.get("tags")) -Card = Union[IndividualDebitCardDTO, BusinessDebitCardDTO, IndividualVirtualDebitCardDTO, BusinessVirtualDebitCardDTO] +class BusinessVirtualCreditCardDTO(BusinessVirtualCardDTO): + def __init__(self, card: BusinessVirtualCardDTO): + self.id = card.id + self.type = card.type + self.attributes = card.attributes + self.relationships = card.relationships + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return BusinessVirtualCreditCardDTO(BusinessVirtualCardDTO.from_json_api(_id, _type, attributes, relationships)) -class CreateIndividualDebitCard(object): - def __init__(self, relationships: Dict[str, Relationship], shipping_address: Optional[Address] = None, - design: Optional[str] = None, idempotency_key: Optional[str] = None, - tags: Optional[Dict[str, str]] = None): +Card = Union[IndividualDebitCardDTO, BusinessDebitCardDTO, IndividualVirtualDebitCardDTO, BusinessVirtualDebitCardDTO, + BusinessVirtualCreditCardDTO, BusinessCreditCardDTO] + + +class CreateIndividualDebitCard(UnitRequest): + def __init__(self, relationships: Dict[str, Relationship], limits: Optional[CardLevelLimits] = None, + shipping_address: Optional[Address] = None, design: Optional[str] = None, + idempotency_key: Optional[str] = None, tags: Optional[Dict[str, str]] = None): self.shipping_address = shipping_address self.design = design + self.limits = limits self.idempotency_key = idempotency_key self.tags = tags self.relationships = relationships @@ -115,6 +173,9 @@ def to_json_api(self) -> Dict: if self.shipping_address: payload["data"]["attributes"]["shippingAddress"] = self.shipping_address + if self.limits: + payload["data"]["attributes"]["limits"] = self.limits + if self.design: payload["data"]["attributes"]["design"] = self.design @@ -129,17 +190,18 @@ def to_json_api(self) -> Dict: def __repr__(self): json.dumps(self.to_json_api()) -class CreateBusinessDebitCard(object): +class CreateBusinessCard(UnitRequest): def __init__(self, full_name: FullName, date_of_birth: date, address: Address, phone: Phone, email: str, - status: CardStatus, shipping_address: Optional[Address], ssn: Optional[str], passport: Optional[str], - nationality: Optional[str], design: Optional[str], idempotency_key: Optional[str], - tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + relationships: Dict[str, Relationship], shipping_address: Optional[Address] = None, + ssn: Optional[str] = None, passport: Optional[str] = None, nationality: Optional[str] = None, + design: Optional[str] = None, idempotency_key: Optional[str] = None, + tags: Optional[Dict[str, str]] = None, limits: Optional[CardLevelLimits] = None, + additional_embossed_text: Optional[str] = None, print_only_business_name: Optional[bool] = None): self.full_name = full_name self.date_of_birth = date_of_birth self.address = address self.phone = phone self.email = email - self.status = status self.shipping_address = shipping_address self.ssn = ssn self.passport = passport @@ -148,11 +210,14 @@ def __init__(self, full_name: FullName, date_of_birth: date, address: Address, p self.idempotency_key = idempotency_key self.tags = tags self.relationships = relationships + self.limits = limits + self.additional_embossed_text = additional_embossed_text + self.print_only_business_name = print_only_business_name - def to_json_api(self) -> Dict: + def to_json_api(self, _type: str) -> Dict: payload = { "data": { - "type": "businessDebitCard", + "type": _type, "attributes": { "fullName": self.full_name, "dateOfBirth": self.date_of_birth, @@ -185,15 +250,35 @@ def to_json_api(self) -> Dict: if self.tags: payload["data"]["attributes"]["tags"] = self.tags + if self.limits: + payload["data"]["attributes"]["limits"] = self.limits + + if self.additional_embossed_text: + payload["data"]["attributes"]["additionalEmbossedText"] = self.additional_embossed_text + + if self.print_only_business_name is not None: + payload["data"]["attributes"]["printOnlyBusinessName"] = self.print_only_business_name + return payload def __repr__(self): - json.dumps(self.to_json_api()) + return json.dumps(self.to_json_api()) + -class CreateIndividualVirtualDebitCard(object): +class CreateBusinessDebitCard(CreateBusinessCard): + def to_json_api(self): + return super().to_json_api("businessDebitCard") + + +class CreateBusinessCreditCard(CreateBusinessCard): + def to_json_api(self): + return super().to_json_api("businessCreditCard") + +class CreateIndividualVirtualDebitCard(UnitRequest): def __init__(self, relationships: Dict[str, Relationship], idempotency_key: Optional[str] = None, - tags: Optional[Dict[str, str]] = None): + limits: Optional[CardLevelLimits] = None, tags: Optional[Dict[str, str]] = None): self.idempotency_key = idempotency_key + self.limits = limits self.tags = tags self.relationships = relationships @@ -209,6 +294,9 @@ def to_json_api(self) -> Dict: if self.idempotency_key: payload["data"]["attributes"]["idempotencyKey"] = self.idempotency_key + if self.limits: + payload["data"]["attributes"]["limits"] = self.limits + if self.tags: payload["data"]["attributes"]["tags"] = self.tags @@ -218,28 +306,28 @@ def __repr__(self): json.dumps(self.to_json_api()) -class CreateBusinessVirtualDebitCard(object): +class CreateBusinessVirtualCard(UnitRequest): def __init__(self, full_name: FullName, date_of_birth: date, address: Address, phone: Phone, email: str, - status: CardStatus, ssn: Optional[str], passport: Optional[str], nationality: Optional[str], - idempotency_key: Optional[str], tags: Optional[Dict[str, str]], - relationships: Optional[Dict[str, Relationship]]): + relationships: Dict[str, Relationship], ssn: Optional[str] = None, passport: Optional[str] = None, + nationality: Optional[str] = None, idempotency_key: Optional[str] = None, + tags: Optional[Dict[str, str]] = None, limits: Optional[CardLevelLimits] = None): self.full_name = full_name self.date_of_birth = date_of_birth self.address = address self.phone = phone self.email = email - self.status = status self.ssn = ssn self.passport = passport self.nationality = nationality self.idempotency_key = idempotency_key self.tags = tags self.relationships = relationships + self.limits = limits - def to_json_api(self) -> Dict: + def to_json_api(self, _type: str) -> Dict: payload = { "data": { - "type": "businessVirtualDebitCard", + "type": _type, "attributes": { "fullName": self.full_name, "dateOfBirth": self.date_of_birth, @@ -266,21 +354,35 @@ def to_json_api(self) -> Dict: if self.tags: payload["data"]["attributes"]["tags"] = self.tags + if self.limits: + payload["data"]["attributes"]["limits"] = self.limits + return payload def __repr__(self): - json.dumps(self.to_json_api()) + return json.dumps(self.to_json_api()) + + +class CreateBusinessVirtualDebitCard(CreateBusinessVirtualCard): + def to_json_api(self): + return super().to_json_api("businessVirtualDebitCard") + + +class CreateBusinessVirtualCreditCard(CreateBusinessVirtualCard): + def to_json_api(self): + return super().to_json_api("businessVirtualCreditCard") CreateCardRequest = Union[CreateIndividualDebitCard, CreateBusinessDebitCard, CreateIndividualVirtualDebitCard, - CreateBusinessVirtualDebitCard] + CreateBusinessVirtualDebitCard, CreateBusinessVirtualCreditCard, CreateBusinessCreditCard] -class PatchIndividualDebitCard(object): +class PatchIndividualDebitCard(UnitRequest): def __init__(self,card_id: str, shipping_address: Optional[Address] = None, design: Optional[str] = None, - tags: Optional[Dict[str, str]] = None): + limits: CardLevelLimits = None, tags: Optional[Dict[str, str]] = None): self.card_id = card_id self.shipping_address = shipping_address self.design = design + self.limits = limits self.tags = tags def to_json_api(self) -> Dict: @@ -294,6 +396,9 @@ def to_json_api(self) -> Dict: if self.shipping_address: payload["data"]["attributes"]["shippingAddress"] = self.shipping_address + if self.limits: + payload["data"]["attributes"]["limits"] = self.limits + if self.design: payload["data"]["attributes"]["design"] = self.design @@ -306,19 +411,23 @@ def __repr__(self): json.dumps(self.to_json_api()) -class PatchBusinessDebitCard(object): +class PatchBusinessCard(UnitRequest): def __init__(self, card_id: str, shipping_address: Optional[Address] = None, address: Optional[Address] = None, phone: Optional[Phone] = None, email: Optional[str] = None, design: Optional[str] = None, - tags: Optional[Dict[str, str]] = None): + tags: Optional[Dict[str, str]] = None, limits: Optional[CardLevelLimits] = None): self.card_id = card_id self.shipping_address = shipping_address + self.address = address + self.phone = phone + self.email = email self.design = design self.tags = tags + self.limits = limits - def to_json_api(self) -> Dict: + def to_json_api(self, _type: str = "businessDebitCard") -> Dict: payload = { "data": { - "type": "businessDebitCard", + "type": _type, "attributes": {}, } } @@ -341,14 +450,27 @@ def to_json_api(self) -> Dict: if self.tags: payload["data"]["attributes"]["tags"] = self.tags + if self.limits: + payload["data"]["attributes"]["limits"] = self.limits + return payload def __repr__(self): - json.dumps(self.to_json_api()) + return json.dumps(self.to_json_api()) -class PatchIndividualVirtualDebitCard(object): - def __init__(self, card_id: str, tags: Optional[Dict[str, str]] = None): + +class PatchBusinessDebitCard(PatchBusinessCard): + pass + + +class PatchBusinessCreditCard(PatchBusinessCard): + def to_json_api(self) -> Dict: + return super().to_json_api("businessCreditCard") + +class PatchIndividualVirtualDebitCard(UnitRequest): + def __init__(self, card_id: str, limits: CardLevelLimits = None, tags: Optional[Dict[str, str]] = None): self.card_id = card_id + self.limits = limits self.tags = tags def to_json_api(self) -> Dict: @@ -359,27 +481,31 @@ def to_json_api(self) -> Dict: } } - if self.tags: - payload["data"]["attributes"]["tags"] = self.tags + if self.limits: + payload["data"]["attributes"]["limits"] = self.limits + + payload["data"]["attributes"]["tags"] = self.tags or {} return payload def __repr__(self): json.dumps(self.to_json_api()) -class PatchBusinessVirtualDebitCard(object): +class PatchBusinessVirtualCard(UnitRequest): def __init__(self, card_id: str, address: Optional[Address] = None, phone: Optional[Phone] = None, - email: Optional[str] = None, tags: Optional[Dict[str, str]] = None): + email: Optional[str] = None, tags: Optional[Dict[str, str]] = None, + _type: str = "businessVirtualDebitCard", limits: Optional[CardLevelLimits] = None): self.card_id = card_id self.address = address self.phone = phone self.email = email self.tags = tags + self.limits = limits - def to_json_api(self) -> Dict: + def to_json_api(self, _type: str = "businessVirtualDebitCard") -> Dict: payload = { "data": { - "type": "businessVirtualDebitCard", + "type": _type, "attributes": {}, } } @@ -396,13 +522,23 @@ def to_json_api(self) -> Dict: if self.tags: payload["data"]["attributes"]["tags"] = self.tags + if self.limits: + payload["data"]["attributes"]["limits"] = self.limits + return payload - def __repr__(self): - json.dumps(self.to_json_api()) + +class PatchBusinessVirtualDebitCard(PatchBusinessVirtualCard): + pass + + +class PatchBusinessVirtualCreditCard(PatchBusinessVirtualCard): + def to_json_api(self) -> Dict: + return super().to_json_api("businessVirtualCreditCard") + PatchCardRequest = Union[PatchIndividualDebitCard, PatchBusinessDebitCard, PatchIndividualVirtualDebitCard, - PatchBusinessVirtualDebitCard] + PatchBusinessVirtualDebitCard, PatchBusinessCreditCard, PatchBusinessVirtualCreditCard] class ReplaceCardRequest(object): def __init__(self, shipping_address: Optional[Address] = None): @@ -421,8 +557,8 @@ def to_json_api(self) -> Dict: return payload - def __repr__(self): - json.dumps(self.to_json_api()) + def __repr__(self): + json.dumps(self.to_json_api()) PinStatus = Literal["Set", "NotSet"] @@ -450,13 +586,17 @@ def from_json_api(attributes): class ListCardParams(UnitParams): def __init__(self, offset: int = 0, limit: int = 100, account_id: Optional[str] = None, - customer_id: Optional[str] = None, tags: Optional[object] = None, include: Optional[str] = None): + customer_id: Optional[str] = None, tags: Optional[Dict[str, str]] = None, include: Optional[str] = None, + sort: Optional[Literal["createdAt", "-createdAt"]] = None, + status: Optional[List[CardStatus]] = None): self.offset = offset self.limit = limit self.account_id = account_id self.customer_id = customer_id self.tags = tags self.include = include + self.sort = sort + self.status = status def to_dict(self) -> Dict: parameters = {"page[limit]": self.limit, "page[offset]": self.offset} @@ -465,8 +605,13 @@ def to_dict(self) -> Dict: if self.account_id: parameters["filter[accountId]"] = self.account_id if self.tags: - parameters["filter[tags]"] = self.tags + parameters["filter[tags]"] = json.dumps(self.tags) if self.include: parameters["include"] = self.include + if self.sort: + parameters["sort"] = self.sort + if self.status: + for idx, status_filter in enumerate(self.status): + parameters[f"filter[status][{idx}]"] = status_filter return parameters diff --git a/unit/models/check_deposit.py b/unit/models/check_deposit.py new file mode 100644 index 00000000..a240fe19 --- /dev/null +++ b/unit/models/check_deposit.py @@ -0,0 +1,63 @@ +import json +from typing import Optional +from unit.models import * +from unit.utils import date_utils + +CheckDepositStatus = Literal[ + "AwaitingImages", "AwaitingFrontImage", "AwaitingBackImage", "Pending", "PendingReview", "Rejected", "Clearing", "Sent", "Canceled", "Returned", +] + + +class CheckDepositDTO(object): + def __init__( + self, + id: str, + created_at: datetime, + status: CheckDepositStatus, + reason: Optional[str], + description: str, + amount: int, + check_number: str, + counterparty: Optional[CheckCounterparty], + settlement_date: Optional[datetime], + tags: Optional[Dict[str, str]], + relationships: Dict[str, Relationship] + ): + self.id = id + self.type = "authorization" + self.attributes = { + "createdAt": created_at, + "status": status, + "description": description, + "amount": amount, + "checkNumber": check_number, + } + if reason: + self.attributes["reason"] = reason + if counterparty: + self.attributes["counterparty"] = counterparty + if settlement_date: + self.attributes["settlementDate"] = settlement_date + if tags: + self.attributes["tages"] = tags + self.relationships = relationships + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + settlement_date = attributes.get("settlementDate") + if settlement_date: + settlement_date = date_utils.to_datetime(settlement_date) + + return CheckDepositDTO( + id=_id, + created_at=date_utils.to_datetime(attributes["createdAt"]), + status=attributes["status"], + reason=attributes.get("reason"), + description=attributes["description"], + amount=attributes["amount"], + check_number=attributes.get("checkNumber"), + counterparty=attributes.get("counterparty"), + settlementDate=settlement_date, + tags=attributes.get("tags"), + relationships=relationships, + ) diff --git a/unit/models/check_payment.py b/unit/models/check_payment.py new file mode 100644 index 00000000..87a16f87 --- /dev/null +++ b/unit/models/check_payment.py @@ -0,0 +1,162 @@ +import json +from typing import Optional, Literal +from unit.models import * +from unit.utils import date_utils + + +CheckPaymentStatus = Literal["New", "Pending", "PendingCancellation", "Canceled", "InDelivery", "Delivered", + "ReturnedToSender", "Processed", "PendingReview", "MarkedForReturn", "Returned", "Rejected"] +CheckPaymentAdditionalVerificationStatus = Literal["Required", "NotRequired", "Approved"] +CheckPaymentReturnStatusReason = Literal[ + "NotSufficientFunds", + "UncollectedFundsHold", + "StopPayment", + "ClosedAccount", + "UnableToLocateAccount", + "FrozenOrBlockedAccount", + "StaleDated", + "PostDated", + "NotValidCheckOrCashItem", + "AlteredOrFictitious", + "UnableToProcess", + "ItemExceedsDollarLimit", + "NotAuthorized", + "ReferToMaker", + "UnusableImage", + "DuplicatePresentment", + "WarrantyBreach", + "UnauthorizedWarrantyBreach" +] +CheckPaymentDeliveryStatus = Literal["Mailed", "InLocalArea", "Delivered", "Rerouted", "ReturnedToSender"] + + +class CheckPaymentDTO(object): + def __init__(self, id: str, created_at: datetime, updated_at: datetime, amount: int, status: CheckPaymentStatus, + description: str, originated: bool, check_number: Optional[str], on_us: Optional[str], + on_us_auxiliary: Optional[str], counterparty_routing_number: Optional[str], + return_status_reason: Optional[CheckPaymentReturnStatusReason], reject_reason: Optional[str], + pending_review_reasons: Optional[List[str]], return_cutoff_time: Optional[datetime], + additional_verification_status: Optional[CheckPaymentAdditionalVerificationStatus], + tags: Optional[Dict[str, str]], delivery_status: Optional[CheckPaymentDeliveryStatus], + tracked_at: Optional[datetime], postal_code: Optional[str], expiration_date: Optional[date], + expected_delivery: Optional[date], send_at: Optional[datetime], + counterparty_name: Optional[str], counterparty_moved: Optional[bool], + counterparty_street: Optional[str], + counterparty_street2: Optional[str], counterparty_city: Optional[str], + counterparty_state: Optional[str], + counterparty_postal_code: Optional[str], counterparty_country: Optional[str], memo: Optional[str], + relationships: Optional[Dict[str, Relationship]]): + self.id = id + self.type = "checkPayment" + self.attributes = self.attributes = { + "createdAt": created_at, + "updatedAt": updated_at, + "amount": amount, + "status": status, + "description": description, + "checkNumber": check_number, + "originated": originated, + "onUs": on_us, + "onUsAuxiliary": on_us_auxiliary, + "counterpartyRoutingNumber": counterparty_routing_number, + "returnStatusReason": return_status_reason, + "rejectReason": reject_reason, + "pendingReviewReasons": pending_review_reasons, + "returnCutoffTime": return_cutoff_time, + "additionalVerificationStatus": additional_verification_status, + "tags": tags, + "deliveryStatus": delivery_status, + "trackedAt": tracked_at, + "postalCode": postal_code, + "expirationDate": expiration_date, + "expectedDelivery": expected_delivery, + "sendAt": send_at, + "counterparty": { + "name": counterparty_name, + "moved": counterparty_moved, + "address": { + "street": counterparty_street, + "street2": counterparty_street2, + "city": counterparty_city, + "state": counterparty_state, + "postalCode": counterparty_postal_code, + "country": counterparty_country, + } + }, + "memo": memo, + } + self.relationships = relationships + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentDTO( + id=_id, + created_at=date_utils.to_datetime(attributes["createdAt"]), + updated_at=date_utils.to_datetime(attributes["updatedAt"]), + amount=attributes["amount"], + status=attributes["status"], + description=attributes["description"], + check_number=attributes.get("checkNumber"), + originated=attributes["originated"], + on_us=attributes.get("onUs"), + on_us_auxiliary=attributes.get("onUsAuxiliary"), + counterparty_routing_number=attributes.get("counterpartyRoutingNumber"), + return_status_reason=attributes.get("returnStatusReason"), + reject_reason=attributes.get("rejectReason"), + pending_review_reasons=attributes.get("pendingReviewReasons"), + return_cutoff_time=attributes.get("returnCutoffTime"), + additional_verification_status=attributes.get("additionalVerificationStatus"), + tags=attributes.get("tags"), + delivery_status=attributes.get("deliveryStatus"), + tracked_at=attributes.get("trackedAt"), + postal_code=attributes.get("postalCode"), + expiration_date=attributes.get("expirationDate"), + expected_delivery=attributes.get("expectedDelivery"), + send_at=attributes.get("sendAt"), + counterparty_name=attributes.get("counterparty", {}).get("name"), + counterparty_moved=attributes.get("counterparty", {}).get("moved"), + counterparty_street=attributes.get("counterparty", {}).get("address", {}).get("street"), + counterparty_street2=attributes.get("counterparty", {}).get("address", {}).get("street2"), + counterparty_city=attributes.get("counterparty", {}).get("address", {}).get("city"), + counterparty_state=attributes.get("counterparty", {}).get("address", {}).get("state"), + counterparty_postal_code=attributes.get("counterparty", {}).get("address", {}).get("postalCode"), + counterparty_country=attributes.get("counterparty", {}).get("address", {}).get("country"), + memo=attributes.get("memo"), + relationships=relationships, + ) + + +class ApproveCheckPaymentRequest(UnitRequest): + def __init__(self, check_payment_id: str): + self.check_payment_id = check_payment_id + + def to_json_api(self) -> Dict: + payload = { + "data": { + "type": "additionalVerification", + } + } + return payload + + def __repr__(self): + json.dumps(self.to_json_api()) + + +class ReturnCheckPaymentRequest(UnitRequest): + def __init__(self, check_payment_id: str, reason: CheckPaymentReturnStatusReason): + self.check_payment_id = check_payment_id + self.reason = reason + + def to_json_api(self) -> Dict: + payload = { + "data": { + "type": "checkPaymentReturn", + "attributes": { + "reason": self.reason, + } + } + } + return payload + + def __repr__(self): + return json.dumps(self.to_json_api()) diff --git a/unit/models/check_registered_address.py b/unit/models/check_registered_address.py new file mode 100644 index 00000000..6862a4c8 --- /dev/null +++ b/unit/models/check_registered_address.py @@ -0,0 +1,55 @@ +from unit.models import * + + + +class CheckRegisteredAddressRequest(UnitRequest): + def __init__( + self, + street: str, + city: str, + state: str, + postal_code: str, + country: str, + street2: Optional[str] = None, + ): + self.type = "checkRegisteredAgentAddress" + + self.attributes = { + "address": { + "street": street, + "city": city, + "state": state, + "postalCode": postal_code, + "country": country, + } + } + + if street2: + self.attributes["address"]["street2"] = street2 + + def to_json_api(self) -> Dict: + payload = { + "data": { + "type": self.type, + "attributes": self.attributes, + } + } + return payload + + +class CheckRegisteredAddressResponse(UnitDTO): + def __init__( + self, + is_registered_agent_address: bool, + ): + self.type = "checkRegisteredAgentAddress" + self.is_registered_agent_address = is_registered_agent_address + self.attributes = { + "isRegisteredAgentAddress": is_registered_agent_address, + } + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckRegisteredAddressResponse( + attributes.get("isRegisteredAgentAddress"), + ) diff --git a/unit/models/check_stop_payment.py b/unit/models/check_stop_payment.py new file mode 100644 index 00000000..e5598ac4 --- /dev/null +++ b/unit/models/check_stop_payment.py @@ -0,0 +1,33 @@ +from unit.models import * +from unit.utils import date_utils + + +StopPaymentStatus = Literal["Active", "Disabled"] + +class CheckStopPaymentDTO(object): + def __init__(self, id: str, created_at: datetime, updated_at: datetime, amount: int, status: StopPaymentStatus, + check_number: str, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + self.id = id + self.type = "checkStopPayment" + self.attributes = self.attributes = { + "createdAt": created_at, + "updatedAt": updated_at, + "amount": amount, + "status": status, + "checkNumber": check_number, + "tags": tags, + } + self.relationships = relationships + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckStopPaymentDTO( + id=_id, + created_at=date_utils.to_datetime(attributes["createdAt"]), + updated_at=date_utils.to_datetime(attributes["updatedAt"]), + amount=attributes["amount"], + status=attributes["status"], + check_number=attributes["checkNumber"], + tags=attributes.get("tags"), + relationships=relationships, + ) diff --git a/unit/models/codecs.py b/unit/models/codecs.py index 45bd1f6c..1eb1cb31 100644 --- a/unit/models/codecs.py +++ b/unit/models/codecs.py @@ -1,6 +1,10 @@ import json from unit.models import * from datetime import datetime, date + +from unit.models.batch_release import BatchReleaseDTO +from unit.models.check_payment import CheckPaymentDTO +from unit.models.reward import RewardDTO from unit.utils import date_utils from unit.models.applicationForm import ApplicationFormDTO from unit.models.application import IndividualApplicationDTO, BusinessApplicationDTO, ApplicationDocumentDTO @@ -9,8 +13,8 @@ from unit.models.card import IndividualDebitCardDTO, BusinessDebitCardDTO, IndividualVirtualDebitCardDTO,\ BusinessVirtualDebitCardDTO, PinStatusDTO, CardLimitsDTO from unit.models.transaction import * -from unit.models.payment import AchPaymentDTO, BookPaymentDTO, WirePaymentDTO, AchReceivedPaymentDTO -from unit.models.payment import AchPaymentDTO, BookPaymentDTO, WirePaymentDTO, BillPaymentDTO +from unit.models.payment import AchPaymentDTO, BookPaymentDTO, WirePaymentDTO, AchReceivedPaymentDTO, BillPaymentDTO, \ + SimulateIncomingAchPaymentDTO, PushToCardPaymentDTO from unit.models.customerToken import CustomerTokenDTO, CustomerVerificationTokenDTO from unit.models.fee import FeeDTO from unit.models.event import * @@ -24,6 +28,8 @@ from unit.models.authorization import AuthorizationDTO from unit.models.authorization_request import PurchaseAuthorizationRequestDTO from unit.models.account_end_of_day import AccountEndOfDayDTO +from unit.models.accrued_interest import AccruedInterestTotalDTO +from unit.models.benificial_owner import BenificialOwnerDTO mappings = { "individualApplication": lambda _id, _type, attributes, relationships: @@ -110,8 +116,14 @@ "returnedCheckDepositTransaction": lambda _id, _type, attributes, relationships: ReturnedCheckDepositTransactionDTO.from_json_api(_id, _type, attributes, relationships), - "paymentAdvanceTransaction": lambda _id, _type, attributes, relationships: - PaymentAdvanceTransactionTransactionDTO.from_json_api(_id, _type, attributes, relationships), + "checkPaymentTransaction": lambda _id, _type, attributes, relationships: + CheckPaymentTransactionDTO.from_json_api(_id, _type, attributes, relationships), + + "returnedCheckPaymentTransaction": lambda _id, _type, attributes, relationships: + ReturnedCheckPaymentTransactionDTO.from_json_api(_id, _type, attributes, relationships), + + #"paymentAdvanceTransaction": lambda _id, _type, attributes, relationships: + #PaymentAdvanceTransactionTransactionDTO.from_json_api(_id, _type, attributes, relationships), "repaidPaymentAdvanceTransaction": lambda _id, _type, attributes, relationships: RepaidPaymentAdvanceTransactionDTO.from_json_api(_id, _type, attributes, relationships), @@ -131,6 +143,63 @@ "achReceivedPayment": lambda _id, _type, attributes, relationships: AchReceivedPaymentDTO.from_json_api(_id, _type, attributes, relationships), + "checkPayment": lambda _id, _type, attributes, relationships: + CheckPaymentDTO.from_json_api(_id, _type, attributes, relationships), + + "pushToCardPayment": lambda _id, _type, attributes, relationships: + PushToCardPaymentDTO.from_json_api(_id, _type, attributes, relationships), + + "checkPayment.created": lambda _id, _type, attributes, relationships: + CheckPaymentCreatedEvent.from_json_api(_id, _type, attributes, relationships), + + "checkPayment.markedForReturn": lambda _id, _type, attributes, relationships: + CheckPaymentMarkedForReturnEvent.from_json_api(_id, _type, attributes, relationships), + + "checkPayment.processed": lambda _id, _type, attributes, relationships: + CheckPaymentProcessedEvent.from_json_api(_id, _type, attributes, relationships), + + "checkPayment.returned": lambda _id, _type, attributes, relationships: + CheckPaymentReturnedEvent.from_json_api(_id, _type, attributes, relationships), + + "checkPayment.pending": lambda _id, _type, attributes, relationships: + CheckPaymentPendingEvent.from_json_api(_id, _type, attributes, relationships), + + "checkPayment.rejected": lambda _id, _type, attributes, relationships: + CheckPaymentRejectedEvent.from_json_api(_id, _type, attributes, relationships), + + "checkPayment.inProduction": lambda _id, _type, attributes, relationships: + CheckPaymentInProductionEvent.from_json_api(_id, _type, attributes, relationships), + + "checkPayment.inDelivery": lambda _id, _type, attributes, relationships: + CheckPaymentInDeliveryEvent.from_json_api(_id, _type, attributes, relationships), + + "checkPayment.delivered": lambda _id, _type, attributes, relationships: + CheckPaymentDeliveredEvent.from_json_api(_id, _type, attributes, relationships), + + "checkPayment.returnToSender": lambda _id, _type, attributes, relationships: + CheckPaymentReturnToSenderEvent.from_json_api(_id, _type, attributes, relationships), + + "checkPayment.canceled": lambda _id, _type, attributes, relationships: + CheckPaymentCanceledEvent.from_json_api(_id, _type, attributes, relationships), + + "checkPayment.deliveryStatusChanged": lambda _id, _type, attributes, relationships: + CheckPaymentDeliveryStatusChangedEvent.from_json_api(_id, _type, attributes, relationships), + + "checkPayment.additionalVerificationRequired": lambda _id, _type, attributes, relationships: + CheckPaymentAdditionalVerificationRequiredEvent.from_json_api(_id, _type, attributes, relationships), + + "checkPayment.additionalVerificationApproved": lambda _id, _type, attributes, relationships: + CheckPaymentAdditionalVerificationApprovedEvent.from_json_api(_id, _type, attributes, relationships), + + "stopPayment.created": lambda _id, _type, attributes, relationships: + StopPaymentCreatedEvent.from_json_api(_id, _type, attributes, relationships), + + "stopPayment.paymentStopped": lambda _id, _type, attributes, relationships: + StopPaymentPaymentStoppedEvent.from_json_api(_id, _type, attributes, relationships), + + "stopPayment.disabled": lambda _id, _type, attributes, relationships: + StopPaymentDisabledEvent.from_json_api(_id, _type, attributes, relationships), + "accountStatementDTO": lambda _id, _type, attributes, relationships: StatementDTO.from_json_api(_id, _type, attributes, relationships), @@ -176,6 +245,12 @@ "authorization.created": lambda _id, _type, attributes, relationships: AuthorizationCreatedEvent.from_json_api(_id, _type, attributes, relationships), + "authorization.canceled": lambda _id, _type, attributes, relationships: + AuthorizationCanceledEvent.from_json_api(_id, _type, attributes, relationships), + + "authorization.declined": lambda _id, _type, attributes, relationships: + AuthorizationDeclinedEvent.from_json_api(_id, _type, attributes, relationships), + "authorizationRequest.declined": lambda _id, _type, attributes, relationships: AuthorizationRequestDeclinedEvent.from_json_api(_id, _type, attributes, relationships), @@ -185,9 +260,6 @@ "authorizationRequest.approved": lambda _id, _type, attributes, relationships: AuthorizationRequestApprovedEvent.from_json_api(_id, _type, attributes, relationships), - "document.approved": lambda _id, _type, attributes, relationships: - DocumentApprovedEvent.from_json_api(_id, _type, attributes, relationships), - "document.rejected": lambda _id, _type, attributes, relationships: DocumentRejectedEvent.from_json_api(_id, _type, attributes, relationships), @@ -197,21 +269,48 @@ "checkDeposit.created": lambda _id, _type, attributes, relationships: CheckDepositCreatedEvent.from_json_api(_id, _type, attributes, relationships), + "checkDeposit.pendingReview": lambda _id, _type, attributes, relationships: + CheckDepositPendingReviewEvent.from_json_api(_id, _type, attributes, relationships), + + "checkDeposit.pending": lambda _id, _type, attributes, relationships: + CheckDepositPendingEvent.from_json_api(_id, _type, attributes, relationships), + "checkDeposit.clearing": lambda _id, _type, attributes, relationships: CheckDepositClearingEvent.from_json_api(_id, _type, attributes, relationships), "checkDeposit.sent": lambda _id, _type, attributes, relationships: CheckDepositSentEvent.from_json_api(_id, _type, attributes, relationships), + "checkDeposit.rejected": lambda _id, _type, attributes, relationships: + CheckDepositRejectedEvent.from_json_api(_id, _type, attributes, relationships), + + "checkDeposit.returned": lambda _id, _type, attributes, relationships: + CheckDepositReturnedEvent.from_json_api(_id, _type, attributes, relationships), + + "checkPayment.additionalVerificationRequired": lambda _id, _type, attributes, relationships: + CheckPaymentAdditionalVerificationRequiredEvent.from_json_api(_id, _type, attributes, relationships), + "payment.clearing": lambda _id, _type, attributes, relationships: PaymentClearingEvent.from_json_api(_id, _type, attributes, relationships), + "payment.created": lambda _id, _type, attributes, relationships: + PaymentCreatedEvent.from_json_api(_id, _type, attributes, relationships), + "payment.sent": lambda _id, _type, attributes, relationships: PaymentSentEvent.from_json_api(_id, _type, attributes, relationships), "payment.returned": lambda _id, _type, attributes, relationships: PaymentReturnedEvent.from_json_api(_id, _type, attributes, relationships), + "payment.canceled": lambda _id, _type, attributes, relationships: + PaymentCanceledEvent.from_json_api(_id, _type, attributes, relationships), + + "payment.Canceled": lambda _id, _type, attributes, relationships: + PaymentCanceledUpperCasedEvent.from_json_api(_id, _type, attributes, relationships), + + "payment.rejected": lambda _id, _type, attributes, relationships: + PaymentRejectedEvent.from_json_api(_id, _type, attributes, relationships), + "statements.created": lambda _id, _type, attributes, relationships: StatementsCreatedEvent.from_json_api(_id, _type, attributes, relationships), @@ -221,6 +320,9 @@ "customer.created": lambda _id, _type, attributes, relationships: CustomerCreatedEvent.from_json_api(_id, _type, attributes, relationships), + "customer.updated": lambda _id, _type, attributes, relationships: + CustomerUpdatedEvent.from_json_api(_id, _type, attributes, relationships), + "account.reopened": lambda _id, _type, attributes, relationships: AccountReopenedEvent.from_json_api(_id, _type, attributes, relationships), @@ -248,12 +350,42 @@ "accountEndOfDay": lambda _id, _type, attributes, relationships: AccountEndOfDayDTO.from_json_api(_id, _type, attributes, relationships), + "accruedInterestTotal": lambda _id, _type, attributes, relationships: + AccruedInterestTotalDTO.from_json_api(_id, _type, attributes, relationships), + "counterpartyBalance": lambda _id, _type, attributes, relationships: CounterpartyBalanceDTO.from_json_api(_id, _type, attributes, relationships), "pinStatus": lambda _id, _type, attributes, relationships: PinStatusDTO.from_json_api(attributes), - } + + "beneficialOwner": lambda _id, _type, attributes, relationships: + BenificialOwnerDTO.from_json_api(attributes), + + "reward": lambda _id, _type, attributes, relationships: + RewardDTO.from_json_api(_id, attributes, relationships), + + "batchRelease": lambda _id, _type, attributes, relationships: + BatchReleaseDTO.from_json_api(_id, _type, attributes, relationships), + + "dispute.created": lambda _id, _type, attributes, relationships: + DisputeCreatedEvent.from_json_api(_id, _type, attributes, relationships), + + "dispute.statusChanged": lambda _id, _type, attributes, relationships: + DisputeStatusChangedEvent.from_json_api(_id, _type, attributes, relationships), + + "accountLowBalanceClosureTransaction": lambda _id, _type, attributes, relationships: + AccountLowBalanceClosureTransactionDTO.from_json_api(_id, _type, attributes, relationships), + + "negativeBalanceCoverageTransaction": lambda _id, _type, attributes, relationships: + NegativeBalanceCoverageTransactionDTO.from_json_api(_id, _type, attributes, relationships), + + "receivedPayment.created": lambda _id, _type, attributes, relationships: + ReceivedPaymentCreatedEvent.from_json_api(_id, _type, attributes, relationships), + + "writeOffTransaction": lambda _id, _type, attributes, relationships: + WriteOffTransactionDTO.from_json_api(_id, _type, attributes, relationships), +} def split_json_api_single_response(payload: Dict): @@ -314,6 +446,13 @@ def decode(payload): class UnitEncoder(json.JSONEncoder): def default(self, obj): + if isinstance(obj, CardLevelLimits): + return { + "dailyWithdrawal": obj.daily_withdrawal, + "dailyPurchase": obj.daily_purchase, + "monthlyWithdrawal": obj.monthly_withdrawal, + "monthlyPurchase": obj.monthly_purchase + } if isinstance(obj, FullName): return {"first": obj.first, "last": obj.last} if isinstance(obj, Phone): @@ -332,6 +471,11 @@ def default(self, obj): return addr if isinstance(obj, BusinessContact): return {"fullName": obj.full_name, "email": obj.email, "phone": obj.phone} + if isinstance(obj, AuthorizedUser): + authorized_user = {"fullName": obj.full_name, "email": obj.email, "phone": obj.phone} + if obj.jwt_subject is not None: + authorized_user["jwtSubject"] = obj.jwt_subject + return authorized_user if isinstance(obj, Officer): officer = {"fullName": obj.full_name, "dateOfBirth": date_utils.to_date_str(obj.date_of_birth), "address": obj.address, "phone": obj.phone, "email": obj.email} @@ -345,6 +489,14 @@ def default(self, obj): officer["passport"] = obj.passport if obj.nationality is not None: officer["nationality"] = obj.nationality + if obj.id_theft_score is not None: + officer["idTheftScore"] = obj.id_theft_score + if obj.occupation is not None: + officer["occupation"] = obj.occupation + if obj.annual_income is not None: + officer["annualIncome"] = obj.annual_income + if obj.source_of_income is not None: + officer["sourceOfIncome"] = obj.source_of_income return officer if isinstance(obj, BeneficialOwner): beneficial_owner = {"fullName": obj.full_name, "dateOfBirth": date_utils.to_date_str(obj.date_of_birth), @@ -359,6 +511,12 @@ def default(self, obj): beneficial_owner["nationality"] = obj.nationality if obj.percentage is not None: beneficial_owner["percentage"] = obj.percentage + if obj.occupation is not None: + beneficial_owner["occupation"] = obj.occupation + if obj.annual_income is not None: + beneficial_owner["annualIncome"] = obj.annual_income + if obj.source_of_income is not None: + beneficial_owner["sourceOfIncome"] = obj.source_of_income return beneficial_owner if isinstance(obj, RelationshipArray): return {"data": list(map(lambda r: r.to_dict(), obj.relationships))} @@ -369,4 +527,9 @@ def default(self, obj): "accountType": obj.account_type, "name": obj.name} if isinstance(obj, Coordinates): return {"longitude": obj.longitude, "latitude": obj.latitude} + if isinstance(obj, CheckPaymentCounterparty): + return { + "name": obj.name, + "address": obj.address + } return json.JSONEncoder.default(self, obj) diff --git a/unit/models/counterparty.py b/unit/models/counterparty.py index c91ecb8d..987c4ba7 100644 --- a/unit/models/counterparty.py +++ b/unit/models/counterparty.py @@ -23,7 +23,7 @@ def from_json_api(_id, _type, attributes, relationships): attributes["accountType"], attributes["type"], attributes["permissions"], relationships) -class CreateCounterpartyRequest(object): +class CreateCounterpartyRequest(UnitRequest): def __init__(self, name: str, routing_number: str, account_number: str, account_type: str, type: str, relationships: [Dict[str, Relationship]], tags: Optional[object] = None, idempotency_key: Optional[str] = None): @@ -156,11 +156,14 @@ def from_json_api(_id, _type, attributes, relationships): class ListCounterpartyParams(UnitParams): def __init__(self, offset: int = 0, limit: int = 100, customer_id: Optional[str] = None, - tags: Optional[object] = None): + tags: Optional[object] = None, account_number: Optional[str] = None, + routing_number: Optional[str] = None): self.offset = offset self.limit = limit self.customer_id = customer_id self.tags = tags + self.account_number = account_number + self.routing_number = routing_number def to_dict(self) -> Dict: parameters = {"page[limit]": self.limit, "page[offset]": self.offset} @@ -168,5 +171,9 @@ def to_dict(self) -> Dict: parameters["filter[customerId]"] = self.customer_id if self.tags: parameters["filter[tags]"] = self.tags + if self.account_number: + parameters["filter[accountNumber]"] = self.account_number + if self.routing_number: + parameters["filter[routingNumber]"] = self.routing_number return parameters diff --git a/unit/models/customer.py b/unit/models/customer.py index 4d9e7e34..d1ceab7b 100644 --- a/unit/models/customer.py +++ b/unit/models/customer.py @@ -1,16 +1,23 @@ from unit.utils import date_utils from unit.models import * +ArchiveReason = Literal["Inactive", "FraudACHActivity", "FraudCardActivity", "FraudCheckActivity", + "FraudApplicationHistory", "FraudAccountActivity", "FraudClientIdentified"] + +CustomerStatus = Literal["Active", "Archived"] class IndividualCustomerDTO(object): def __init__(self, id: str, created_at: datetime, full_name: FullName, date_of_birth: date, address: Address, phone: Phone, email: str, ssn: Optional[str], passport: Optional[str], nationality: Optional[str], - tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + authorized_users: [AuthorizedUser], tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]], status: CustomerStatus, + archive_reason: Optional[ArchiveReason]): self.id = id self.type = 'individualCustomer' self.attributes = {"createdAt": created_at, "fullName": full_name, "dateOfBirth": date_of_birth, "address": address, "phone": phone, "email": email, "ssn": ssn, "passport": passport, - "nationality": nationality, "tags": tags} + "nationality": nationality, "authorizedUsers": authorized_users, "tags": tags, + "status": status, "archiveReason": archive_reason} self.relationships = relationships @staticmethod @@ -20,7 +27,8 @@ def from_json_api(_id, _type, attributes, relationships): FullName.from_json_api(attributes["fullName"]), date_utils.to_date(attributes["dateOfBirth"]), Address.from_json_api(attributes["address"]), Phone.from_json_api(attributes["phone"]), attributes["email"], attributes.get("ssn"), attributes.get("passport"), attributes.get("nationality"), - attributes.get("tags"), relationships + AuthorizedUser.from_json_api(attributes["authorizedUsers"]), attributes.get("tags"), relationships, + attributes.get("status"), attributes.get("archiveReason") ) @@ -28,12 +36,14 @@ class BusinessCustomerDTO(object): def __init__(self, id: str, created_at: datetime, name: str, address: Address, phone: Phone, state_of_incorporation: str, ein: str, entity_type: EntityType, contact: BusinessContact, authorized_users: [AuthorizedUser], dba: Optional[str], tags: Optional[Dict[str, str]], - relationships: Optional[Dict[str, Relationship]]): + relationships: Optional[Dict[str, Relationship]], status: CustomerStatus, + archive_reason: Optional[ArchiveReason]): self.id = id self.type = 'businessCustomer' self.attributes = {"createdAt": created_at, "name": name, "address": address, "phone": phone, "stateOfIncorporation": state_of_incorporation, "ein": ein, "entityType": entity_type, - "contact": contact, "authorizedUsers": authorized_users, "dba": dba, "tags": tags} + "contact": contact, "authorizedUsers": authorized_users, "dba": dba, "tags": tags, + "status": status, "archiveReason": archive_reason} self.relationships = relationships @staticmethod @@ -43,20 +53,23 @@ def from_json_api(_id, _type, attributes, relationships): Address.from_json_api(attributes["address"]), Phone.from_json_api(attributes["phone"]), attributes["stateOfIncorporation"], attributes["ein"], attributes["entityType"], BusinessContact.from_json_api(attributes["contact"]), - [AuthorizedUser.from_json_api(user) for user in attributes["authorizedUsers"]], - attributes.get("dba"), attributes.get("tags"), relationships) + AuthorizedUser.from_json_api(attributes["authorizedUsers"]), + attributes.get("dba"), attributes.get("tags"), relationships, attributes.get("status"), + attributes.get("archiveReason")) CustomerDTO = Union[IndividualCustomerDTO, BusinessCustomerDTO] class PatchIndividualCustomerRequest(UnitRequest): def __init__(self, customer_id: str, address: Optional[Address] = None, phone: Optional[Phone] = None, - email: Optional[str] = None, dba: Optional[str] = None, tags: Optional[Dict[str, str]] = None): + email: Optional[str] = None, dba: Optional[str] = None, + authorized_users: Optional[List[AuthorizedUser]] = None, tags: Optional[Dict[str, str]] = None): self.customer_id = customer_id self.address = address self.phone = phone self.email = email self.dba = dba + self.authorized_users = authorized_users self.tags = tags def to_json_api(self) -> Dict: @@ -79,6 +92,9 @@ def to_json_api(self) -> Dict: if self.dba: payload["data"]["attributes"]["dba"] = self.dba + if self.authorized_users: + payload["data"]["attributes"]["authorizedUsers"] = self.authorized_users + if self.tags: payload["data"]["attributes"]["tags"] = self.tags @@ -150,3 +166,24 @@ def to_dict(self) -> Dict: parameters["sort"] = self.sort return parameters + +class ArchiveCustomerRequest(UnitRequest): + def __init__(self, customer_id: str, reason: Optional[ArchiveReason] = None): + self.customer_id = customer_id + self.reason = reason + + def to_json_api(self) -> Dict: + payload = { + "data": { + "type": "archiveCustomer", + "attributes": {} + } + } + + if self.reason: + payload["data"]["attributes"]["reason"] = self.reason + + return payload + + def __repr__(self): + return json.dumps(self.to_json_api()) diff --git a/unit/models/customerToken.py b/unit/models/customerToken.py index 856ab64a..b8a5b153 100644 --- a/unit/models/customerToken.py +++ b/unit/models/customerToken.py @@ -21,12 +21,13 @@ def from_json_api(_id, _type, attributes, relationships): class CreateCustomerToken(UnitRequest): def __init__(self, customer_id: str, scope: str, verification_token: Optional[str] = None, - verification_code: Optional[str] = None, expires_in: Optional[int] = None): + verification_code: Optional[str] = None, expires_in: Optional[int] = None, jwt_token: Optional[str] = None): self.customer_id = customer_id self.scope = scope self.verification_token = verification_token self.verification_code = verification_code self.expires_in = expires_in + self.jwt_token = jwt_token def to_json_api(self) -> Dict: payload = { @@ -47,6 +48,9 @@ def to_json_api(self) -> Dict: if self.verification_code: payload["data"]["attributes"]["verificationCode"] = self.verification_code + if self.jwt_token: + payload["data"]["attributes"]["jwtToken"] = self.jwt_token + return payload def __repr__(self): diff --git a/unit/models/dispute.py b/unit/models/dispute.py new file mode 100644 index 00000000..ca3f6fb8 --- /dev/null +++ b/unit/models/dispute.py @@ -0,0 +1,54 @@ +from unit.utils import date_utils +from unit.models import * + +DisputeStatus = Literal[ + "InvestigationStarted", + "ProvisionallyCredited", + "Denied", + "ResolvedLost", + "ResolvedWon", +] + + +class DisputeDTO(object): + def __init__( + self, + id: str, + source: str, + status_history: List[Dict[str, str]], + status: DisputeStatus, + description: str, + dispute_type: str, + created_at: datetime, + amount: int, + decision_reason: Optional[str], + relationships: Optional[Dict[str, Relationship]], + ): + self.id = id + self.type = "dispute" + self.attributes = { + "createdAt": created_at, + "source": source, + "statusHistory": status_history, + "status": status, + "description": description, + "disputeType": dispute_type, + "amount": amount, + "decisionReason": decision_reason, + } + self.relationships = relationships + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return DisputeDTO( + _id, + date_utils.to_datetime(attributes["createdAt"]), + attributes["source"], + attributes["statusHistory"], + attributes["status"], + attributes["description"], + attributes["disputeType"], + attributes["amount"], + attributes.get("decisionReason"), + relationships, + ) diff --git a/unit/models/event.py b/unit/models/event.py index cf922d1d..f8d08998 100644 --- a/unit/models/event.py +++ b/unit/models/event.py @@ -1,6 +1,8 @@ import json from datetime import datetime, date from typing import Literal, Optional + +from unit.models.check_stop_payment import StopPaymentStatus from unit.utils import date_utils from unit.models import * @@ -73,20 +75,57 @@ def from_json_api(_id, _type, attributes, relationships): return ApplicationAwaitingDocumentsEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes.get("tags"), relationships) +class AuthorizationCanceledEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, amount: int, card_last_4_digits: str, + recurring: str, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'authorization.canceled' + self.attributes["cardLast4Digits"] = card_last_4_digits + self.attributes["amount"] = amount + self.attributes["recurring"] = recurring + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return AuthorizationCanceledEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["amount"], attributes["cardLast4Digits"], + attributes["recurring"], attributes.get("tags"), + relationships) + +class AuthorizationDeclinedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, amount: int, card_last_4_digits: str, merchant: Merchant, + reason: str, recurring: str, tags: Optional[Dict[str, str]] = None, + relationships: Optional[Dict[str, Relationship]] = None): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'authorization.declined' + self.attributes["cardLast4Digits"] = card_last_4_digits + self.attributes["amount"] = amount + self.attributes["reason"] = reason + self.attributes["merchant"] = merchant + self.attributes["recurring"] = recurring + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return AuthorizationDeclinedEvent( + id=_id, created_at=date_utils.to_datetime(attributes["createdAt"]), + amount=attributes["amount"], card_last_4_digits=attributes["cardLast4Digits"], + merchant=Merchant.from_json_api(attributes["merchant"]), reason=attributes.get("reason"), recurring=attributes["recurring"], + tags=attributes.get("tags"), relationships=relationships) class AuthorizationCreatedEvent(BaseEvent): - def __init__(self, id: str, created_at: datetime, card_last_4_digits: str, recurring: str, - tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + def __init__(self, id: str, created_at: datetime, amount: int, card_last_4_digits: str, merchant: Merchant, + recurring: str, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): BaseEvent.__init__(self, id, created_at, tags, relationships) self.type = 'authorization.created' self.attributes["cardLast4Digits"] = card_last_4_digits + self.attributes["amount"] = amount + self.attributes["merchant"] = merchant self.attributes["recurring"] = recurring @staticmethod def from_json_api(_id, _type, attributes, relationships): return AuthorizationCreatedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), - attributes["cardLast4Digits"], attributes["recurring"], - attributes.get("tags"), relationships) + attributes["amount"], attributes["cardLast4Digits"], Merchant.from_json_api(attributes["merchant"]), + attributes["recurring"], attributes.get("tags"), relationships) class AuthorizationRequestApprovedEvent(BaseEvent): def __init__(self, id: str, created_at: datetime, amount: str, status: str, approved_amount: str, @@ -112,8 +151,8 @@ def from_json_api(_id, _type, attributes, relationships): class AuthorizationRequestDeclinedEvent(BaseEvent): - def __init__(self, id: str, created_at: datetime, amount: str, status: str, decline_reason: str, - partial_approval_allowed: str, merchant: Dict[str, str], recurring: str, + def __init__(self, id: str, created_at: datetime, amount: str, status: Optional[str], decline_reason: str, + partial_approval_allowed: str, merchant: Optional[Dict[str, str]], recurring: Optional[str], tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): BaseEvent.__init__(self, id, created_at, tags, relationships) self.type = 'authorizationRequest.declined' @@ -128,20 +167,23 @@ def __init__(self, id: str, created_at: datetime, amount: str, status: str, decl @staticmethod def from_json_api(_id, _type, attributes, relationships): return AuthorizationRequestDeclinedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), - attributes["amount"], attributes["status"], + attributes["amount"], attributes.get("status"), attributes["declineReason"], attributes["partialApprovalAllowed"], - attributes["merchant"], attributes["recurring"], + attributes.get("merchant"), attributes.get("recurring"), attributes.get("tags"), relationships) class AuthorizationRequestPendingEvent(BaseEvent): - def __init__(self, id: str, created_at: datetime, amount: str, status: str, partial_approval_allowed: str, - merchant: Dict[str, str], recurring: str, tags: Optional[Dict[str, str]], - relationships: Optional[Dict[str, Relationship]]): + def __init__(self, id: str, created_at: datetime, amount: int, status: str, partial_approval_allowed: str, + card_present: bool, digital_wallet: str, ecommerce: bool, merchant: Merchant, recurring: str, + tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): BaseEvent.__init__(self, id, created_at, tags, relationships) self.type = 'authorizationRequest.pending' self.attributes["amount"] = amount self.attributes["status"] = status + self.attributes["cardPresent"] = card_present + self.attributes["digitalWallet"] = digital_wallet + self.attributes["ecommerce"] = ecommerce self.attributes["partialApprovalAllowed"] = partial_approval_allowed self.attributes["merchant"] = merchant self.attributes["recurring"] = recurring @@ -149,10 +191,20 @@ def __init__(self, id: str, created_at: datetime, amount: str, status: str, part @staticmethod def from_json_api(_id, _type, attributes, relationships): - return AuthorizationRequestPendingEvent(_id, date_utils.to_datetime(attributes["createdAt"]), - attributes["amount"], attributes["status"], - attributes["partialApprovalAllowed"], attributes["merchant"], - attributes["recurring"], attributes.get("tags"), relationships) + return AuthorizationRequestPendingEvent( + id=_id, + created_at=date_utils.to_datetime(attributes["createdAt"]), + amount=attributes["amount"], + status=attributes["status"], + ecommerce=attributes.get("ecommerce"), + card_present=attributes.get("cardPresent"), + digital_wallet=attributes.get("digitalWallet"), + partial_approval_allowed=attributes["partialApprovalAllowed"], + merchant=Merchant.from_json_api(attributes["merchant"]), + recurring=attributes["recurring"], + tags=attributes.get("tags"), + relationships=relationships + ) class CardActivatedEvent(BaseEvent): def __init__(self, id: str, created_at: datetime, tags: Optional[Dict[str, str]], @@ -180,6 +232,96 @@ def from_json_api(_id, _type, attributes, relationships): attributes["previousStatus"], attributes.get("tags"), relationships) +class CardFraudCaseCreatedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, decision: str, activity_type: str, + expires_at: datetime, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'cardFraudCase.created' + self.attributes["status"] = status + self.attributes["decision"] = decision + self.attributes["activityType"] = activity_type + self.attributes["expiresAt"] = expires_at + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CardFraudCaseCreatedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["status"], attributes["decision"], attributes["activityType"], + date_utils.to_datetime(attributes["expiresAt"]), attributes.get("tags"), + relationships) + + +class CardFraudCaseActivatedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, decision: str, activity_type: str, + expires_at: datetime, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'cardFraudCase.activated' + self.attributes["status"] = status + self.attributes["decision"] = decision + self.attributes["activityType"] = activity_type + self.attributes["expiresAt"] = expires_at + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CardFraudCaseActivatedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["status"], attributes["decision"], attributes["activityType"], + date_utils.to_datetime(attributes["expiresAt"]), attributes.get("tags"), + relationships) + + +class CardFraudCaseExpiredEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, decision: str, activity_type: str, + expires_at: datetime, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'cardFraudCase.expired' + self.attributes["status"] = status + self.attributes["decision"] = decision + self.attributes["activityType"] = activity_type + self.attributes["expiresAt"] = expires_at + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CardFraudCaseExpiredEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["status"], attributes["decision"], attributes["activityType"], + date_utils.to_datetime(attributes["expiresAt"]), attributes.get("tags"), + relationships) + + +class CardFraudCaseFraudEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, decision: str, activity_type: str, + expires_at: datetime, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'cardFraudCase.fraud' + self.attributes["status"] = status + self.attributes["decision"] = decision + self.attributes["activityType"] = activity_type + self.attributes["expiresAt"] = expires_at + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CardFraudCaseFraudEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["status"], attributes["decision"], attributes["activityType"], + date_utils.to_datetime(attributes["expiresAt"]), attributes.get("tags"), + relationships) + + +class CardFraudCaseNoFraudEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, decision: str, activity_type: str, + expires_at: datetime, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'cardFraudCase.noFraud' + self.attributes["status"] = status + self.attributes["decision"] = decision + self.attributes["activityType"] = activity_type + self.attributes["expiresAt"] = expires_at + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CardFraudCaseNoFraudEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["status"], attributes["decision"], attributes["activityType"], + date_utils.to_datetime(attributes["expiresAt"]), attributes.get("tags"), + relationships) + + class CheckDepositCreatedEvent(BaseEvent): def __init__(self, id: str, created_at: datetime, status: str, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): @@ -192,6 +334,33 @@ def from_json_api(_id, _type, attributes, relationships): return CheckDepositCreatedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["status"], attributes.get("tags"), relationships) + +class CheckDepositPendingReviewEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, previous_status: str, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkDeposit.pendingReview' + self.attributes["previousStatus"] = previous_status + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckDepositPendingReviewEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["previousStatus"], attributes.get("tags"), relationships) + + +class CheckDepositPendingEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, previous_status: str, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkDeposit.pending' + self.attributes["previousStatus"] = previous_status + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckDepositPendingEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["previousStatus"], attributes.get("tags"), relationships) + + class CheckDepositClearingEvent(BaseEvent): def __init__(self, id: str, created_at: datetime, previous_status: str, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): @@ -218,6 +387,21 @@ def from_json_api(_id, _type, attributes, relationships): attributes["previousStatus"], attributes.get("tags"), relationships) +class CheckDepositRejectedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, previous_status: str, reason: str, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkDeposit.rejected' + self.attributes["previousStatus"] = previous_status + self.attributes["reason"] = reason + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckDepositRejectedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["previousStatus"], attributes["reason"], attributes.get("tags"), relationships) + + + class CheckDepositReturnedEvent(BaseEvent): def __init__(self, id: str, created_at: datetime, previous_status: str, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): @@ -231,6 +415,216 @@ def from_json_api(_id, _type, attributes, relationships): attributes["previousStatus"], attributes.get("tags"), relationships) +class CheckPaymentCreatedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, additional_verification_required: bool, + tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkPayment.created' + self.attributes["status"] = status + self.attributes["additionalVerificationRequired"] = additional_verification_required + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentCreatedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["status"], attributes["additionalVerificationRequired"], + attributes.get("tags"), relationships) + + +class CheckPaymentMarkedForReturnEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, previous_status: str, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkPayment.markedForReturn' + self.attributes["previousStatus"] = previous_status + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentMarkedForReturnEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["previousStatus"], attributes.get("tags"), relationships) + + +class CheckPaymentProcessedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, previous_status: str, additional_verification_required: bool, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkPayment.processed' + self.attributes["previousStatus"] = previous_status + self.attributes["additionalVerificationRequired"] = additional_verification_required + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentProcessedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["previousStatus"], attributes["additionalVerificationRequired"], + attributes.get("tags"), relationships) + + +class CheckPaymentReturnedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, previous_status: str, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkPayment.returned' + self.attributes["previousStatus"] = previous_status + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentReturnedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["previousStatus"], attributes.get("tags"), relationships) + + +class CheckPaymentPendingEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, previous_status: str, + counterparty_moved: Optional[bool], tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkPayment.pending' + self.attributes["status"] = status + self.attributes["previousStatus"] = previous_status + self.attributes["counterpartyMoved"] = counterparty_moved + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentPendingEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["status"], attributes["previousStatus"], + attributes.get("counterpartyMoved"), attributes.get("tags"), relationships) + + +class CheckPaymentRejectedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, previous_status: str, + reject_reason: str, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkPayment.rejected' + self.attributes["status"] = status + self.attributes["previousStatus"] = previous_status + self.attributes["rejectReason"] = reject_reason + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentRejectedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["status"], attributes["previousStatus"], + attributes["rejectReason"], attributes.get("tags"), relationships) + + +class CheckPaymentInProductionEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, previous_status: str, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkPayment.inProduction' + self.attributes["status"] = status + self.attributes["previousStatus"] = previous_status + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentInProductionEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["status"], + attributes["previousStatus"], attributes.get("tags"), relationships) + + +class CheckPaymentInDeliveryEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, previous_status: str, delivery_status: str, + tracked_at: datetime, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkPayment.inDelivery' + self.attributes["status"] = status + self.attributes["previousStatus"] = previous_status + self.attributes["deliveryStatus"] = delivery_status + self.attributes["trackedAt"] = tracked_at + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentInDeliveryEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["status"], + attributes["previousStatus"], attributes["deliveryStatus"], + attributes["trackedAt"], attributes.get("tags"), relationships) + + +class CheckPaymentDeliveredEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, previous_status: str, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkPayment.delivered' + self.attributes["status"] = status + self.attributes["previousStatus"] = previous_status + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentDeliveredEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["status"], + attributes["previousStatus"], attributes.get("tags"), relationships) + + +class CheckPaymentReturnToSenderEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, previous_status: str, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkPayment.returnToSender' + self.attributes["status"] = status + self.attributes["previousStatus"] = previous_status + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentReturnToSenderEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["status"], + attributes["previousStatus"], attributes.get("tags"), relationships) + + +class CheckPaymentCanceledEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, previous_status: str, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkPayment.canceled' + self.attributes["previousStatus"] = previous_status + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentCanceledEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["previousStatus"], attributes.get("tags"), relationships) + + +class CheckPaymentDeliveryStatusChangedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, previous_delivery_status: str, new_delivery_status: str, + tracked_at: datetime, postal_code: str, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkPayment.deliveryStatusChanged' + self.attributes["previousDeliveryStatus"] = previous_delivery_status + self.attributes["newDeliveryStatus"] = new_delivery_status + self.attributes["trackedAt"] = tracked_at + self.attributes["postalCode"] = postal_code + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentDeliveryStatusChangedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["previousDeliveryStatus"], + attributes["newDeliveryStatus"], attributes["trackedAt"], + attributes["postalCode"], attributes.get("tags"), relationships) + + +class CheckPaymentAdditionalVerificationRequiredEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, amount: int, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkPayment.additionalVerificationRequired' + self.attributes["status"] = status + self.attributes["amount"] = amount + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentAdditionalVerificationRequiredEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["status"], attributes["amount"], attributes.get("tags"), relationships) + + +class CheckPaymentAdditionalVerificationApprovedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, amount: int, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'checkPayment.additionalVerificationApproved' + self.attributes["status"] = status + self.attributes["amount"] = amount + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentAdditionalVerificationApprovedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["status"], attributes["amount"], attributes.get("tags"), relationships) + + class CustomerCreatedEvent(BaseEvent): def __init__(self, id: str, created_at: datetime, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): @@ -242,6 +636,19 @@ def from_json_api(_id, _type, attributes, relationships): return CustomerCreatedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes.get("tags"), relationships) + +class CustomerUpdatedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'customer.updated' + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CustomerUpdatedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes.get("tags"), relationships) + + class DocumentApprovedEvent(BaseEvent): def __init__(self, id: str, created_at: datetime, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): @@ -267,6 +674,29 @@ def from_json_api(_id, _type, attributes, relationships): attributes["reason"], attributes["reasonCode"], attributes.get("tags"), relationships) +class PaymentCreatedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'payment.created' + self.attributes["status"] = status + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return PaymentCreatedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["status"], + attributes.get("tags"), relationships) + +class PaymentRejectedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'payment.rejected' + self.attributes["status"] = status + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return PaymentClearingEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["status"], + attributes.get("tags"), relationships) class PaymentClearingEvent(BaseEvent): def __init__(self, id: str, created_at: datetime, previous_status: str, tags: Optional[Dict[str, str]], @@ -304,6 +734,38 @@ def from_json_api(_id, _type, attributes, relationships): return PaymentReturnedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["previousStatus"], attributes.get("tags"), relationships) +class PaymentCanceledEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'payment.canceled' + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return PaymentCanceledEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes.get("tags"), relationships) + +class PaymentCanceledUpperCasedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'payment.Canceled' + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return PaymentCanceledUpperCasedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes.get("tags"), relationships) + +class PaymentRejectedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, reason: str, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'payment.rejected' + self.attributes["reason"] = reason + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return PaymentRejectedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["reason"], + attributes.get("tags"), relationships) + class StatementsCreatedEvent(BaseEvent): def __init__(self, id: str, created_at: datetime, period: str, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): @@ -317,7 +779,7 @@ def from_json_api(_id, _type, attributes, relationships): attributes.get("tags"), relationships) class TransactionCreatedEvent(BaseEvent): - def __init__(self, id: str, created_at: datetime, summary: str, direction: str, amount: str, + def __init__(self, id: str, created_at: datetime, summary: str, direction: str, amount: int, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): BaseEvent.__init__(self, id, created_at, tags, relationships) self.type = 'transaction.created' @@ -342,23 +804,159 @@ def from_json_api(_id, _type, attributes, relationships): return AccountReopenedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes.get("tags"), relationships) -EventDTO = Union[AccountClosedEvent, AccountFrozenEvent, ApplicationDeniedEvent, ApplicationAwaitingDocumentsEvent, - ApplicationPendingReviewEvent, CardActivatedEvent, CardStatusChangedEvent, - AuthorizationCreatedEvent, AuthorizationRequestDeclinedEvent, AuthorizationRequestPendingEvent, - AuthorizationRequestApprovedEvent, DocumentApprovedEvent, DocumentRejectedEvent, - CheckDepositCreatedEvent, CheckDepositClearingEvent, CheckDepositSentEvent, - CheckDepositReturnedEvent, CustomerCreatedEvent, PaymentClearingEvent, PaymentSentEvent, - PaymentReturnedEvent, StatementsCreatedEvent, TransactionCreatedEvent, AccountReopenedEvent, RawUnitObject] +class StopPaymentCreatedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.type = 'stopPayment.created' + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return StopPaymentCreatedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes.get("tags"), + relationships) + + +class StopPaymentPaymentStoppedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, stopped_payment_type: str, + tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.attributes['stoppedPaymentType'] = stopped_payment_type + self.type = 'stopPayment.paymentStopped' + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return StopPaymentPaymentStoppedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["stoppedPaymentType"], attributes.get("tags"), + relationships) + + +class StopPaymentDisabledEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, status: str, previous_status: str, + tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.attributes['status'] = status + self.attributes['previousStatus'] = previous_status + self.type = 'stopPayment.disabled' + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return StopPaymentDisabledEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["status"], + attributes["previousStatus"], attributes.get("tags"), relationships) + +class DisputeCreatedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, amount: int, description: str, source: str, status: str, + tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.attributes['amount'] = amount + self.attributes['description'] = description + self.attributes['source'] = source + self.attributes['status'] = status + self.type = 'dispute.created' + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return DisputeCreatedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["amount"], + attributes["description"], attributes["source"], attributes["status"], + attributes.get("tags"), relationships) + +class DisputeStatusChangedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, previous_status: str, new_status: str, + tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.attributes['previousStatus'] = previous_status + self.attributes['newStatus'] = new_status + self.type = 'dispute.statusChanged' + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return DisputeStatusChangedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["previousStatus"], + attributes["newStatus"], attributes.get("tags"), relationships) + +class ReceivedPaymentCreatedEvent(BaseEvent): + def __init__(self, id: str, created_at: datetime, + status: str, + type: str, + amount: int, + completion_date: date, + company_name: str, + counterparty_routing_number: str, + description: str, + trace_number: Optional[str], + sec_code: Optional[str], + return_cutoff_time: Optional[datetime], + can_be_reprocessed: Optional[bool], + addenda: Optional[str], + tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BaseEvent.__init__(self, id, created_at, tags, relationships) + self.attributes["status"] = status + self.attributes["type"] = type + self.attributes["amount"] = amount + self.attributes["completionDate"] = completion_date + self.attributes["companyName"] = company_name + self.attributes["counterpartyRoutingNumber"] = counterparty_routing_number + self.attributes["description"] = description + self.attributes["traceNumber"] = trace_number + self.attributes["secCode"] = sec_code + self.attributes["returnCutoffTime"] = return_cutoff_time + self.attributes["canBeReprocessed"] = can_be_reprocessed + self.attributes["addenda"] = addenda + + self.type = 'receivedPayment.created' + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return ReceivedPaymentCreatedEvent(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["status"], attributes["type"], attributes["amount"], + attributes["completionDate"], attributes["companyName"], + attributes["counterpartyRoutingNumber"], attributes["description"], + attributes.get("traceNumber"), attributes.get("secCode"), + attributes.get("returnCutoffTime"), attributes.get("canBeReprocessed"), + attributes.get("addenda"), attributes.get("tags"), relationships) + + +EventDTO = Union[ + AccountClosedEvent, AccountFrozenEvent, ApplicationDeniedEvent, ApplicationAwaitingDocumentsEvent, + ApplicationPendingReviewEvent, CardActivatedEvent, CardStatusChangedEvent, + CardFraudCaseCreatedEvent, CardFraudCaseActivatedEvent, CardFraudCaseExpiredEvent, + CardFraudCaseFraudEvent, CardFraudCaseNoFraudEvent, + AuthorizationCreatedEvent, AuthorizationCanceledEvent, AuthorizationDeclinedEvent, + AuthorizationRequestDeclinedEvent, AuthorizationRequestPendingEvent, + AuthorizationRequestApprovedEvent, DocumentApprovedEvent, DocumentRejectedEvent, + CheckDepositCreatedEvent, CheckDepositPendingReviewEvent, CheckDepositPendingEvent, + CheckDepositClearingEvent, CheckDepositSentEvent, CheckDepositRejectedEvent, CheckDepositReturnedEvent, + CheckPaymentCreatedEvent, CheckPaymentMarkedForReturnEvent, CheckPaymentProcessedEvent, CheckPaymentReturnedEvent, + CheckPaymentPendingEvent, CheckPaymentRejectedEvent, CheckPaymentInProductionEvent, CheckPaymentInDeliveryEvent, + CheckPaymentDeliveredEvent, CheckPaymentReturnToSenderEvent, CheckPaymentCanceledEvent, + CheckPaymentDeliveryStatusChangedEvent, CheckPaymentAdditionalVerificationRequiredEvent, + CheckPaymentAdditionalVerificationApprovedEvent, + CustomerCreatedEvent, PaymentClearingEvent, PaymentSentEvent, PaymentReturnedEvent, PaymentCanceledEvent, PaymentCanceledUpperCasedEvent, + StatementsCreatedEvent, TransactionCreatedEvent, AccountReopenedEvent, RawUnitObject, + StopPaymentCreatedEvent, StopPaymentPaymentStoppedEvent, StopPaymentDisabledEvent, + DisputeCreatedEvent, DisputeStatusChangedEvent, ReceivedPaymentCreatedEvent +] class ListEventParams(UnitParams): - def __init__(self, limit: int = 100, offset: int = 0, type: Optional[List[str]] = None): + def __init__( + self, + limit: int = 100, + offset: int = 0, + type: Optional[str] = None, + since: Optional[str] = None, + until: Optional[str] = None, + ): self.limit = limit self.offset = offset self.type = type + self.since = since + self.until = until def to_dict(self) -> Dict: parameters = {"page[limit]": self.limit, "page[offset]": self.offset} if self.type: parameters["filter[type][]"] = self.type + if self.since: + parameters["filter[since]"] = self.since + if self.until: + parameters["filter[until]"] = self.until return parameters diff --git a/unit/models/fee.py b/unit/models/fee.py index 296ce187..efbf2e0d 100644 --- a/unit/models/fee.py +++ b/unit/models/fee.py @@ -38,7 +38,7 @@ def to_json_api(self) -> Dict: } if self.idempotency_key: - payload["data"]["attributes"]["idempotencyKey"] = self.tags + payload["data"]["attributes"]["idempotencyKey"] = self.idempotency_key if self.tags: payload["data"]["attributes"]["tags"] = self.tags diff --git a/unit/models/payment.py b/unit/models/payment.py index 2548d980..971b1731 100644 --- a/unit/models/payment.py +++ b/unit/models/payment.py @@ -5,15 +5,20 @@ PaymentDirections = Literal["Debit", "Credit"] PaymentStatus = Literal["Pending", "Rejected", "Clearing", "Sent", "Canceled", "Returned"] + class BasePayment(object): def __init__(self, id: str, created_at: datetime, status: PaymentStatus, direction: PaymentDirections, description: str, amount: int, reason: Optional[str], tags: Optional[Dict[str, str]], - relationships: Optional[Dict[str, Relationship]]): + relationships: Optional[Dict[str, Relationship]], astra_routine_id: Optional[str] = None): self.id = id self.attributes = {"createdAt": created_at, "status": status, "direction": direction, "description": description, "amount": amount, "reason": reason, "tags": tags} self.relationships = relationships + if astra_routine_id: + self.attributes["astraRoutineId"] = astra_routine_id + + class AchPaymentDTO(BasePayment): def __init__(self, id: str, created_at: datetime, status: PaymentStatus, counterparty: Counterparty, direction: str, description: str, amount: int, addenda: Optional[str], reason: Optional[str], @@ -33,6 +38,53 @@ def from_json_api(_id, _type, attributes, relationships): attributes["amount"], attributes.get("addenda"), attributes.get("reason"), settlement_date, attributes.get("tags"), relationships) +class SimulateIncomingAchPaymentDTO(BasePayment): + def __init__(self, id: str, created_at: datetime, status: PaymentStatus, direction: str, + description: str, amount: int, reason: Optional[str], + settlement_date: Optional[datetime], tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BasePayment.__init__(self, id, created_at, direction, description, amount, reason, tags, relationships) + self.type = 'achPayment' + self.attributes["status"] = status + self.settlement_date = settlement_date + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return BasePayment( + _id, + date_utils.to_datetime(attributes["createdAt"]), + attributes.get("direction"), + attributes["description"], + attributes["amount"], + attributes.get("reason"), + attributes.get("tags"), + relationships + ) + +class SimulateAchPaymentDTO(object): + def __init__(self, id: str, created_at: datetime, status: PaymentStatus, direction: str, + description: str, amount: int, reason: Optional[str], + settlement_date: Optional[datetime], tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BasePayment.__init__(self, id, created_at, direction, description, amount, reason, tags, relationships) + self.type = 'achPayment' + self.attributes["status"] = status + self.settlement_date = settlement_date + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return BasePayment( + _id, + date_utils.to_datetime(attributes["createdAt"]), + attributes.get("status"), + attributes.get("direction"), + attributes["description"], + attributes["amount"], + attributes.get("reason"), + attributes.get("tags"), + relationships + ) + class BookPaymentDTO(BasePayment): def __init__(self, id: str, created_at: datetime, status: PaymentStatus, direction: Optional[str], description: str, amount: int, reason: Optional[str], tags: Optional[Dict[str, str]], @@ -61,10 +113,26 @@ def from_json_api(_id, _type, attributes, relationships): attributes["description"], attributes["amount"], attributes.get("reason"), attributes.get("tags"), relationships) + +class PushToCardPaymentDTO(BasePayment): + def __init__(self, id: str, created_at: datetime, status: PaymentStatus, direction: Optional[str], description: str, + amount: int, astra_routine_id: str, reason: Optional[str], tags: Optional[Dict[str, str]], + relationships: Optional[Dict[str, Relationship]]): + BasePayment.__init__(self, id, created_at, status, direction, description, amount, reason, tags, relationships, astra_routine_id) + self.type = 'pushToCardPayment' + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return PushToCardPaymentDTO(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["status"], + attributes.get("direction"), attributes["description"], attributes["amount"], + attributes.get("astraRoutineId"), attributes.get("reason"), attributes.get("tags"), + relationships) + + class BillPaymentDTO(BasePayment): def __init__(self, id: str, created_at: datetime, status: PaymentStatus, direction: str, description: str, amount: int, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): - BasePayment.__init__(self, id, created_at, status, direction, description, amount, reason, tags, relationships) + BasePayment.__init__(self, id, created_at, status, direction, description, amount, tags, relationships) self.type = 'billPayment' @staticmethod @@ -81,13 +149,15 @@ class AchReceivedPaymentDTO(object): def __init__(self, id: str, created_at: datetime, status: AchReceivedPaymentStatus, was_advanced: bool, completion_date: datetime, return_reason: Optional[str], amount: int, description: str, addenda: Optional[str], company_name: str, counterparty_routing_number: str, trace_number: str, - sec_code: Optional[str], tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + sec_code: Optional[str], return_cutoff_time: Optional[datetime], can_be_reprocessed: Optional[bool], + tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): self.type = "achReceivedPayment" self.attributes = {"createdAt": created_at, "status": status, "wasAdvanced": was_advanced, "completionDate": completion_date, "returnReason": return_reason, "description": description, "amount": amount, "addenda": addenda, "companyName": company_name, "counterpartyRoutingNumber": counterparty_routing_number, "traceNumber": trace_number, - "secCode": sec_code, "tags": tags} + "secCode": sec_code, "returnCutoffTime": return_cutoff_time, "canBeReprocessed": can_be_reprocessed, + "tags": tags} self.relationships = relationships @staticmethod @@ -97,19 +167,22 @@ def from_json_api(_id, _type, attributes, relationships): attributes.get("returnReason"),attributes["amount"], attributes["description"], attributes.get("addenda"), attributes.get("companyName"), attributes.get("counterpartyRoutingNumber"), attributes.get("traceNumber"), - attributes.get("secCode"), attributes.get("tags"), relationships) + attributes.get("secCode"), attributes.get("returnCutoffTime"), attributes.get("canBeReprocessed"), + attributes.get("tags"), relationships) class CreatePaymentBaseRequest(UnitRequest): def __init__(self, amount: int, description: str, relationships: Dict[str, Relationship], - idempotency_key: Optional[str], tags: Optional[Dict[str, str]], direction: str = "Credit", - type: str = "achPayment"): + idempotency_key: Optional[str], tags: Optional[Dict[str, str]], direction: Optional[str], + type: str = "achPayment", same_day: Optional[bool] = False, configuration: dict = None): self.type = type self.amount = amount + self.same_day = same_day self.description = description self.direction = direction self.idempotency_key = idempotency_key self.tags = tags self.relationships = relationships + self.configuration = configuration def to_json_api(self) -> Dict: payload = { @@ -117,19 +190,27 @@ def to_json_api(self) -> Dict: "type": self.type, "attributes": { "amount": self.amount, - "direction": self.direction, "description": self.description }, "relationships": self.relationships } } + if self.direction: + payload["data"]["attributes"]["direction"] = self.direction + if self.idempotency_key: payload["data"]["attributes"]["idempotencyKey"] = self.idempotency_key + + if self.same_day: + payload["data"]["attributes"]["sameDay"] = self.same_day if self.tags: payload["data"]["attributes"]["tags"] = self.tags + if self.configuration: + payload["data"]["attributes"]["configuration"] = self.configuration + return payload def __repr__(self): @@ -138,8 +219,8 @@ def __repr__(self): class CreateInlinePaymentRequest(CreatePaymentBaseRequest): def __init__(self, amount: int, description: str, counterparty: Counterparty, relationships: Dict[str, Relationship], addenda: Optional[str], idempotency_key: Optional[str], tags: Optional[Dict[str, str]], - direction: str = "Credit"): - CreatePaymentBaseRequest.__init__(self, amount, description, relationships, idempotency_key, tags, direction) + direction: str = "Credit", same_day: Optional[bool] = False): + CreatePaymentBaseRequest.__init__(self, amount, description, relationships, idempotency_key, tags, direction, same_day=same_day) self.counterparty = counterparty self.addenda = addenda @@ -156,8 +237,8 @@ def to_json_api(self) -> Dict: class CreateLinkedPaymentRequest(CreatePaymentBaseRequest): def __init__(self, amount: int, description: str, relationships: Dict[str, Relationship], addenda: Optional[str], verify_counterparty_balance: Optional[bool], idempotency_key: Optional[str], - tags: Optional[Dict[str, str]], direction: str = "Credit"): - CreatePaymentBaseRequest.__init__(self, amount, description, relationships, idempotency_key, tags, direction) + tags: Optional[Dict[str, str]], direction: str = "Credit", same_day: Optional[bool] = False): + CreatePaymentBaseRequest.__init__(self, amount, description, relationships, idempotency_key, tags, direction, same_day=same_day) self.addenda = addenda self.verify_counterparty_balance = verify_counterparty_balance @@ -172,6 +253,68 @@ def to_json_api(self) -> Dict: return payload +class SimulateIncomingAchRequest(CreatePaymentBaseRequest): + def __init__( + self, amount: int, description: str, + relationships: Dict[str, Relationship], + direction: str = "Credit" + ): + CreatePaymentBaseRequest.__init__(self, amount, description, relationships, None, None, direction) + self.verify_counterparty_balance = False + + def to_json_api(self) -> Dict: + payload = CreatePaymentBaseRequest.to_json_api(self) + + return payload + +class SimulateTransmitAchRequest(UnitRequest): + def __init__( + self, payment_id: int + ): + self.type = "transmitAchPayment" + self.id = payment_id + self.relationships = { + "payment": { + "data": { + "type": "achPayment", + "id": self.id + } + } + } + + def to_json_api(self) -> Dict: + payload = { + "data": { + "type": self.type, + "relationships": self.relationships + } + } + return payload + +class SimulateClearAchRequest(UnitRequest): + def __init__( + self, payment_id: int + ): + self.type = "clearAchPayment" + self.id = payment_id + self.relationships = { + "payment": { + "data": { + "type": "achPayment", + "id": self.id + } + } + } + + def to_json_api(self) -> Dict: + payload = { + "data": { + "type": self.type, + "relationships": self.relationships + } + } + return payload + class CreateVerifiedPaymentRequest(CreatePaymentBaseRequest): def __init__(self, amount: int, description: str, plaid_processor_token: str, relationships: Dict[str, Relationship], counterparty_name: Optional[str], verify_counterparty_balance: Optional[bool], @@ -213,8 +356,60 @@ def to_json_api(self) -> Dict: payload["data"]["attributes"]["counterparty"] = self.counterparty return payload + +class CreatePushToCardPaymentRequest(CreatePaymentBaseRequest): + def __init__(self, amount: int, description: str, configuration: dict, + relationships: Dict[str, Relationship], + idempotency_key: Optional[str] = None, tags: Optional[Dict[str, str]] = None): + super().__init__(amount, description, relationships, idempotency_key, tags, None, "pushToCardPayment", False, configuration) + + +class CreateCheckPaymentRequest(UnitRequest): + def __init__( + self, + description: str, + amount: int, + counterparty: CheckPaymentCounterparty, + idempotency_key: str, + relationships: Dict[str, Relationship], + memo: Optional[str] = None, + send_date: Optional[str] = None, + tags: Optional[Dict[str, str]] = None, + ): + self.description = description + self.amount = amount + self.send_date = send_date + self.counterparty = counterparty + self.memo = memo + self.idempotency_key = idempotency_key + self.tags = tags + self.relationships = relationships + + def to_json_api(self) -> Dict: + return super().to_payload("checkPayment", self.relationships) + + +class CreateCheckStopPaymentRequest(UnitRequest): + def __init__( + self, + check_number: str, + relationships: Dict[str, Relationship], + amount: Optional[int] = None, + tags: Optional[Dict[str, str]] = None, + idempotency_key: Optional[str] = None + ): + self.amount = amount + self.check_number = check_number + self.tags = tags + self.relationships = relationships + + def to_json_api(self) -> Dict: + return super().to_payload("checkStopPayment", self.relationships) + + CreatePaymentRequest = Union[CreateInlinePaymentRequest, CreateLinkedPaymentRequest, CreateVerifiedPaymentRequest, - CreateBookPaymentRequest, CreateWirePaymentRequest] + CreateBookPaymentRequest, CreateWirePaymentRequest, CreatePushToCardPaymentRequest, + CreateCheckPaymentRequest, CreateCheckStopPaymentRequest] class PatchAchPaymentRequest(object): def __init__(self, payment_id: str, tags: Dict[str, str]): @@ -256,7 +451,27 @@ def to_json_api(self) -> Dict: def __repr__(self): json.dumps(self.to_json_api()) -PatchPaymentRequest = Union[PatchAchPaymentRequest, PatchBookPaymentRequest] +class PatchCheckPaymentRequest(object): + def __init__(self, payment_id: str, tags: Dict[str, str]): + self.payment_id = payment_id + self.tags = tags + + def to_json_api(self) -> Dict: + payload = { + "data": { + "type": "checkPayment", + "attributes": { + "tags": self.tags + } + } + } + + return payload + + def __repr__(self): + json.dumps(self.to_json_api()) + +PatchPaymentRequest = Union[PatchAchPaymentRequest, PatchBookPaymentRequest, PatchCheckPaymentRequest] class ListPaymentParams(UnitParams): def __init__(self, limit: int = 100, offset: int = 0, account_id: Optional[str] = None, diff --git a/unit/models/repayment.py b/unit/models/repayment.py new file mode 100644 index 00000000..3554b94b --- /dev/null +++ b/unit/models/repayment.py @@ -0,0 +1,114 @@ +try: + from typing import Optional, Dict, Union, List, Literal +except ImportError: + from typing import Optional, Dict, Union, List + from typing_extensions import Literal + +from unit.models import UnitDTO, extract_attributes, UnitRequest, Relationship, UnitParams +from unit.utils import date_utils + + +class BaseRepayment(UnitDTO): + def __init__(self, _id, _type, attributes, relationships): + self.id = _id + self.type = _type + self.attributes = extract_attributes(["amount", "status", "tags"], attributes) + attrs = {"createdAt": date_utils.to_datetime(attributes["createdAt"]), + "updatedAt": date_utils.to_datetime(attributes["updatedAt"])} + self.attributes.update(attrs) + self.relationships = relationships + + +class BookRepaymentDTO(BaseRepayment): + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return BookRepaymentDTO(_id, _type, attributes, relationships) + + +class AchRepaymentDTO(BaseRepayment): + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return AchRepaymentDTO(_id, _type, attributes, relationships) + + +RepaymentDTO = Union[BookRepaymentDTO, AchRepaymentDTO] + + +class CreateBookRepaymentRequest(UnitRequest): + def __init__(self, description: str, amount: int, relationships: Dict[str, Relationship], + transaction_summary_override: Optional[str] = None, tags: Optional[Dict[str, str]] = None, + idempotency_key: Optional[str] = None): + self.description = description + self.amount = amount + self.transaction_summary_override = transaction_summary_override + self.tags = tags + self.idempotency_key = idempotency_key + self.relationships = relationships + + def to_json_api(self) -> Dict: + return super().to_payload("bookRepayment", self.relationships) + + +class CreateAchRepaymentRequest(UnitRequest): + def __init__(self, description: str, amount: int, relationships: Dict[str, Relationship], + addenda: Optional[str] = None, tags: Optional[Dict[str, str]] = None, same_day: Optional[bool] = None, + idempotency_key: Optional[str] = None): + self.description = description + self.amount = amount + self.addenda = addenda + self.tags = tags + self.same_day = same_day + self.idempotency_key = idempotency_key + self.relationships = relationships + + def to_json_api(self) -> Dict: + return super().to_payload("achRepayment", self.relationships) + + +CreateRepaymentRequest = Union[CreateBookRepaymentRequest, CreateAchRepaymentRequest] + +RepaymentStatus = Literal["Pending", "PendingReview", "Returned", "Sent", "Rejected"] +RepaymentType = Literal["bookRepayment", "achRepayment"] + + +class ListRepaymentParams(UnitParams): + def __init__( + self, + limit: int = 100, + offset: int = 0, + account_id: Optional[str] = None, + credit_account_id: Optional[str] = None, + customer_id: Optional[str] = None, + status: Optional[List[RepaymentStatus]] = None, + _type: Optional[List[str]] = None, + ): + self.limit = limit + self.offset = offset + self.account_id = account_id + self.credit_account_id = credit_account_id + self.customer_id = customer_id + self.status = status + self.type = _type + + def to_dict(self) -> Dict: + parameters = {"page[limit]": self.limit, "page[offset]": self.offset} + + if self.account_id: + parameters["filter[accountId]"] = self.account_id + + if self.credit_account_id: + parameters["filter[creditAccountId]"] = self.credit_account_id + + if self.customer_id: + parameters["filter[customerId]"] = self.customer_id + + if self.status: + for idx, status_filter in enumerate(self.status): + parameters[f"filter[status][{idx}]"] = status_filter + + if self.type: + for idx, type_filter in enumerate(self.type): + parameters[f"filter[type][{idx}]"] = type_filter + + return parameters + diff --git a/unit/models/returnAch.py b/unit/models/returnAch.py index ecd9eb2c..6c972246 100644 --- a/unit/models/returnAch.py +++ b/unit/models/returnAch.py @@ -2,10 +2,10 @@ from typing import Literal from unit.models import * -AchReturnReason = Literal["Unauthorized"] +AchReturnReason = Literal["InsufficientFunds", "Unauthorized", "UncollectedFunds"] -class ReturnReceivedAchTransactionRequest(object): +class ReturnReceivedAchTransactionRequest(UnitRequest): def __init__(self, transaction_id: str, reason: AchReturnReason, relationships: [Dict[str, Relationship]]): self.transaction_id = transaction_id self.reason = reason diff --git a/unit/models/reward.py b/unit/models/reward.py new file mode 100644 index 00000000..07f1535b --- /dev/null +++ b/unit/models/reward.py @@ -0,0 +1,137 @@ +import json +from typing import Optional, Literal, Dict, List +from datetime import datetime + +from unit.models import Relationship, UnitRequest, UnitParams + + +SORT_ORDERS = Literal["created_at", "-created_at"] +RELATED_RESOURCES = Literal["customer", "account", "transaction"] + + +class RewardDTO(object): + def __init__(self, id: str, amount: int, description: str, status: str, tags: Optional[Dict[str, str]] = None, + relationships: Optional[Dict[str, Relationship]] = None): + self.id = id + self.type = "reward" + self.attributes = {"amount": amount, "description": description, "status": status, "tags": tags} + self.relationships = relationships + + @staticmethod + def from_json_api(_id, attributes, relationships): + return RewardDTO(_id, attributes["amount"], attributes["description"], attributes["status"], attributes.get("tags"), relationships) + + +class CreateRewardRequest(UnitRequest): + def __init__( + self, + amount: int, + description: str, + receiving_account_id: str, + rewarded_transaction_id: Optional[str] = None, + funding_account_id: Optional[str] = None, + idempotency_key: Optional[str] = None, + tags: Optional[Dict[str, str]] = None + ): + self.type = "reward" + self.amount = amount + self.description = description + self.rewarded_transaction_id = rewarded_transaction_id + self.receiving_account_id = receiving_account_id + self.funding_account_id = funding_account_id + self.idempotency_key = idempotency_key + self.tags = tags + + self.relationships = { + "receivingAccount": Relationship(_type="depositAccount", _id=self.receiving_account_id) + } + if self.rewarded_transaction_id: + self.relationships["rewardedTransaction"] = Relationship(_type="transaction", _id=self.rewarded_transaction_id) + + if self.funding_account_id: + self.relationships["fundingAccount"] = Relationship(_type="depositAccount", _id=self.funding_account_id) + + def to_json_api(self) -> Dict: + payload = { + "data": { + "type": self.type, + "attributes": { + "amount": self.amount, + "description": self.description + }, + "relationships": self.relationships + } + } + + if self.idempotency_key: + payload["data"]["attributes"]["idempotencyKey"] = self.idempotency_key + + if self.tags: + payload["data"]["attributes"]["tags"] = self.tags + + return payload + + def __repr__(self): + json.dumps(self.to_json_api()) + + +class ListRewardsParams(UnitParams): + def __init__( + self, + limit: int = 100, + offset: int = 0, + transaction_id: Optional[str] = None, + rewarded_transaction_id: Optional[str] = None, + receiving_account_id: Optional[str] = None, + customer_id: Optional[str] = None, + card_id: Optional[str] = None, + status: Optional[str] = None, + since: Optional[datetime] = None, + until: Optional[datetime] = None, + sort: Optional[SORT_ORDERS] = None, + include: Optional[List[RELATED_RESOURCES]] = None, + ): + self.limit = limit + self.offset = offset + self.transaction_id = transaction_id + self.rewarded_transaction_id = rewarded_transaction_id + self.receiving_account_id = receiving_account_id + self.customer_id = customer_id + self.card_id = card_id + self.status = status + self.since = since + self.until = until + self.sort = sort + self.include = include + + def to_dict(self) -> Dict: + parameters = {"page[limit]": self.limit, "page[offset]": self.offset} + + if self.transaction_id: + parameters["filter[transactionId]"] = self.transaction_id + + if self.rewarded_transaction_id: + parameters["filter[rewardedTransactionId]"] = self.rewarded_transaction_id + + if self.receiving_account_id: + parameters["filter[receivingAccountId]"] = self.receiving_account_id + + if self.customer_id: + parameters["filter[customerId]"] = self.customer_id + + if self.card_id: + parameters["filter[cardId]"] = self.card_id + + if self.status: + parameters["filter[status]"] = self.status + + if self.since: + parameters["filter[since]"] = self.since + + if self.unitl: + parameters["filter[until]"] = self.until + + if self.sort: + parameters["sort"] = self.sort + + return parameters diff --git a/unit/models/statement.py b/unit/models/statement.py index 0b519ade..590e2604 100644 --- a/unit/models/statement.py +++ b/unit/models/statement.py @@ -16,7 +16,7 @@ def from_json_api(_id, _type, attributes, relationships): OutputType = Literal["html", "pdf"] -class GetStatementParams(object): +class GetStatementParams(UnitRequest): def __init__(self, statement_id: str, output_type: Optional[OutputType] = "html", language: Optional[str] = "en", customer_id: Optional[str] = None): self.statement_id = statement_id diff --git a/unit/models/transaction.py b/unit/models/transaction.py index 67d40f70..59e2d17e 100644 --- a/unit/models/transaction.py +++ b/unit/models/transaction.py @@ -113,8 +113,8 @@ def from_json_api(_id, _type, attributes, relationships): class BookTransactionDTO(BaseTransactionDTO): def __init__(self, id: str, created_at: datetime, direction: str, amount: int, balance: int, - summary: str, counterparty: Counterparty, tags: Optional[Dict[str, str]], - relationships: Optional[Dict[str, Relationship]]): + summary: str, counterparty: Counterparty, tags: Optional[Dict[str, str]] = None, + relationships: Optional[Dict[str, Relationship]] = None): BaseTransactionDTO.__init__(self, id, created_at, direction, amount, balance, summary, tags, relationships) self.type = 'bookTransaction' self.attributes["counterparty"] = counterparty @@ -122,21 +122,28 @@ def __init__(self, id: str, created_at: datetime, direction: str, amount: int, b @staticmethod def from_json_api(_id, _type, attributes, relationships): return BookTransactionDTO( - _id, date_utils.to_datetime(attributes["createdAt"]), attributes["direction"], - attributes["amount"], attributes["balance"], attributes["summary"], - Counterparty.from_json_api(attributes["counterparty"]), attributes.get("tags"), relationships) + id=_id, created_at=date_utils.to_datetime(attributes["createdAt"]), direction=attributes["direction"], + amount=attributes["amount"], balance=attributes["balance"], summary=attributes["summary"], + counterparty=Counterparty.from_json_api(attributes["counterparty"]), + tags=attributes.get("tags"), relationships=relationships + ) class PurchaseTransactionDTO(BaseTransactionDTO): def __init__(self, id: str, created_at: datetime, direction: str, amount: int, balance: int, - summary: str, card_last_4_digits: str, merchant: Merchant, coordinates: Coordinates, recurring: bool, - interchange: Optional[int], ecommerce: bool, card_present: bool, payment_method: Optional[str], - digital_wallet: Optional[str], card_verification_data, card_network: Optional[str], - tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + summary: str, card_last_4_digits: str, merchant: Optional[Merchant], coordinates: Optional[Coordinates], + recurring: bool, ecommerce: bool, card_present: bool, card_verification_data, + interchange: Optional[int] = None, payment_method: Optional[str] = None, + digital_wallet: Optional[str] = None, card_network: Optional[str] = None, + tags: Optional[Dict[str, str]] = None, relationships: Optional[Dict[str, Relationship]] = None, + gross_interchange: Optional[str] = None, cash_withdrawal_amount: Optional[int] = None, + currency_conversion: Optional[CurrencyConversion] = None, + rich_merchant_data: Optional[RichMerchantData] = None, last_4_digits: str = None, ): BaseTransactionDTO.__init__(self, id, created_at, direction, amount, balance, summary, tags, relationships) self.type = 'purchaseTransaction' self.attributes["cardLast4Digits"] = card_last_4_digits - self.attributes["merchant"] = merchant + if merchant: + self.attributes["merchant"] = merchant self.attributes["coordinates"] = coordinates self.attributes["recurring"] = recurring self.attributes["interchange"] = interchange @@ -146,17 +153,33 @@ def __init__(self, id: str, created_at: datetime, direction: str, amount: int, b self.attributes["digitalWallet"] = digital_wallet self.attributes["cardVerificationData"] = card_verification_data self.attributes["cardNetwork"] = card_network + self.attributes["grossInterchange"] = gross_interchange + self.attributes["cashWithdrawalAmount"] = cash_withdrawal_amount + self.attributes["currencyConversion"] = currency_conversion + self.attributes["richMerchantData"] = rich_merchant_data + + # Unit incorrectly returns last4Digits for simulation responses + if last_4_digits: + self.attributes["last4Digits"] = last_4_digits @staticmethod def from_json_api(_id, _type, attributes, relationships): + # Purchase simulations do not return the merchant attribute + simulation_merchant = dict( + name=attributes.get("merchantName", None), + type=attributes.get("merchantType", None), + location=attributes.get("merchantLocation", None), + ) return PurchaseTransactionDTO( - _id, date_utils.to_datetime(attributes["createdAt"]), attributes["direction"], - attributes["amount"], attributes["balance"], attributes["summary"], attributes["cardLast4Digits"], - Merchant.from_json_api(attributes["merchant"]), Coordinates.from_json_api(attributes["coordinates"]), - attributes["recurring"], attributes.get("interchange"), attributes.get("ecommerce"), - attributes.get("cardPresent"), attributes.get("paymentMethod"), attributes.get("digitalWallet"), - attributes.get("cardVerificationData"), attributes.get("cardNetwork"), attributes.get("tags"), - relationships) + _id, date_utils.to_datetime(attributes["createdAt"]), attributes["direction"], attributes["amount"], + attributes["balance"], attributes.get("summary"), attributes.get("cardLast4Digits", None), + Merchant.from_json_api(attributes.get("merchant") or simulation_merchant), Coordinates.from_json_api(attributes.get("coordinates")), + attributes["recurring"], attributes.get("ecommerce"), attributes.get("cardPresent"), + attributes.get("cardVerificationData"), attributes.get("interchange"), attributes.get("paymentMethod"), + attributes.get("digitalWallet"), attributes.get("cardNetwork"), attributes.get("tags"), + relationships, attributes.get("grossInterchange"), attributes.get("cashWithdrawalAmount"), + CurrencyConversion.from_json_api(attributes.get("currencyConversion")), + RichMerchantData.from_json_api(attributes.get("richMerchantData")), attributes.get("last4Digits", None)) class AtmTransactionDTO(BaseTransactionDTO): @@ -197,14 +220,15 @@ def from_json_api(_id, _type, attributes, relationships): class CardTransactionDTO(BaseTransactionDTO): def __init__(self, id: str, created_at: datetime, direction: str, amount: int, balance: int, - summary: str, card_last_4_digits: str, merchant: Merchant, recurring: Optional[bool], + summary: str, card_last_4_digits: str, merchant: Optional[Merchant], recurring: Optional[bool], interchange: Optional[int], payment_method: Optional[str], digital_wallet: Optional[str], card_verification_data: Optional[Dict], card_network: Optional[str], tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): BaseTransactionDTO.__init__(self, id, created_at, direction, amount, balance, summary, tags, relationships) self.type = 'cardTransaction' self.attributes["cardLast4Digits"] = card_last_4_digits - self.attributes["merchant"] = merchant + if merchant: + self.attributes["merchant"] = merchant self.attributes["recurring"] = recurring self.attributes["interchange"] = interchange self.attributes["paymentMethod"] = payment_method @@ -217,7 +241,7 @@ def __init__(self, id: str, created_at: datetime, direction: str, amount: int, b def from_json_api(_id, _type, attributes, relationships): return CardTransactionDTO(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["direction"], attributes["amount"], attributes["balance"], attributes["summary"], - attributes["cardLast4Digits"], Merchant.from_json_api(attributes["merchant"]), + attributes["cardLast4Digits"], Merchant.from_json_api(attributes.get("merchant")), attributes.get("recurring"), attributes.get("interchange"), attributes.get("paymentMethod"), attributes.get("digitalWallet"), attributes.get("cardVerificationData"), attributes.get("cardNetwork"), @@ -361,6 +385,35 @@ def from_json_api(_id, _type, attributes, relationships): attributes["amount"], attributes["balance"], attributes["summary"], attributes["reason"], attributes.get("tags"), relationships) + +class CheckPaymentTransactionDTO(BaseTransactionDTO): + def __init__(self, id: str, created_at: datetime, direction: str, amount: int, balance: int, summary: str, + tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseTransactionDTO.__init__(self, id, created_at, direction, amount, balance, summary, tags, relationships) + self.type = 'checkPaymentTransaction' + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return CheckPaymentTransactionDTO(_id, date_utils.to_datetime(attributes["createdAt"]), attributes["direction"], + attributes["amount"], attributes["balance"], attributes["summary"], + attributes.get("tags"), relationships) + + +class ReturnedCheckPaymentTransactionDTO(BaseTransactionDTO): + def __init__(self, id: str, created_at: datetime, direction: str, amount: int, balance: int, summary: str, + return_reason: str, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseTransactionDTO.__init__(self, id, created_at, direction, amount, balance, summary, tags, relationships) + self.type = 'returnedCheckPaymentTransaction' + self.attributes["returnReason"] = return_reason + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return ReturnedCheckPaymentTransactionDTO(_id, date_utils.to_datetime(attributes["createdAt"]), + attributes["direction"], + attributes["amount"], attributes["balance"], attributes["summary"], + attributes["returnReason"], attributes.get("tags"), relationships) + + class PaymentAdvanceTransactionDTO(BaseTransactionDTO): def __init__(self, id: str, created_at: datetime, direction: str, amount: int, balance: int, summary: str, reason: str, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): @@ -387,13 +440,50 @@ def from_json_api(_id, _type, attributes, relationships): attributes["amount"], attributes["balance"], attributes["summary"], attributes.get("tags"), relationships) +class AccountLowBalanceClosureTransactionDTO(BaseTransactionDTO): + def __init__(self, id: str, created_at: datetime, amount: int, direction: str, + balance: int, summary: str, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseTransactionDTO.__init__(self, id, created_at, direction, amount, balance, summary, tags, relationships) + self.type = 'accountLowBalanceClosureTransaction' + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return AccountLowBalanceClosureTransactionDTO( + _id, date_utils.to_datetime(attributes["createdAt"]), + attributes["amount"], attributes["direction"], attributes["balance"], attributes["summary"], attributes.get("tags"), relationships) + +class NegativeBalanceCoverageTransactionDTO(BaseTransactionDTO): + def __init__(self, id: str, created_at: datetime, amount: int, direction: str, + balance: int, summary: str, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseTransactionDTO.__init__(self, id, created_at, direction, amount, balance, summary, tags, relationships) + self.type = 'negativeBalanceCoverageTransaction' + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return NegativeBalanceCoverageTransactionDTO( + _id, date_utils.to_datetime(attributes["createdAt"]), attributes["amount"], attributes["direction"], attributes["balance"], attributes["summary"], attributes.get("tags"), relationships) + +class WriteOffTransactionDTO(BaseTransactionDTO): + def __init__(self, id: str, created_at: datetime, amount: int, direction: str, + balance: int, summary: str, tags: Optional[Dict[str, str]], relationships: Optional[Dict[str, Relationship]]): + BaseTransactionDTO.__init__(self, id, created_at, direction, amount, balance, summary, tags, relationships) + self.type = 'writeOffTransaction' + + @staticmethod + def from_json_api(_id, _type, attributes, relationships): + return WriteOffTransactionDTO( + _id, date_utils.to_datetime(attributes["createdAt"]), attributes["amount"], attributes["direction"], attributes["balance"], attributes["summary"], attributes.get("tags"), relationships) + + TransactionDTO = Union[OriginatedAchTransactionDTO, ReceivedAchTransactionDTO, ReturnedAchTransactionDTO, ReturnedReceivedAchTransactionDTO, DishonoredAchTransactionDTO, BookTransactionDTO, PurchaseTransactionDTO, AtmTransactionDTO, FeeTransactionDTO, CardTransactionDTO, CardReversalTransactionDTO, WireTransactionDTO, ReleaseTransactionDTO, AdjustmentTransactionDTO, InterestTransactionDTO, DisputeTransactionDTO, CheckDepositTransactionDTO, - ReturnedCheckDepositTransactionDTO, PaymentAdvanceTransactionDTO, - RepaidPaymentAdvanceTransactionDTO] + ReturnedCheckDepositTransactionDTO, CheckPaymentTransactionDTO, + ReturnedCheckPaymentTransactionDTO, PaymentAdvanceTransactionDTO, + RepaidPaymentAdvanceTransactionDTO, AccountLowBalanceClosureTransactionDTO, NegativeBalanceCoverageTransactionDTO, + WriteOffTransactionDTO] class PatchTransactionRequest(BaseTransactionDTO, UnitRequest): @@ -461,4 +551,118 @@ def to_dict(self) -> Dict: parameters["sort"] = self.sort if self.include: parameters["include"] = self.include - return parameters \ No newline at end of file + return parameters + + +class SimulatePurchaseTransaction(UnitRequest): + def __init__( + self, + amount: int, + card_id: str, + last_4_Digits: str, + deposit_account_id: str, + merchantName: str, + merchantType: str, + merchantLocation: str, + direction: str = "Debit", + authorization_id: str = None, + ): + self.authorization_id = authorization_id + self.last_4_Digits = last_4_Digits + self.deposit_account_id = deposit_account_id + self.direction = direction + self.amount = amount + self.card_id = card_id + self.merchantName = merchantName + self.merchantType = merchantType + self.merchantLocation = merchantLocation + + def to_json_api(self) -> Dict: + payload = { + "data": { + "type": "purchaseTransaction", + "attributes": { + "amount": self.amount, + "direction": self.direction, + "last4Digits": self.last_4_Digits, + "merchantName": self.merchantName, + "merchantType": self.merchantType, + "merchantLocation": self.merchantLocation, + "recurring": False + }, + "relationships": { + "account": { + "data": { + "type": "depositAccount", + "id": self.deposit_account_id + } + } + } + + } + } + + if self.authorization_id: + payload["data"]["relationships"]["authorization"] = { + "data": { + "type": "authorization", + "id": self.authorization_id + } + } + + return payload + + def __repr__(self): + json.dumps(self.to_json_api()) + + +class SimulateCardTransaction(UnitRequest): + def __init__( + self, + amount: int, + card_id: str, + card_last_4_Digits: str, + deposit_account: str, + merchantName: str, + merchantType: str, + merchantLocation: str, + direction: str = "Debit", + ): + self.card_last_4_Digits = card_last_4_Digits + self.deposit_account = deposit_account + self.direction = direction + self.amount = amount + self.card_id = card_id + self.merchantName = merchantName + self.merchantType = merchantType + self.merchantLocation = merchantLocation + + def to_json_api(self) -> Dict: + payload = { + "data": { + "type": "cardTransaction", + "attributes": { + "amount": self.amount, + "direction": self.direction, + "cardLast4Digits": self.card_last_4_Digits, + "merchantName": "The Home Depot", + "merchantType": self.merchantType, + "merchantLocation": self.merchantLocation, + "recurring": False + }, + "relationships": { + "account": { + "data": { + "type": "depositAccount", + "id": self.deposit_account + } + }, + } + + } + } + + return payload + + def __repr__(self): + json.dumps(self.to_json_api()) \ No newline at end of file diff --git a/unit_python_sdk.egg-info/PKG-INFO b/unit_python_sdk.egg-info/PKG-INFO new file mode 100644 index 00000000..c11c3a0c --- /dev/null +++ b/unit_python_sdk.egg-info/PKG-INFO @@ -0,0 +1,29 @@ +Metadata-Version: 2.4 +Name: unit-python-sdk +Version: 0.10.5 +Summary: This library provides a python wrapper to http://unit.co API. See https://docs.unit.co/ +Home-page: https://github.com/unit-finance/unit-python-sdk +Download-URL: https://github.com/unit-finance/unit-python-sdk.git +Author: unit.co +Author-email: dev@unit.co +License: Mozilla Public License 2.0 +Keywords: unit,finance,banking,banking-as-a-service,API,SDK +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: Topic :: Software Development :: Build Tools +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +License-File: LICENSE +Requires-Dist: requests +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: download-url +Dynamic: home-page +Dynamic: keywords +Dynamic: license +Dynamic: license-file +Dynamic: requires-dist +Dynamic: summary diff --git a/unit_python_sdk.egg-info/SOURCES.txt b/unit_python_sdk.egg-info/SOURCES.txt new file mode 100644 index 00000000..09594612 --- /dev/null +++ b/unit_python_sdk.egg-info/SOURCES.txt @@ -0,0 +1,79 @@ +LICENSE +README.md +setup.cfg +setup.py +unit/__init__.py +unit/api/__init__.py +unit/api/account_end_of_day_resource.py +unit/api/account_resource.py +unit/api/accrued_interest_resource.py +unit/api/ach_resource.py +unit/api/api_token_resource.py +unit/api/applicationForm_resource.py +unit/api/application_resource.py +unit/api/atmLocation_resource.py +unit/api/authorization_request_resource.py +unit/api/authorization_resource.py +unit/api/base_resource.py +unit/api/batch_release_resource.py +unit/api/bill_pay_resource.py +unit/api/card_resource.py +unit/api/check_deposit_resource.py +unit/api/check_payment_resource.py +unit/api/check_registered_address.py +unit/api/check_stop_payment_resource.py +unit/api/counterparty_resource.py +unit/api/customerToken_resource.py +unit/api/customer_resource.py +unit/api/dispute_resource.py +unit/api/event_resource.py +unit/api/fee_resource.py +unit/api/institution_resource.py +unit/api/payment_resource.py +unit/api/received_payment_resource.py +unit/api/repayment_resource.py +unit/api/returnAch_resource.py +unit/api/reward_resource.py +unit/api/statement_resource.py +unit/api/transaction_resource.py +unit/api/webhook_resource.py +unit/models/__init__.py +unit/models/account.py +unit/models/account_end_of_day.py +unit/models/accrued_interest.py +unit/models/api_token.py +unit/models/application.py +unit/models/applicationForm.py +unit/models/atm_location.py +unit/models/authorization.py +unit/models/authorization_request.py +unit/models/batch_release.py +unit/models/benificial_owner.py +unit/models/bill_pay.py +unit/models/card.py +unit/models/check_deposit.py +unit/models/check_payment.py +unit/models/check_registered_address.py +unit/models/check_stop_payment.py +unit/models/codecs.py +unit/models/counterparty.py +unit/models/customer.py +unit/models/customerToken.py +unit/models/dispute.py +unit/models/event.py +unit/models/fee.py +unit/models/institution.py +unit/models/payment.py +unit/models/repayment.py +unit/models/returnAch.py +unit/models/reward.py +unit/models/statement.py +unit/models/transaction.py +unit/models/webhook.py +unit/utils/__init__.py +unit/utils/date_utils.py +unit_python_sdk.egg-info/PKG-INFO +unit_python_sdk.egg-info/SOURCES.txt +unit_python_sdk.egg-info/dependency_links.txt +unit_python_sdk.egg-info/requires.txt +unit_python_sdk.egg-info/top_level.txt \ No newline at end of file diff --git a/unit_python_sdk.egg-info/dependency_links.txt b/unit_python_sdk.egg-info/dependency_links.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/unit_python_sdk.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/unit_python_sdk.egg-info/requires.txt b/unit_python_sdk.egg-info/requires.txt new file mode 100644 index 00000000..f2293605 --- /dev/null +++ b/unit_python_sdk.egg-info/requires.txt @@ -0,0 +1 @@ +requests diff --git a/unit_python_sdk.egg-info/top_level.txt b/unit_python_sdk.egg-info/top_level.txt new file mode 100644 index 00000000..47b23a20 --- /dev/null +++ b/unit_python_sdk.egg-info/top_level.txt @@ -0,0 +1 @@ +unit