diff --git a/pyproject.toml b/pyproject.toml index d7973a1..063ee85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ dev = [ "flake8>=6.0.0", "isort>=5.12.0", "mypy>=1.0.0", + "types-requests>=2.31.0", ] [build-system] diff --git a/src/tmo_api/__init__.py b/src/tmo_api/__init__.py index bd7b6f3..9b85fad 100644 --- a/src/tmo_api/__init__.py +++ b/src/tmo_api/__init__.py @@ -10,7 +10,14 @@ ValidationError, ) from .models import BaseModel, BaseResponse -from .resources import PoolsResource, PoolType +from .resources import ( + CertificatesResource, + DistributionsResource, + HistoryResource, + PartnersResource, + PoolsResource, + PoolType, +) __version__ = "0.0.1" @@ -27,4 +34,8 @@ "BaseResponse", "PoolsResource", "PoolType", + "PartnersResource", + "DistributionsResource", + "CertificatesResource", + "HistoryResource", ] diff --git a/src/tmo_api/client.py b/src/tmo_api/client.py index 2f8fb48..6e08eb4 100644 --- a/src/tmo_api/client.py +++ b/src/tmo_api/client.py @@ -9,7 +9,13 @@ from .environments import DEFAULT_ENVIRONMENT, Environment from .exceptions import APIError, AuthenticationError, NetworkError -from .resources import PoolsResource +from .resources import ( + CertificatesResource, + DistributionsResource, + HistoryResource, + PartnersResource, + PoolsResource, +) class TheMortgageOfficeClient: @@ -62,16 +68,20 @@ def __init__( # Initialize Shares resources self.shares_pools: PoolsResource = PoolsResource(self, PoolType.SHARES) - self.shares_partners: PoolsResource = PoolsResource(self, PoolType.SHARES) - self.shares_distributions: PoolsResource = PoolsResource(self, PoolType.SHARES) - self.shares_certificates: PoolsResource = PoolsResource(self, PoolType.SHARES) - self.shares_history: PoolsResource = PoolsResource(self, PoolType.SHARES) + self.shares_partners: PartnersResource = PartnersResource(self, PoolType.SHARES) + self.shares_distributions: DistributionsResource = DistributionsResource( + self, PoolType.SHARES + ) + self.shares_certificates: CertificatesResource = CertificatesResource(self, PoolType.SHARES) + self.shares_history: HistoryResource = HistoryResource(self, PoolType.SHARES) # Initialize Capital resources self.capital_pools: PoolsResource = PoolsResource(self, PoolType.CAPITAL) - self.capital_partners: PoolsResource = PoolsResource(self, PoolType.CAPITAL) - self.capital_distributions: PoolsResource = PoolsResource(self, PoolType.CAPITAL) - self.capital_history: PoolsResource = PoolsResource(self, PoolType.CAPITAL) + self.capital_partners: PartnersResource = PartnersResource(self, PoolType.CAPITAL) + self.capital_distributions: DistributionsResource = DistributionsResource( + self, PoolType.CAPITAL + ) + self.capital_history: HistoryResource = HistoryResource(self, PoolType.CAPITAL) def _debug_log(self, message: str) -> None: """Log debug message to stderr if debug mode is enabled.""" diff --git a/src/tmo_api/resources/__init__.py b/src/tmo_api/resources/__init__.py index f4eb833..f28cd44 100644 --- a/src/tmo_api/resources/__init__.py +++ b/src/tmo_api/resources/__init__.py @@ -1,5 +1,16 @@ """Resources package for The Mortgage Office SDK.""" +from .certificates import CertificatesResource +from .distributions import DistributionsResource +from .history import HistoryResource +from .partners import PartnersResource from .pools import PoolsResource, PoolType -__all__ = ["PoolsResource", "PoolType"] +__all__ = [ + "PoolsResource", + "PoolType", + "PartnersResource", + "DistributionsResource", + "CertificatesResource", + "HistoryResource", +] diff --git a/src/tmo_api/resources/certificates.py b/src/tmo_api/resources/certificates.py new file mode 100644 index 0000000..73afea5 --- /dev/null +++ b/src/tmo_api/resources/certificates.py @@ -0,0 +1,91 @@ +"""Certificates resource for The Mortgage Office SDK.""" + +from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast + +from .pools import PoolType + +if TYPE_CHECKING: + from ..client import TheMortgageOfficeClient + + +class CertificatesResource: + """Resource for managing share certificates.""" + + def __init__( + self, client: "TheMortgageOfficeClient", pool_type: PoolType = PoolType.SHARES + ) -> None: + """Initialize the certificates resource. + + Args: + client: The base client instance + pool_type: The type of pool (Shares or Capital) - Note: Certificates + are only available for Shares + """ + self.client = client + self.pool_type = pool_type + self.base_path = f"LSS.svc/{pool_type.value}" + + def get_certificates( + self, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + partner_account: Optional[str] = None, + pool_account: Optional[str] = None, + ) -> List[Any]: + """Get share certificates with optional filtering. + + Args: + start_date: Start date for filtering (MM/DD/YYYY format) + end_date: End date for filtering (MM/DD/YYYY format) + partner_account: Partner account filter + pool_account: Pool account filter + + Returns: + List of share certificates + + Raises: + APIError: If the API returns an error + ValidationError: If date format is invalid + """ + endpoint = f"{self.base_path}/Certificates" + params: Dict[str, str] = {} + + if start_date: + if not self._validate_date_format(start_date): + from ..exceptions import ValidationError + + raise ValidationError("start_date must be in MM/DD/YYYY format") + params["from-date"] = start_date + + if end_date: + if not self._validate_date_format(end_date): + from ..exceptions import ValidationError + + raise ValidationError("end_date must be in MM/DD/YYYY format") + params["to-date"] = end_date + + if partner_account: + params["partner-account"] = partner_account + + if pool_account: + params["pool-account"] = pool_account + + response_data = self.client.get(endpoint, params=params if params else None) + return cast(List[Any], response_data.get("Data", [])) + + def _validate_date_format(self, date_str: str) -> bool: + """Validate date format MM/DD/YYYY. + + Args: + date_str: Date string to validate + + Returns: + True if format is valid, False otherwise + """ + try: + from datetime import datetime + + datetime.strptime(date_str, "%m/%d/%Y") + return True + except ValueError: + return False diff --git a/src/tmo_api/resources/distributions.py b/src/tmo_api/resources/distributions.py new file mode 100644 index 0000000..0a34f73 --- /dev/null +++ b/src/tmo_api/resources/distributions.py @@ -0,0 +1,107 @@ +"""Distributions resource for The Mortgage Office SDK.""" + +from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast + +from .pools import PoolType + +if TYPE_CHECKING: + from ..client import TheMortgageOfficeClient + + +class DistributionsResource: + """Resource for managing pool distributions.""" + + def __init__( + self, client: "TheMortgageOfficeClient", pool_type: PoolType = PoolType.SHARES + ) -> None: + """Initialize the distributions resource. + + Args: + client: The base client instance + pool_type: The type of pool (Shares or Capital) + """ + self.client = client + self.pool_type = pool_type + self.base_path = f"LSS.svc/{pool_type.value}" + + def get_distribution(self, rec_id: str) -> Dict[str, Any]: + """Get distribution details by RecID. + + Args: + rec_id: The distribution record ID + + Returns: + Distribution data dictionary + + Raises: + APIError: If the API returns an error + ValidationError: If rec_id is invalid + """ + if not rec_id: + from ..exceptions import ValidationError + + raise ValidationError("RecID parameter is required") + + endpoint = f"{self.base_path}/Distributions/{rec_id}" + response_data = self.client.get(endpoint) + return cast(Dict[str, Any], response_data.get("Data", {})) + + def list_all( + self, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + pool_account: Optional[str] = None, + ) -> List[Any]: + """List all distributions with optional filtering. + + Args: + start_date: Start date for filtering (MM/DD/YYYY format) + end_date: End date for filtering (MM/DD/YYYY format) + pool_account: Pool account filter + + Returns: + List of distributions + + Raises: + APIError: If the API returns an error + ValidationError: If date format is invalid + """ + endpoint = f"{self.base_path}/Distributions" + params: Dict[str, str] = {} + + if start_date: + if not self._validate_date_format(start_date): + from ..exceptions import ValidationError + + raise ValidationError("start_date must be in MM/DD/YYYY format") + params["from-date"] = start_date + + if end_date: + if not self._validate_date_format(end_date): + from ..exceptions import ValidationError + + raise ValidationError("end_date must be in MM/DD/YYYY format") + params["to-date"] = end_date + + if pool_account: + params["pool-account"] = pool_account + + response_data = self.client.get(endpoint, params=params if params else None) + return cast(List[Any], response_data.get("Data", [])) + + def _validate_date_format(self, date_str: str) -> bool: + """Validate date format MM/DD/YYYY. + + Args: + date_str: Date string to validate + + Returns: + True if format is valid, False otherwise + """ + try: + from datetime import datetime + + datetime.strptime(date_str, "%m/%d/%Y") + return True + except ValueError: + return False diff --git a/src/tmo_api/resources/history.py b/src/tmo_api/resources/history.py new file mode 100644 index 0000000..c97eea6 --- /dev/null +++ b/src/tmo_api/resources/history.py @@ -0,0 +1,90 @@ +"""History resource for The Mortgage Office SDK.""" + +from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast + +from .pools import PoolType + +if TYPE_CHECKING: + from ..client import TheMortgageOfficeClient + + +class HistoryResource: + """Resource for managing share transaction history.""" + + def __init__( + self, client: "TheMortgageOfficeClient", pool_type: PoolType = PoolType.SHARES + ) -> None: + """Initialize the history resource. + + Args: + client: The base client instance + pool_type: The type of pool (Shares or Capital) + """ + self.client = client + self.pool_type = pool_type + self.base_path = f"LSS.svc/{pool_type.value}" + + def get_history( + self, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + partner_account: Optional[str] = None, + pool_account: Optional[str] = None, + ) -> List[Any]: + """Get share transaction history with optional filtering. + + Args: + start_date: Start date for filtering (MM/DD/YYYY format) + end_date: End date for filtering (MM/DD/YYYY format) + partner_account: Partner account filter + pool_account: Pool account filter + + Returns: + List of share transaction history records + + Raises: + APIError: If the API returns an error + ValidationError: If date format is invalid + """ + endpoint = f"{self.base_path}/History" + params: Dict[str, str] = {} + + if start_date: + if not self._validate_date_format(start_date): + from ..exceptions import ValidationError + + raise ValidationError("start_date must be in MM/DD/YYYY format") + params["from-date"] = start_date + + if end_date: + if not self._validate_date_format(end_date): + from ..exceptions import ValidationError + + raise ValidationError("end_date must be in MM/DD/YYYY format") + params["to-date"] = end_date + + if partner_account: + params["partner-account"] = partner_account + + if pool_account: + params["pool-account"] = pool_account + + response_data = self.client.get(endpoint, params=params if params else None) + return cast(List[Any], response_data.get("Data", [])) + + def _validate_date_format(self, date_str: str) -> bool: + """Validate date format MM/DD/YYYY. + + Args: + date_str: Date string to validate + + Returns: + True if format is valid, False otherwise + """ + try: + from datetime import datetime + + datetime.strptime(date_str, "%m/%d/%Y") + return True + except ValueError: + return False diff --git a/src/tmo_api/resources/partners.py b/src/tmo_api/resources/partners.py new file mode 100644 index 0000000..3627509 --- /dev/null +++ b/src/tmo_api/resources/partners.py @@ -0,0 +1,122 @@ +"""Partners resource for The Mortgage Office SDK.""" + +from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast + +from .pools import PoolType + +if TYPE_CHECKING: + from ..client import TheMortgageOfficeClient + + +class PartnersResource: + """Resource for managing pool partners.""" + + def __init__( + self, client: "TheMortgageOfficeClient", pool_type: PoolType = PoolType.SHARES + ) -> None: + """Initialize the partners resource. + + Args: + client: The base client instance + pool_type: The type of pool (Shares or Capital) + """ + self.client = client + self.pool_type = pool_type + self.base_path = f"LSS.svc/{pool_type.value}" + + def get_partner(self, account: str) -> Dict[str, Any]: + """Get partner details by Account. + + Args: + account: The partner account identifier + + Returns: + Partner data dictionary + + Raises: + APIError: If the API returns an error + ValidationError: If account is invalid + """ + if not account: + from ..exceptions import ValidationError + + raise ValidationError("Account parameter is required") + + endpoint = f"{self.base_path}/Partners/{account}" + response_data = self.client.get(endpoint) + return cast(Dict[str, Any], response_data.get("Data", {})) + + def get_partner_attachments(self, account: str) -> List[Any]: + """Get partner attachments by Account. + + Args: + account: The partner account identifier + + Returns: + List of partner attachments + + Raises: + APIError: If the API returns an error + ValidationError: If account is invalid + """ + if not account: + from ..exceptions import ValidationError + + raise ValidationError("Account parameter is required") + + endpoint = f"{self.base_path}/Partners/{account}/Attachments" + response_data = self.client.get(endpoint) + return cast(List[Any], response_data.get("Data", [])) + + def list_all( + self, start_date: Optional[str] = None, end_date: Optional[str] = None + ) -> List[Any]: + """List all partners with optional date filtering. + + Args: + start_date: Start date for filtering (MM/DD/YYYY format) + end_date: End date for filtering (MM/DD/YYYY format) + + Returns: + List of partners + + Raises: + APIError: If the API returns an error + ValidationError: If date format is invalid + """ + endpoint = f"{self.base_path}/Partners" + params: Dict[str, str] = {} + + if start_date: + if not self._validate_date_format(start_date): + from ..exceptions import ValidationError + + raise ValidationError("start_date must be in MM/DD/YYYY format") + params["from-date"] = start_date + + if end_date: + if not self._validate_date_format(end_date): + from ..exceptions import ValidationError + + raise ValidationError("end_date must be in MM/DD/YYYY format") + params["to-date"] = end_date + + response_data = self.client.get(endpoint, params=params if params else None) + return cast(List[Any], response_data.get("Data", [])) + + def _validate_date_format(self, date_str: str) -> bool: + """Validate date format MM/DD/YYYY. + + Args: + date_str: Date string to validate + + Returns: + True if format is valid, False otherwise + """ + try: + from datetime import datetime + + datetime.strptime(date_str, "%m/%d/%Y") + return True + except ValueError: + return False diff --git a/tests/test_resources_certificates.py b/tests/test_resources_certificates.py new file mode 100644 index 0000000..1017090 --- /dev/null +++ b/tests/test_resources_certificates.py @@ -0,0 +1,144 @@ +"""Tests for CertificatesResource.""" + +from unittest.mock import patch + +import pytest + +from tmo_api.client import TheMortgageOfficeClient +from tmo_api.exceptions import ValidationError +from tmo_api.resources.certificates import CertificatesResource +from tmo_api.resources.pools import PoolType + + +class TestCertificatesResource: + """Test CertificatesResource functionality.""" + + @pytest.fixture + def client(self, mock_token, mock_database): + """Create a test client.""" + return TheMortgageOfficeClient(token=mock_token, database=mock_database) + + def test_certificates_resource_init_shares(self, client): + """Test CertificatesResource initialization with Shares type.""" + resource = CertificatesResource(client, PoolType.SHARES) + assert resource.client == client + assert resource.pool_type == PoolType.SHARES + assert resource.base_path == "LSS.svc/Shares" + + def test_certificates_resource_init_capital(self, client): + """Test CertificatesResource initialization with Capital type.""" + resource = CertificatesResource(client, PoolType.CAPITAL) + assert resource.pool_type == PoolType.CAPITAL + assert resource.base_path == "LSS.svc/Capital" + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_certificates_no_filters(self, mock_get, client): + """Test get_certificates without filters.""" + mock_get.return_value = {"Status": 0, "Data": [{"certificate_id": 1}]} + resource = CertificatesResource(client, PoolType.SHARES) + + certificates = resource.get_certificates() + + mock_get.assert_called_once_with("LSS.svc/Shares/Certificates", params=None) + assert isinstance(certificates, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_certificates_with_date_filters(self, mock_get, client): + """Test get_certificates with date filters.""" + mock_get.return_value = {"Status": 0, "Data": [{"certificate_id": 1}]} + resource = CertificatesResource(client, PoolType.SHARES) + + certificates = resource.get_certificates(start_date="01/01/2024", end_date="12/31/2024") + + mock_get.assert_called_once_with( + "LSS.svc/Shares/Certificates", + params={"from-date": "01/01/2024", "to-date": "12/31/2024"}, + ) + assert isinstance(certificates, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_certificates_with_partner_account(self, mock_get, client): + """Test get_certificates with partner_account filter.""" + mock_get.return_value = {"Status": 0, "Data": [{"certificate_id": 1}]} + resource = CertificatesResource(client, PoolType.SHARES) + + certificates = resource.get_certificates(partner_account="PARTNER001") + + mock_get.assert_called_once_with( + "LSS.svc/Shares/Certificates", params={"partner-account": "PARTNER001"} + ) + assert isinstance(certificates, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_certificates_with_pool_account(self, mock_get, client): + """Test get_certificates with pool_account filter.""" + mock_get.return_value = {"Status": 0, "Data": [{"certificate_id": 1}]} + resource = CertificatesResource(client, PoolType.SHARES) + + certificates = resource.get_certificates(pool_account="POOL001") + + mock_get.assert_called_once_with( + "LSS.svc/Shares/Certificates", params={"pool-account": "POOL001"} + ) + assert isinstance(certificates, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_certificates_with_all_filters(self, mock_get, client): + """Test get_certificates with all filters.""" + mock_get.return_value = {"Status": 0, "Data": [{"certificate_id": 1}]} + resource = CertificatesResource(client, PoolType.SHARES) + + certificates = resource.get_certificates( + start_date="01/01/2024", + end_date="12/31/2024", + partner_account="PARTNER001", + pool_account="POOL001", + ) + + mock_get.assert_called_once_with( + "LSS.svc/Shares/Certificates", + params={ + "from-date": "01/01/2024", + "to-date": "12/31/2024", + "partner-account": "PARTNER001", + "pool-account": "POOL001", + }, + ) + assert isinstance(certificates, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_certificates_invalid_start_date(self, mock_get, client): + """Test get_certificates with invalid start_date format.""" + resource = CertificatesResource(client, PoolType.SHARES) + + with pytest.raises(ValidationError) as exc_info: + resource.get_certificates(start_date="2024-01-01") + + assert "start_date must be in MM/DD/YYYY format" in str(exc_info.value) + mock_get.assert_not_called() + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_certificates_invalid_end_date(self, mock_get, client): + """Test get_certificates with invalid end_date format.""" + resource = CertificatesResource(client, PoolType.SHARES) + + with pytest.raises(ValidationError) as exc_info: + resource.get_certificates(end_date="invalid-date") + + assert "end_date must be in MM/DD/YYYY format" in str(exc_info.value) + mock_get.assert_not_called() + + def test_validate_date_format_valid(self, client): + """Test _validate_date_format with valid date.""" + resource = CertificatesResource(client, PoolType.SHARES) + + assert resource._validate_date_format("12/31/2024") is True + assert resource._validate_date_format("01/01/2024") is True + + def test_validate_date_format_invalid(self, client): + """Test _validate_date_format with invalid dates.""" + resource = CertificatesResource(client, PoolType.SHARES) + + assert resource._validate_date_format("2024-12-31") is False + assert resource._validate_date_format("31/12/2024") is False + assert resource._validate_date_format("invalid") is False diff --git a/tests/test_resources_distributions.py b/tests/test_resources_distributions.py new file mode 100644 index 0000000..4fe26d7 --- /dev/null +++ b/tests/test_resources_distributions.py @@ -0,0 +1,149 @@ +"""Tests for DistributionsResource.""" + +from unittest.mock import patch + +import pytest + +from tmo_api.client import TheMortgageOfficeClient +from tmo_api.exceptions import ValidationError +from tmo_api.resources.distributions import DistributionsResource +from tmo_api.resources.pools import PoolType + + +class TestDistributionsResource: + """Test DistributionsResource functionality.""" + + @pytest.fixture + def client(self, mock_token, mock_database): + """Create a test client.""" + return TheMortgageOfficeClient(token=mock_token, database=mock_database) + + def test_distributions_resource_init_shares(self, client): + """Test DistributionsResource initialization with Shares type.""" + resource = DistributionsResource(client, PoolType.SHARES) + assert resource.client == client + assert resource.pool_type == PoolType.SHARES + assert resource.base_path == "LSS.svc/Shares" + + def test_distributions_resource_init_capital(self, client): + """Test DistributionsResource initialization with Capital type.""" + resource = DistributionsResource(client, PoolType.CAPITAL) + assert resource.pool_type == PoolType.CAPITAL + assert resource.base_path == "LSS.svc/Capital" + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_distribution_success(self, mock_get, client, mock_api_response_success): + """Test successful get_distribution call.""" + mock_get.return_value = mock_api_response_success + resource = DistributionsResource(client, PoolType.SHARES) + + distribution = resource.get_distribution("12345") + + mock_get.assert_called_once_with("LSS.svc/Shares/Distributions/12345") + assert distribution is not None + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_distribution_empty_rec_id(self, mock_get, client): + """Test get_distribution with empty rec_id raises ValidationError.""" + resource = DistributionsResource(client, PoolType.SHARES) + + with pytest.raises(ValidationError) as exc_info: + resource.get_distribution("") + + assert "RecID parameter is required" in str(exc_info.value) + mock_get.assert_not_called() + + @patch.object(TheMortgageOfficeClient, "get") + def test_list_all_no_filters(self, mock_get, client): + """Test list_all without filters.""" + mock_get.return_value = {"Status": 0, "Data": [{"distribution_id": 1}]} + resource = DistributionsResource(client, PoolType.SHARES) + + distributions = resource.list_all() + + mock_get.assert_called_once_with("LSS.svc/Shares/Distributions", params=None) + assert isinstance(distributions, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_list_all_with_date_filters(self, mock_get, client): + """Test list_all with date filters.""" + mock_get.return_value = {"Status": 0, "Data": [{"distribution_id": 1}]} + resource = DistributionsResource(client, PoolType.SHARES) + + distributions = resource.list_all(start_date="01/01/2024", end_date="12/31/2024") + + mock_get.assert_called_once_with( + "LSS.svc/Shares/Distributions", + params={"from-date": "01/01/2024", "to-date": "12/31/2024"}, + ) + assert isinstance(distributions, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_list_all_with_pool_account(self, mock_get, client): + """Test list_all with pool_account filter.""" + mock_get.return_value = {"Status": 0, "Data": [{"distribution_id": 1}]} + resource = DistributionsResource(client, PoolType.SHARES) + + distributions = resource.list_all(pool_account="POOL001") + + mock_get.assert_called_once_with( + "LSS.svc/Shares/Distributions", params={"pool-account": "POOL001"} + ) + assert isinstance(distributions, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_list_all_with_all_filters(self, mock_get, client): + """Test list_all with all filters.""" + mock_get.return_value = {"Status": 0, "Data": [{"distribution_id": 1}]} + resource = DistributionsResource(client, PoolType.SHARES) + + distributions = resource.list_all( + start_date="01/01/2024", end_date="12/31/2024", pool_account="POOL001" + ) + + mock_get.assert_called_once_with( + "LSS.svc/Shares/Distributions", + params={ + "from-date": "01/01/2024", + "to-date": "12/31/2024", + "pool-account": "POOL001", + }, + ) + assert isinstance(distributions, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_list_all_invalid_start_date(self, mock_get, client): + """Test list_all with invalid start_date format.""" + resource = DistributionsResource(client, PoolType.SHARES) + + with pytest.raises(ValidationError) as exc_info: + resource.list_all(start_date="2024-01-01") + + assert "start_date must be in MM/DD/YYYY format" in str(exc_info.value) + mock_get.assert_not_called() + + @patch.object(TheMortgageOfficeClient, "get") + def test_list_all_invalid_end_date(self, mock_get, client): + """Test list_all with invalid end_date format.""" + resource = DistributionsResource(client, PoolType.SHARES) + + with pytest.raises(ValidationError) as exc_info: + resource.list_all(end_date="invalid-date") + + assert "end_date must be in MM/DD/YYYY format" in str(exc_info.value) + mock_get.assert_not_called() + + def test_validate_date_format_valid(self, client): + """Test _validate_date_format with valid date.""" + resource = DistributionsResource(client, PoolType.SHARES) + + assert resource._validate_date_format("12/31/2024") is True + assert resource._validate_date_format("01/01/2024") is True + + def test_validate_date_format_invalid(self, client): + """Test _validate_date_format with invalid dates.""" + resource = DistributionsResource(client, PoolType.SHARES) + + assert resource._validate_date_format("2024-12-31") is False + assert resource._validate_date_format("31/12/2024") is False + assert resource._validate_date_format("invalid") is False diff --git a/tests/test_resources_history.py b/tests/test_resources_history.py new file mode 100644 index 0000000..742df07 --- /dev/null +++ b/tests/test_resources_history.py @@ -0,0 +1,144 @@ +"""Tests for HistoryResource.""" + +from unittest.mock import patch + +import pytest + +from tmo_api.client import TheMortgageOfficeClient +from tmo_api.exceptions import ValidationError +from tmo_api.resources.history import HistoryResource +from tmo_api.resources.pools import PoolType + + +class TestHistoryResource: + """Test HistoryResource functionality.""" + + @pytest.fixture + def client(self, mock_token, mock_database): + """Create a test client.""" + return TheMortgageOfficeClient(token=mock_token, database=mock_database) + + def test_history_resource_init_shares(self, client): + """Test HistoryResource initialization with Shares type.""" + resource = HistoryResource(client, PoolType.SHARES) + assert resource.client == client + assert resource.pool_type == PoolType.SHARES + assert resource.base_path == "LSS.svc/Shares" + + def test_history_resource_init_capital(self, client): + """Test HistoryResource initialization with Capital type.""" + resource = HistoryResource(client, PoolType.CAPITAL) + assert resource.pool_type == PoolType.CAPITAL + assert resource.base_path == "LSS.svc/Capital" + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_history_no_filters(self, mock_get, client): + """Test get_history without filters.""" + mock_get.return_value = {"Status": 0, "Data": [{"history_id": 1}]} + resource = HistoryResource(client, PoolType.SHARES) + + history = resource.get_history() + + mock_get.assert_called_once_with("LSS.svc/Shares/History", params=None) + assert isinstance(history, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_history_with_date_filters(self, mock_get, client): + """Test get_history with date filters.""" + mock_get.return_value = {"Status": 0, "Data": [{"history_id": 1}]} + resource = HistoryResource(client, PoolType.SHARES) + + history = resource.get_history(start_date="01/01/2024", end_date="12/31/2024") + + mock_get.assert_called_once_with( + "LSS.svc/Shares/History", + params={"from-date": "01/01/2024", "to-date": "12/31/2024"}, + ) + assert isinstance(history, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_history_with_partner_account(self, mock_get, client): + """Test get_history with partner_account filter.""" + mock_get.return_value = {"Status": 0, "Data": [{"history_id": 1}]} + resource = HistoryResource(client, PoolType.SHARES) + + history = resource.get_history(partner_account="PARTNER001") + + mock_get.assert_called_once_with( + "LSS.svc/Shares/History", params={"partner-account": "PARTNER001"} + ) + assert isinstance(history, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_history_with_pool_account(self, mock_get, client): + """Test get_history with pool_account filter.""" + mock_get.return_value = {"Status": 0, "Data": [{"history_id": 1}]} + resource = HistoryResource(client, PoolType.SHARES) + + history = resource.get_history(pool_account="POOL001") + + mock_get.assert_called_once_with( + "LSS.svc/Shares/History", params={"pool-account": "POOL001"} + ) + assert isinstance(history, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_history_with_all_filters(self, mock_get, client): + """Test get_history with all filters.""" + mock_get.return_value = {"Status": 0, "Data": [{"history_id": 1}]} + resource = HistoryResource(client, PoolType.SHARES) + + history = resource.get_history( + start_date="01/01/2024", + end_date="12/31/2024", + partner_account="PARTNER001", + pool_account="POOL001", + ) + + mock_get.assert_called_once_with( + "LSS.svc/Shares/History", + params={ + "from-date": "01/01/2024", + "to-date": "12/31/2024", + "partner-account": "PARTNER001", + "pool-account": "POOL001", + }, + ) + assert isinstance(history, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_history_invalid_start_date(self, mock_get, client): + """Test get_history with invalid start_date format.""" + resource = HistoryResource(client, PoolType.SHARES) + + with pytest.raises(ValidationError) as exc_info: + resource.get_history(start_date="2024-01-01") + + assert "start_date must be in MM/DD/YYYY format" in str(exc_info.value) + mock_get.assert_not_called() + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_history_invalid_end_date(self, mock_get, client): + """Test get_history with invalid end_date format.""" + resource = HistoryResource(client, PoolType.SHARES) + + with pytest.raises(ValidationError) as exc_info: + resource.get_history(end_date="invalid-date") + + assert "end_date must be in MM/DD/YYYY format" in str(exc_info.value) + mock_get.assert_not_called() + + def test_validate_date_format_valid(self, client): + """Test _validate_date_format with valid date.""" + resource = HistoryResource(client, PoolType.SHARES) + + assert resource._validate_date_format("12/31/2024") is True + assert resource._validate_date_format("01/01/2024") is True + + def test_validate_date_format_invalid(self, client): + """Test _validate_date_format with invalid dates.""" + resource = HistoryResource(client, PoolType.SHARES) + + assert resource._validate_date_format("2024-12-31") is False + assert resource._validate_date_format("31/12/2024") is False + assert resource._validate_date_format("invalid") is False diff --git a/tests/test_resources_partners.py b/tests/test_resources_partners.py new file mode 100644 index 0000000..a13d745 --- /dev/null +++ b/tests/test_resources_partners.py @@ -0,0 +1,138 @@ +"""Tests for PartnersResource.""" + +from unittest.mock import patch + +import pytest + +from tmo_api.client import TheMortgageOfficeClient +from tmo_api.exceptions import ValidationError +from tmo_api.resources.partners import PartnersResource +from tmo_api.resources.pools import PoolType + + +class TestPartnersResource: + """Test PartnersResource functionality.""" + + @pytest.fixture + def client(self, mock_token, mock_database): + """Create a test client.""" + return TheMortgageOfficeClient(token=mock_token, database=mock_database) + + def test_partners_resource_init_shares(self, client): + """Test PartnersResource initialization with Shares type.""" + resource = PartnersResource(client, PoolType.SHARES) + assert resource.client == client + assert resource.pool_type == PoolType.SHARES + assert resource.base_path == "LSS.svc/Shares" + + def test_partners_resource_init_capital(self, client): + """Test PartnersResource initialization with Capital type.""" + resource = PartnersResource(client, PoolType.CAPITAL) + assert resource.pool_type == PoolType.CAPITAL + assert resource.base_path == "LSS.svc/Capital" + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_partner_success(self, mock_get, client, mock_api_response_success): + """Test successful get_partner call.""" + mock_get.return_value = mock_api_response_success + resource = PartnersResource(client, PoolType.SHARES) + + partner = resource.get_partner("PARTNER001") + + mock_get.assert_called_once_with("LSS.svc/Shares/Partners/PARTNER001") + assert partner is not None + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_partner_empty_account(self, mock_get, client): + """Test get_partner with empty account raises ValidationError.""" + resource = PartnersResource(client, PoolType.SHARES) + + with pytest.raises(ValidationError) as exc_info: + resource.get_partner("") + + assert "Account parameter is required" in str(exc_info.value) + mock_get.assert_not_called() + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_partner_attachments(self, mock_get, client): + """Test get_partner_attachments.""" + mock_get.return_value = {"Status": 0, "Data": [{"attachment_id": 1}]} + resource = PartnersResource(client, PoolType.SHARES) + + attachments = resource.get_partner_attachments("PARTNER001") + + mock_get.assert_called_once_with("LSS.svc/Shares/Partners/PARTNER001/Attachments") + assert isinstance(attachments, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_get_partner_attachments_empty_account(self, mock_get, client): + """Test get_partner_attachments with empty account raises ValidationError.""" + resource = PartnersResource(client, PoolType.SHARES) + + with pytest.raises(ValidationError) as exc_info: + resource.get_partner_attachments("") + + assert "Account parameter is required" in str(exc_info.value) + mock_get.assert_not_called() + + @patch.object(TheMortgageOfficeClient, "get") + def test_list_all_no_filters(self, mock_get, client): + """Test list_all without filters.""" + mock_get.return_value = {"Status": 0, "Data": [{"partner_id": 1}]} + resource = PartnersResource(client, PoolType.SHARES) + + partners = resource.list_all() + + mock_get.assert_called_once_with("LSS.svc/Shares/Partners", params=None) + assert isinstance(partners, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_list_all_with_date_filters(self, mock_get, client): + """Test list_all with date filters.""" + mock_get.return_value = {"Status": 0, "Data": [{"partner_id": 1}]} + resource = PartnersResource(client, PoolType.SHARES) + + partners = resource.list_all(start_date="01/01/2024", end_date="12/31/2024") + + mock_get.assert_called_once_with( + "LSS.svc/Shares/Partners", + params={"from-date": "01/01/2024", "to-date": "12/31/2024"}, + ) + assert isinstance(partners, list) + + @patch.object(TheMortgageOfficeClient, "get") + def test_list_all_invalid_start_date(self, mock_get, client): + """Test list_all with invalid start_date format.""" + resource = PartnersResource(client, PoolType.SHARES) + + with pytest.raises(ValidationError) as exc_info: + resource.list_all(start_date="2024-01-01") + + assert "start_date must be in MM/DD/YYYY format" in str(exc_info.value) + mock_get.assert_not_called() + + @patch.object(TheMortgageOfficeClient, "get") + def test_list_all_invalid_end_date(self, mock_get, client): + """Test list_all with invalid end_date format.""" + resource = PartnersResource(client, PoolType.SHARES) + + with pytest.raises(ValidationError) as exc_info: + resource.list_all(end_date="invalid-date") + + assert "end_date must be in MM/DD/YYYY format" in str(exc_info.value) + mock_get.assert_not_called() + + def test_validate_date_format_valid(self, client): + """Test _validate_date_format with valid date.""" + resource = PartnersResource(client, PoolType.SHARES) + + assert resource._validate_date_format("12/31/2024") is True + assert resource._validate_date_format("01/01/2024") is True + + def test_validate_date_format_invalid(self, client): + """Test _validate_date_format with invalid dates.""" + resource = PartnersResource(client, PoolType.SHARES) + + assert resource._validate_date_format("2024-12-31") is False + assert resource._validate_date_format("31/12/2024") is False + assert resource._validate_date_format("invalid") is False