Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/tmo_api/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
"""Models package for The Mortgage Office SDK."""

from .base import BaseModel, BaseResponse
from .pool import OtherAsset, OtherLiability, Pool, PoolResponse, PoolsResponse

__all__ = [
"BaseModel",
"BaseResponse",
"Pool",
"PoolResponse",
"PoolsResponse",
"OtherAsset",
"OtherLiability",
]
84 changes: 84 additions & 0 deletions src/tmo_api/models/pool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Pool-related models for The Mortgage Office SDK."""

from typing import Any, Dict, List, Optional

from .base import BaseModel, BaseResponse


class OtherAsset(BaseModel):
"""Represents an other asset in a mortgage pool."""

def _parse_data(self, data: Dict[str, Any]) -> None:
super()._parse_data(data)

# Parse dates specifically (since they need conversion)
if "DateLastEvaluated" in data:
self.DateLastEvaluated = self._parse_date(data.get("DateLastEvaluated"))


class OtherLiability(BaseModel):
"""Represents an other liability in a mortgage pool."""

def _parse_data(self, data: Dict[str, Any]) -> None:
super()._parse_data(data)

# Parse dates specifically (since they need conversion)
if "MaturityDate" in data:
self.MaturityDate = self._parse_date(data.get("MaturityDate"))
if "PaymentNextDue" in data:
self.PaymentNextDue = self._parse_date(data.get("PaymentNextDue"))


class Pool(BaseModel):
"""Represents a mortgage pool."""

def _parse_data(self, data: Dict[str, Any]) -> None:
super()._parse_data(data)

# Parse dates specifically (since they need conversion)
if "InceptionDate" in data:
self.InceptionDate = self._parse_date(data.get("InceptionDate"))
if "LastEvaluation" in data:
self.LastEvaluation = self._parse_date(data.get("LastEvaluation"))
if "SysTimeStamp" in data:
self.SysTimeStamp = self._parse_date(data.get("SysTimeStamp"))

# Parse nested objects (override the raw arrays with parsed objects)
if "OtherAssets" in data:
self.OtherAssets: List[OtherAsset] = []
for asset_data in data.get("OtherAssets", []):
self.OtherAssets.append(OtherAsset(asset_data))

if "OtherLiabilities" in data:
self.OtherLiabilities: List[OtherLiability] = []
for liability_data in data.get("OtherLiabilities", []):
self.OtherLiabilities.append(OtherLiability(liability_data))


class PoolResponse(BaseResponse):
"""Response containing pool data."""

pool: Optional["Pool"]

def __init__(self, data: Dict[str, Any]) -> None:
super().__init__(data)
if self.data:
self.pool = Pool(self.data)
else:
self.pool = None


class PoolsResponse(BaseResponse):
"""Response containing multiple pools."""

def __init__(self, data: Dict[str, Any]) -> None:
super().__init__(data)
self.pools: List[Pool] = []

# Handle both single pool and list of pools
pool_data: Any = self.data
if isinstance(pool_data, list):
for item in pool_data:
self.pools.append(Pool(item))
elif isinstance(pool_data, dict) and pool_data:
self.pools.append(Pool(pool_data))
16 changes: 10 additions & 6 deletions src/tmo_api/resources/pools.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Pools resource for The Mortgage Office SDK."""

from enum import Enum
from typing import TYPE_CHECKING, Any, Dict, List, cast
from typing import TYPE_CHECKING, Any, List, cast

from ..models.pool import Pool, PoolResponse, PoolsResponse

if TYPE_CHECKING:
from ..client import TheMortgageOfficeClient
Expand Down Expand Up @@ -30,14 +32,14 @@ def __init__(
self.pool_type = pool_type
self.base_path = f"LSS.svc/{pool_type.value}"

def get_pool(self, account: str) -> Dict[str, Any]:
def get_pool(self, account: str) -> Pool:
"""Get pool details by account.

Args:
account: The pool account identifier

Returns:
Pool data dictionary
Pool object with detailed information

Raises:
APIError: If the API returns an error
Expand All @@ -50,7 +52,8 @@ def get_pool(self, account: str) -> Dict[str, Any]:

endpoint = f"{self.base_path}/Pools/{account}"
response_data = self.client.get(endpoint)
return response_data
response = PoolResponse(response_data)
return response.pool # type: ignore

def get_pool_partners(self, account: str) -> list:
"""Get pool partners by account.
Expand Down Expand Up @@ -140,7 +143,7 @@ def get_pool_attachments(self, account: str) -> list:
response_data = self.client.get(endpoint)
return cast(List[Any], response_data.get("Data", []))

def list_all(self) -> list:
def list_all(self) -> List[Pool]:
"""List all pools.

Returns:
Expand All @@ -151,4 +154,5 @@ def list_all(self) -> list:
"""
endpoint = f"{self.base_path}/Pools"
response_data = self.client.get(endpoint)
return cast(List[Any], response_data.get("Data", []))
response = PoolsResponse(response_data)
return response.pools
206 changes: 206 additions & 0 deletions tests/test_models_pool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
"""Tests for Pool models."""

from datetime import datetime

import pytest

from tmo_api.models.pool import OtherAsset, OtherLiability, Pool, PoolResponse, PoolsResponse


class TestOtherAsset:
"""Test OtherAsset model."""

def test_other_asset_initialization(self):
"""Test OtherAsset initialization."""
data = {
"rec_id": 123,
"Description": "Test Asset",
"Value": 10000.00,
"DateLastEvaluated": "12/31/2024",
}
asset = OtherAsset(data)

assert asset.rec_id == 123
assert asset.Description == "Test Asset"
assert asset.Value == 10000.00
assert isinstance(asset.DateLastEvaluated, datetime)
assert asset.DateLastEvaluated == datetime(2024, 12, 31)

def test_other_asset_without_date(self):
"""Test OtherAsset without date."""
data = {"rec_id": 456, "Description": "Asset without date"}
asset = OtherAsset(data)

assert asset.rec_id == 456
assert asset.Description == "Asset without date"


class TestOtherLiability:
"""Test OtherLiability model."""

def test_other_liability_initialization(self):
"""Test OtherLiability initialization."""
data = {
"rec_id": 789,
"Description": "Test Liability",
"Balance": 50000.00,
"MaturityDate": "06/30/2025",
"PaymentNextDue": "01/15/2025",
}
liability = OtherLiability(data)

assert liability.rec_id == 789
assert liability.Description == "Test Liability"
assert liability.Balance == 50000.00
assert isinstance(liability.MaturityDate, datetime)
assert liability.MaturityDate == datetime(2025, 6, 30)
assert isinstance(liability.PaymentNextDue, datetime)
assert liability.PaymentNextDue == datetime(2025, 1, 15)

def test_other_liability_without_dates(self):
"""Test OtherLiability without dates."""
data = {"rec_id": 101, "Description": "Liability without dates"}
liability = OtherLiability(data)

assert liability.rec_id == 101
assert liability.Description == "Liability without dates"


class TestPool:
"""Test Pool model."""

def test_pool_initialization(self):
"""Test Pool initialization with basic data."""
data = {
"rec_id": 1,
"Account": "POOL001",
"Name": "Test Pool",
"InceptionDate": "01/01/2024",
"LastEvaluation": "12/31/2024",
"SysTimeStamp": "11/15/2024",
}
pool = Pool(data)

assert pool.rec_id == 1
assert pool.Account == "POOL001"
assert pool.Name == "Test Pool"
assert isinstance(pool.InceptionDate, datetime)
assert pool.InceptionDate == datetime(2024, 1, 1)
assert isinstance(pool.LastEvaluation, datetime)
assert pool.LastEvaluation == datetime(2024, 12, 31)
assert isinstance(pool.SysTimeStamp, datetime)
assert pool.SysTimeStamp == datetime(2024, 11, 15)

def test_pool_with_nested_objects(self):
"""Test Pool with nested OtherAssets and OtherLiabilities."""
data = {
"rec_id": 2,
"Account": "POOL002",
"OtherAssets": [
{"rec_id": 10, "Description": "Asset 1", "Value": 1000},
{"rec_id": 11, "Description": "Asset 2", "Value": 2000},
],
"OtherLiabilities": [
{"rec_id": 20, "Description": "Liability 1", "Balance": 5000},
],
}
pool = Pool(data)

assert pool.rec_id == 2
assert len(pool.OtherAssets) == 2
assert isinstance(pool.OtherAssets[0], OtherAsset)
assert pool.OtherAssets[0].Description == "Asset 1"
assert pool.OtherAssets[1].Description == "Asset 2"

assert len(pool.OtherLiabilities) == 1
assert isinstance(pool.OtherLiabilities[0], OtherLiability)
assert pool.OtherLiabilities[0].Description == "Liability 1"

def test_pool_repr(self):
"""Test Pool string representation."""
data = {"rec_id": 999, "Account": "POOL999"}
pool = Pool(data)
assert repr(pool) == "Pool(999)"


class TestPoolResponse:
"""Test PoolResponse model."""

def test_pool_response_with_data(self):
"""Test PoolResponse with pool data."""
response_data = {
"Status": 0,
"Data": {
"rec_id": 1,
"Account": "POOL001",
"Name": "Test Pool",
},
}
response = PoolResponse(response_data)

assert response.status == 0
assert response.pool is not None
assert isinstance(response.pool, Pool)
assert response.pool.Account == "POOL001"

def test_pool_response_without_data(self):
"""Test PoolResponse without pool data."""
response_data = {"Status": 1, "ErrorMessage": "Not found", "ErrorNumber": 404}
response = PoolResponse(response_data)

assert response.status == 1
assert response.pool is None
assert response.error_message == "Not found"

def test_pool_response_repr(self):
"""Test PoolResponse string representation."""
response_data = {"Status": 0, "Data": {"rec_id": 1}}
response = PoolResponse(response_data)
assert repr(response) == "PoolResponse(status=0)"


class TestPoolsResponse:
"""Test PoolsResponse model."""

def test_pools_response_with_list(self):
"""Test PoolsResponse with list of pools."""
response_data = {
"Status": 0,
"Data": [
{"rec_id": 1, "Account": "POOL001"},
{"rec_id": 2, "Account": "POOL002"},
{"rec_id": 3, "Account": "POOL003"},
],
}
response = PoolsResponse(response_data)

assert response.status == 0
assert len(response.pools) == 3
assert all(isinstance(pool, Pool) for pool in response.pools)
assert response.pools[0].Account == "POOL001"
assert response.pools[1].Account == "POOL002"
assert response.pools[2].Account == "POOL003"

def test_pools_response_with_single_pool(self):
"""Test PoolsResponse with single pool dict."""
response_data = {"Status": 0, "Data": {"rec_id": 1, "Account": "POOL001"}}
response = PoolsResponse(response_data)

assert response.status == 0
assert len(response.pools) == 1
assert isinstance(response.pools[0], Pool)
assert response.pools[0].Account == "POOL001"

def test_pools_response_empty(self):
"""Test PoolsResponse with empty data."""
response_data = {"Status": 0, "Data": []}
response = PoolsResponse(response_data)

assert response.status == 0
assert len(response.pools) == 0

def test_pools_response_repr(self):
"""Test PoolsResponse string representation."""
response_data = {"Status": 0, "Data": []}
response = PoolsResponse(response_data)
assert repr(response) == "PoolsResponse(status=0)"
3 changes: 3 additions & 0 deletions tests/test_resources_pools.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from tmo_api.client import TheMortgageOfficeClient
from tmo_api.exceptions import ValidationError
from tmo_api.models.pool import Pool
from tmo_api.resources.pools import PoolsResource, PoolType


Expand Down Expand Up @@ -40,6 +41,7 @@ def test_get_pool_success(self, mock_get, client, mock_pool_account, mock_api_re

mock_get.assert_called_once_with(f"LSS.svc/Shares/Pools/{mock_pool_account}")
assert pool is not None
assert isinstance(pool, Pool)

@patch.object(TheMortgageOfficeClient, "get")
def test_get_pool_empty_account(self, mock_get, client):
Expand Down Expand Up @@ -106,6 +108,7 @@ def test_list_all_pools(self, mock_get, client, mock_pools_response):

mock_get.assert_called_once_with("LSS.svc/Shares/Pools")
assert isinstance(pools, list)
assert all(isinstance(pool, Pool) for pool in pools)

def test_pool_type_enum(self):
"""Test PoolType enum values."""
Expand Down
Loading