From 0c5de0c2da8e5396619389418b78cdd41319d002 Mon Sep 17 00:00:00 2001 From: moon <152454724+pabloDarkmoon24@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:38:14 -0500 Subject: [PATCH 1/2] fix: solution for issue #75 --- fix_issue_75.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fix_issue_75.py diff --git a/fix_issue_75.py b/fix_issue_75.py new file mode 100644 index 00000000..421c19b8 --- /dev/null +++ b/fix_issue_75.py @@ -0,0 +1,3 @@ +```json +{ + "solution_code": "### File: app/connectors/__init__.py\n```python\n# Bank Sync Connector Architecture\nfrom .base import BankConnector, ConnectorError, AccountInfo, Transaction\nfrom .mock import MockBankConnector\nfrom .registry import ConnectorRegistry\n\n__all__ = [\n \"BankConnector\",\n \"ConnectorError\",\n \"AccountInfo\",\n \"Transaction\",\n \"MockBankConnector\",\n \"ConnectorRegistry\",\n]\n```\n\n### File: app/connectors/base.py\n```python\n\"\"\"Abstract base class for bank sync connectors.\"\"\"\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass, field\nfrom datetime import date\nfrom decimal import Decimal\nfrom typing import List, Optional\n\n\nclass ConnectorError(Exception):\n \"\"\"Raised when a connector operation fails.\"\"\"\n\n def __init__(self, message: str, code: Optional[str] = None):\n super().__init__(message)\n self.code = code\n\n\n@dataclass\nclass AccountInfo:\n \"\"\"Represents a bank account returned by a connector.\"\"\"\n account_id: str\n name: str\n type: str # e.g. 'checking', 'savings', 'credit'\n currency: str # ISO 4217, e.g. 'USD'\n balance: Decimal\n institution_name: str\n mask: Optional[str] = None # last 4 digits\n subtype: Optional[str] = None\n\n\n@dataclass\nclass Transaction:\n \"\"\"Represents a single bank transaction returned by a connector.\"\"\"\n transaction_id: str\n account_id: str\n date: date\n amount: Decimal # negative = debit, positive = credit\n description: str\n category: Optional[str] = None\n pending: bool = False\n merchant_name: Optional[str] = None\n metadata: dict = field(default_factory=dict)\n\n\nclass BankConnector(ABC):\n \"\"\"\n Abstract connector interface that every bank integration must implement.\n\n Lifecycle\n ---------\n 1. Instantiate with user-scoped credentials / config.\n 2. Call ``import_accounts()`` to obtain the list of linked accounts.\n 3. Call ``import_transactions()`` for an initial bulk pull.\n 4. Periodically call ``refresh_transactions()`` for incremental updates.\n 5. Call ``disconnect()`` to revoke tokens and clean up.\n \"\"\"\n\n # Subclasses should override this with a short slug, e.g. 'plaid', 'mock'.\n connector_id: str = \"base\"\n\n def __init__(self, config: dict):\n \"\"\"\n Parameters\n ----------\n config:\n Connector-specific configuration (API keys, access tokens, etc.).\n The base class stores it but does not validate it – each subclass\n is responsible for validation inside ``_validate_config``.\n \"\"\"\n self.config = config\n self._validate_config(config)\n\n # ------------------------------------------------------------------\n # Abstract interface – every connector MUST implement these\n # ------------------------------------------------------------------\n\n @abstractmethod\n def _validate_config(self, config: dict) -> None:\n \"\"\"Raise ``ConnectorError`` if required keys are missing.\"\"\"\n\n @abstractmethod\n def import_accounts(self) -> List[AccountInfo]:\n \"\"\"\n Return all accounts available under the linked credentials.\n\n Raises\n ------\n ConnectorError\n On authentication failure or upstream error.\n \"\"\"\n\n @abstractmethod\n def import_transactions(\n self,\n account_id: str,\n start_date: date,\n end_date: date,\n ) -> List[Transaction]:\n \"\"\"\n Bulk-import transactions for *account_id* between *start_date* and\n *end_date* (both inclusive).\n\n Raises\n ------\n ConnectorError\n On authentication failure, bad account ID, or upstream error.\n \"\"\"\n\n @abstractmethod\n def refresh_transactions(\n self,\n account_id: str,\n cursor: Optional[str] = None,\n ) -> tuple[List[Transaction], str]:\n \"\"\"\n Incrementally fetch new/updated transactions since *cursor*.\n\n Returns\n -------\n (transactions, next_cursor)\n ``next_cursor`` should be persisted and passed on the next call.\n\n Raises\n ------\n ConnectorError\n On authentication failure or upstream error.\n \"\"\"\n\n @abstractmethod\n def disconnect(self) -> None:\n \"\"\"\n Revoke any stored tokens / webhooks and clean up resources.\n\n Raises\n ------\n ConnectorError\n If the remote revocation call fails.\n \"\"\"\n\n # ------------------------------------------------------------------\n # Optional hook – subclasses may override\n # ------------------------------------------------------------------\n\n def health_check(self) -> bool:\n \"\"\"\n Lightweight liveness check. Returns ``True`` if the connector can\n reach the upstream service, ``False`` otherwise. Default\n implementation always returns ``True``; override as needed.\n \"\"\"\n return True\n\n def __repr__(self) -> str:\n return f\"<{self.__class__.__name__} connector_id={self.connector_id!r}>\"\n```\n\n### File: app/connectors/mock.py\n```python\n\"\"\"Mock bank connector for development, testing, and demos.\"\"\"\nimport random\nimport uuid\nfrom datetime import date, timedelta\nfrom decimal import Decimal\nfrom typing import List, Optional\n\nfrom .base import AccountInfo, BankConnector, ConnectorError, Transaction\n\n_MOCK_ACCOUNTS = [\n AccountInfo(\n account_id=\"mock-checking-001\",\n name=\"FinMind Checking\",\n type=\"depository\",\n subtype=\"checking\",\n currency=\"USD\",\n balance=Decimal(\"2450.75\"),\n institution_name=\"FinMind Test Bank\",\n mask=\"0001\",\n ),\n AccountInfo(\n account_id=\"mock-savings-001\",\n name=\"FinMind Savings\",\n type=\"depository\",\n subtype=\"savings\",\n currency=\"USD\",\n balance=Decimal(\"8120.00\"),\n institution_name=\"FinMind Test Bank\",\n mask=\"0002\",\n ),\n AccountInfo(\n account_id=\"mock-credit-001\",\n name=\"FinMind Visa\",\n type=\"credit\",\n subtype=\"credit card\",\n currency=\"USD\",\n balance=Decimal(\"-342.90\"),\n institution_name=\"FinMind Test Bank\",\n mask=\"0003\",\n ),\n]\n\n_MOCK_MERCHANTS = [\n (\"Whole Foods\", \"Food and Drink\"),\n (\"Netflix\", \"Entertainment\"),\n (\"Shell Gas Station\", \"Transportation\"),\n (\"Amazon\", \"Shopping\"),\n (\"Starbucks\", \"Food and Drink\"),\n (\"Gym Membership\", \"Health\"),\n (\"Electric Bill\", \"Utilities\"),\n (\"Spotify\", \"Entertainment\"),\n (\"Uber\", \"Transportation\"),\n (\"Direct Deposit\", \"Income\"),\n]\n\n\nclass MockBankConnector(BankConnector):\n \"\"\"\n In-memory mock connector.\n\n Config keys\n -----------\n simulate_error : bool (default False)\n When ``True`` every method raises ``ConnectorError`` so error-path\n code can be exercised.\n seed : int (optional)\n Random seed for reproducible transaction generation.\n \"\"\"\n\n connector_id = \"mock\"\n\n def _validate_config(self, config: dict) -> None:\n # No mandatory keys for the mock connector\n pass\n\n # ------------------------------------------------------------------\n # Internal helpers\n # ------------------------------------------------------------------\n\n def _maybe_raise(self) -> None:\n if self.config.get(\"simulate_error\", False):\n raise ConnectorError(\n \"Simulated connector error\",\n code=\"MOCK_ERROR\",\n )\n\n def _account_exists(self, account_id: str) -> AccountInfo:\n for acc in _MOCK_ACCOUNTS:\n if acc.account_id == account_id:\n return acc\n raise ConnectorError(\n f\"Account '{account_id}' not found in mock connector.\",\n code=\"ACCOUNT_NOT_FOUND\",\n )\n\n def _generate_transactions(\n self,\n account_id: str,\n start_date: date,\n end_date: date,\n rng: random.Random,\n ) -> List[Transaction]:\n txns: List[Transaction] = []\n current = start_date\n while current <= end_date:\n # Randomly emit 0-3 transactions per day\n for _ in range(rng.randint(0, 3)):\n merchant, category = rng.choice(_MOCK_MERCHANTS)\n amount = Decimal(str(round(rng.uniform(-150.0, 3000.0), 2)))\n txns.append(\n Transaction(\n transaction_id=str(uuid.uuid4()),\n account_id=account_id,\n date=current,\n amount=amount,\n description=merchant,\n category=category,\n merchant_name=merchant,\n pending=(current == end_date and rng.random() > 0.7),\n )\n )\n current += timedelta(days=1)\n return txns\n\n # ------------------------------------------------------------------\n # BankConnector interface\n # ------------------------------------------------------------------\n\n def import_accounts(self) -> List[AccountInfo]:\n self._maybe_raise()\n return list(_MOCK_ACCOUNTS)\n\n def import_transactions(\n self,\n account_id: str,\n start_date: date,\n end_date: date,\n ) -> List[Transaction]:\n self._maybe_raise()\n self._account_exists(account_id)\n seed = self.config.get(\"seed\", 42)\n rng = random.Random(seed)\n return self._generate_transactions(account_id, start_date, end_date, rng)\n\n def refresh_transactions(\n self,\n account_id: str,\n cursor: Optional[str] = None,\n ) -> tuple[List[Transaction], str]:\n self._maybe_raise()\n self._account_exists(account_id)\n seed = self.config.get(\"seed\", 99)\n rng = random.Random(int(cursor or seed))\n today = date.today()\n txns = self._generate_transactions(\n account_ \ No newline at end of file From 2fdde46b54f2054e9fcb3c2ae6d6c6fa6efe4a4c Mon Sep 17 00:00:00 2001 From: moon <152454724+pabloDarkmoon24@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:25:44 -0500 Subject: [PATCH 2/2] fix: solution for issue #75 --- fix_issue_75.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fix_issue_75.py b/fix_issue_75.py index 421c19b8..41a87ce9 100644 --- a/fix_issue_75.py +++ b/fix_issue_75.py @@ -1,3 +1,3 @@ ```json { - "solution_code": "### File: app/connectors/__init__.py\n```python\n# Bank Sync Connector Architecture\nfrom .base import BankConnector, ConnectorError, AccountInfo, Transaction\nfrom .mock import MockBankConnector\nfrom .registry import ConnectorRegistry\n\n__all__ = [\n \"BankConnector\",\n \"ConnectorError\",\n \"AccountInfo\",\n \"Transaction\",\n \"MockBankConnector\",\n \"ConnectorRegistry\",\n]\n```\n\n### File: app/connectors/base.py\n```python\n\"\"\"Abstract base class for bank sync connectors.\"\"\"\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass, field\nfrom datetime import date\nfrom decimal import Decimal\nfrom typing import List, Optional\n\n\nclass ConnectorError(Exception):\n \"\"\"Raised when a connector operation fails.\"\"\"\n\n def __init__(self, message: str, code: Optional[str] = None):\n super().__init__(message)\n self.code = code\n\n\n@dataclass\nclass AccountInfo:\n \"\"\"Represents a bank account returned by a connector.\"\"\"\n account_id: str\n name: str\n type: str # e.g. 'checking', 'savings', 'credit'\n currency: str # ISO 4217, e.g. 'USD'\n balance: Decimal\n institution_name: str\n mask: Optional[str] = None # last 4 digits\n subtype: Optional[str] = None\n\n\n@dataclass\nclass Transaction:\n \"\"\"Represents a single bank transaction returned by a connector.\"\"\"\n transaction_id: str\n account_id: str\n date: date\n amount: Decimal # negative = debit, positive = credit\n description: str\n category: Optional[str] = None\n pending: bool = False\n merchant_name: Optional[str] = None\n metadata: dict = field(default_factory=dict)\n\n\nclass BankConnector(ABC):\n \"\"\"\n Abstract connector interface that every bank integration must implement.\n\n Lifecycle\n ---------\n 1. Instantiate with user-scoped credentials / config.\n 2. Call ``import_accounts()`` to obtain the list of linked accounts.\n 3. Call ``import_transactions()`` for an initial bulk pull.\n 4. Periodically call ``refresh_transactions()`` for incremental updates.\n 5. Call ``disconnect()`` to revoke tokens and clean up.\n \"\"\"\n\n # Subclasses should override this with a short slug, e.g. 'plaid', 'mock'.\n connector_id: str = \"base\"\n\n def __init__(self, config: dict):\n \"\"\"\n Parameters\n ----------\n config:\n Connector-specific configuration (API keys, access tokens, etc.).\n The base class stores it but does not validate it – each subclass\n is responsible for validation inside ``_validate_config``.\n \"\"\"\n self.config = config\n self._validate_config(config)\n\n # ------------------------------------------------------------------\n # Abstract interface – every connector MUST implement these\n # ------------------------------------------------------------------\n\n @abstractmethod\n def _validate_config(self, config: dict) -> None:\n \"\"\"Raise ``ConnectorError`` if required keys are missing.\"\"\"\n\n @abstractmethod\n def import_accounts(self) -> List[AccountInfo]:\n \"\"\"\n Return all accounts available under the linked credentials.\n\n Raises\n ------\n ConnectorError\n On authentication failure or upstream error.\n \"\"\"\n\n @abstractmethod\n def import_transactions(\n self,\n account_id: str,\n start_date: date,\n end_date: date,\n ) -> List[Transaction]:\n \"\"\"\n Bulk-import transactions for *account_id* between *start_date* and\n *end_date* (both inclusive).\n\n Raises\n ------\n ConnectorError\n On authentication failure, bad account ID, or upstream error.\n \"\"\"\n\n @abstractmethod\n def refresh_transactions(\n self,\n account_id: str,\n cursor: Optional[str] = None,\n ) -> tuple[List[Transaction], str]:\n \"\"\"\n Incrementally fetch new/updated transactions since *cursor*.\n\n Returns\n -------\n (transactions, next_cursor)\n ``next_cursor`` should be persisted and passed on the next call.\n\n Raises\n ------\n ConnectorError\n On authentication failure or upstream error.\n \"\"\"\n\n @abstractmethod\n def disconnect(self) -> None:\n \"\"\"\n Revoke any stored tokens / webhooks and clean up resources.\n\n Raises\n ------\n ConnectorError\n If the remote revocation call fails.\n \"\"\"\n\n # ------------------------------------------------------------------\n # Optional hook – subclasses may override\n # ------------------------------------------------------------------\n\n def health_check(self) -> bool:\n \"\"\"\n Lightweight liveness check. Returns ``True`` if the connector can\n reach the upstream service, ``False`` otherwise. Default\n implementation always returns ``True``; override as needed.\n \"\"\"\n return True\n\n def __repr__(self) -> str:\n return f\"<{self.__class__.__name__} connector_id={self.connector_id!r}>\"\n```\n\n### File: app/connectors/mock.py\n```python\n\"\"\"Mock bank connector for development, testing, and demos.\"\"\"\nimport random\nimport uuid\nfrom datetime import date, timedelta\nfrom decimal import Decimal\nfrom typing import List, Optional\n\nfrom .base import AccountInfo, BankConnector, ConnectorError, Transaction\n\n_MOCK_ACCOUNTS = [\n AccountInfo(\n account_id=\"mock-checking-001\",\n name=\"FinMind Checking\",\n type=\"depository\",\n subtype=\"checking\",\n currency=\"USD\",\n balance=Decimal(\"2450.75\"),\n institution_name=\"FinMind Test Bank\",\n mask=\"0001\",\n ),\n AccountInfo(\n account_id=\"mock-savings-001\",\n name=\"FinMind Savings\",\n type=\"depository\",\n subtype=\"savings\",\n currency=\"USD\",\n balance=Decimal(\"8120.00\"),\n institution_name=\"FinMind Test Bank\",\n mask=\"0002\",\n ),\n AccountInfo(\n account_id=\"mock-credit-001\",\n name=\"FinMind Visa\",\n type=\"credit\",\n subtype=\"credit card\",\n currency=\"USD\",\n balance=Decimal(\"-342.90\"),\n institution_name=\"FinMind Test Bank\",\n mask=\"0003\",\n ),\n]\n\n_MOCK_MERCHANTS = [\n (\"Whole Foods\", \"Food and Drink\"),\n (\"Netflix\", \"Entertainment\"),\n (\"Shell Gas Station\", \"Transportation\"),\n (\"Amazon\", \"Shopping\"),\n (\"Starbucks\", \"Food and Drink\"),\n (\"Gym Membership\", \"Health\"),\n (\"Electric Bill\", \"Utilities\"),\n (\"Spotify\", \"Entertainment\"),\n (\"Uber\", \"Transportation\"),\n (\"Direct Deposit\", \"Income\"),\n]\n\n\nclass MockBankConnector(BankConnector):\n \"\"\"\n In-memory mock connector.\n\n Config keys\n -----------\n simulate_error : bool (default False)\n When ``True`` every method raises ``ConnectorError`` so error-path\n code can be exercised.\n seed : int (optional)\n Random seed for reproducible transaction generation.\n \"\"\"\n\n connector_id = \"mock\"\n\n def _validate_config(self, config: dict) -> None:\n # No mandatory keys for the mock connector\n pass\n\n # ------------------------------------------------------------------\n # Internal helpers\n # ------------------------------------------------------------------\n\n def _maybe_raise(self) -> None:\n if self.config.get(\"simulate_error\", False):\n raise ConnectorError(\n \"Simulated connector error\",\n code=\"MOCK_ERROR\",\n )\n\n def _account_exists(self, account_id: str) -> AccountInfo:\n for acc in _MOCK_ACCOUNTS:\n if acc.account_id == account_id:\n return acc\n raise ConnectorError(\n f\"Account '{account_id}' not found in mock connector.\",\n code=\"ACCOUNT_NOT_FOUND\",\n )\n\n def _generate_transactions(\n self,\n account_id: str,\n start_date: date,\n end_date: date,\n rng: random.Random,\n ) -> List[Transaction]:\n txns: List[Transaction] = []\n current = start_date\n while current <= end_date:\n # Randomly emit 0-3 transactions per day\n for _ in range(rng.randint(0, 3)):\n merchant, category = rng.choice(_MOCK_MERCHANTS)\n amount = Decimal(str(round(rng.uniform(-150.0, 3000.0), 2)))\n txns.append(\n Transaction(\n transaction_id=str(uuid.uuid4()),\n account_id=account_id,\n date=current,\n amount=amount,\n description=merchant,\n category=category,\n merchant_name=merchant,\n pending=(current == end_date and rng.random() > 0.7),\n )\n )\n current += timedelta(days=1)\n return txns\n\n # ------------------------------------------------------------------\n # BankConnector interface\n # ------------------------------------------------------------------\n\n def import_accounts(self) -> List[AccountInfo]:\n self._maybe_raise()\n return list(_MOCK_ACCOUNTS)\n\n def import_transactions(\n self,\n account_id: str,\n start_date: date,\n end_date: date,\n ) -> List[Transaction]:\n self._maybe_raise()\n self._account_exists(account_id)\n seed = self.config.get(\"seed\", 42)\n rng = random.Random(seed)\n return self._generate_transactions(account_id, start_date, end_date, rng)\n\n def refresh_transactions(\n self,\n account_id: str,\n cursor: Optional[str] = None,\n ) -> tuple[List[Transaction], str]:\n self._maybe_raise()\n self._account_exists(account_id)\n seed = self.config.get(\"seed\", 99)\n rng = random.Random(int(cursor or seed))\n today = date.today()\n txns = self._generate_transactions(\n account_ \ No newline at end of file + "solution_code": "### File: app/connectors/__init__.py\n```python\n\"\"\"\nBank Sync Connector Architecture\nPluggable connector system for bank integrations.\n\"\"\"\nfrom .base import BaseConnector, ConnectorError, ImportResult, RefreshResult\nfrom .mock import MockBankConnector\nfrom .registry import ConnectorRegistry, registry\n\n__all__ = [\n \"BaseConnector\",\n \"ConnectorError\",\n \"ImportResult\",\n \"RefreshResult\",\n \"MockBankConnector\",\n \"ConnectorRegistry\",\n \"registry\",\n]\n```\n\n### File: app/connectors/base.py\n```python\n\"\"\"\nAbstract base class for all bank sync connectors.\nAll real and mock connectors must implement this interface.\n\"\"\"\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass, field\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional\n\n\nclass ConnectorError(Exception):\n \"\"\"Raised when a connector operation fails.\"\"\"\n\n def __init__(self, message: str, code: Optional[str] = None):\n super().__init__(message)\n self.code = code\n\n\n@dataclass\nclass RawTransaction:\n \"\"\"Normalised representation of a bank transaction.\"\"\"\n\n external_id: str # Unique ID from the bank\n amount: float # Positive = credit, negative = debit\n currency: str\n description: str\n date: datetime\n category_hint: Optional[str] = None # Bank-supplied category (optional)\n metadata: Dict[str, Any] = field(default_factory=dict)\n\n\n@dataclass\nclass ImportResult:\n \"\"\"Result returned from import_transactions().\"\"\"\n\n transactions: List[RawTransaction] = field(default_factory=list)\n has_more: bool = False # Pagination flag\n next_cursor: Optional[str] = None # Opaque cursor for the next page\n errors: List[str] = field(default_factory=list)\n\n\n@dataclass\nclass RefreshResult:\n \"\"\"Result returned from refresh_transactions().\"\"\"\n\n new_transactions: List[RawTransaction] = field(default_factory=list)\n updated_transactions: List[RawTransaction] = field(default_factory=list)\n removed_external_ids: List[str] = field(default_factory=list) # Reversed / deleted\n errors: List[str] = field(default_factory=list)\n\n\nclass BaseConnector(ABC):\n \"\"\"\n Abstract base class for all bank sync connectors.\n\n Implementors must provide:\n - ``import_transactions`` — full historical import (supports pagination)\n - ``refresh_transactions`` — incremental update since last sync\n\n Optional hooks:\n - ``authenticate`` — called once before any data fetch\n - ``validate_credentials`` — lightweight credential check\n - ``disconnect`` — cleanup / token revocation\n \"\"\"\n\n # ------------------------------------------------------------------ #\n # Connector metadata — override in subclasses\n # ------------------------------------------------------------------ #\n CONNECTOR_ID: str = \"base\" # Unique slug, e.g. \"plaid\", \"mock\"\n DISPLAY_NAME: str = \"Base\" # Human-readable name\n SUPPORTS_REFRESH: bool = True # Set False if full re-import is needed\n SUPPORTS_PAGINATION: bool = False\n\n def __init__(self, config: Dict[str, Any]):\n \"\"\"\n :param config: Connector-specific configuration dict.\n May include API keys, institution IDs, etc.\n \"\"\"\n self.config = config\n self._authenticated = False\n\n # ------------------------------------------------------------------ #\n # Required interface\n # ------------------------------------------------------------------ #\n\n @abstractmethod\n def import_transactions(\n self,\n account_id: str,\n start_date: datetime,\n end_date: datetime,\n cursor: Optional[str] = None,\n ) -> ImportResult:\n \"\"\"\n Perform a (potentially paginated) full import of transactions.\n\n :param account_id: Institution-level account identifier.\n :param start_date: Inclusive lower bound.\n :param end_date: Inclusive upper bound.\n :param cursor: If ``SUPPORTS_PAGINATION`` is True, pass the\n ``next_cursor`` from the previous page.\n :returns: :class:`ImportResult`\n :raises ConnectorError: On unrecoverable errors.\n \"\"\"\n\n @abstractmethod\n def refresh_transactions(\n self,\n account_id: str,\n last_synced_at: datetime,\n ) -> RefreshResult:\n \"\"\"\n Fetch only new / changed / removed transactions since *last_synced_at*.\n\n :param account_id: Institution-level account identifier.\n :param last_synced_at: Timestamp of the most recent successful sync.\n :returns: :class:`RefreshResult`\n :raises ConnectorError: On unrecoverable errors.\n \"\"\"\n\n # ------------------------------------------------------------------ #\n # Optional hooks\n # ------------------------------------------------------------------ #\n\n def authenticate(self) -> None:\n \"\"\"\n Perform any necessary authentication / token exchange.\n Called automatically before the first data request if\n ``_authenticated`` is False. Override in subclasses that need it.\n \"\"\"\n self._authenticated = True\n\n def validate_credentials(self) -> bool:\n \"\"\"\n Lightweight check that stored credentials are still valid.\n Returns True by default — override when the bank provides a\n dedicated endpoint.\n \"\"\"\n return True\n\n def disconnect(self) -> None:\n \"\"\"\n Revoke tokens / clean up resources. No-op by default.\n \"\"\"\n self._authenticated = False\n\n # ------------------------------------------------------------------ #\n # Helpers\n # ------------------------------------------------------------------ #\n\n def _ensure_authenticated(self) -> None:\n if not self._authenticated:\n self.authenticate()\n\n def __repr__(self) -> str:\n return f\"<{self.__class__.__name__} connector_id={self.CONNECTOR_ID!r}>\"\n```\n\n### File: app/connectors/mock.py\n```python\n\"\"\"\nMock bank connector — deterministic, no external calls.\nUsed for unit tests, CI, and local development.\n\"\"\"\nfrom datetime import datetime, timedelta\nfrom typing import Any, Dict, List, Optional\n\nfrom .base import (\n BaseConnector,\n ConnectorError,\n ImportResult,\n RawTransaction,\n RefreshResult,\n)\n\n# ---------------------------------------------------------------------------\n# Hardcoded fixture data\n# ---------------------------------------------------------------------------\n_MOCK_TRANSACTIONS: List[Dict[str, Any]] = [\n {\n \"external_id\": \"mock-txn-001\",\n \"amount\": -42.50,\n \"currency\": \"USD\",\n \"description\": \"WHOLE FOODS MARKET\",\n \"date_offset_days\": 0,\n \"category_hint\": \"Groceries\",\n },\n {\n \"external_id\": \"mock-txn-002\",\n \"amount\": -15.00,\n \"currency\": \"USD\",\n \"description\": \"NETFLIX.COM\",\n \"date_offset_days\": -3,\n \"category_hint\": \"Entertainment\",\n },\n {\n \"external_id\": \"mock-txn-003\",\n \"amount\": 2500.00,\n \"currency\": \"USD\",\n \"description\": \"EMPLOYER PAYROLL\",\n \"date_offset_days\": -7,\n \"category_hint\": \"Income\",\n },\n {\n \"external_id\": \"mock-txn-004\",\n \"amount\": -120.00,\n \"currency\": \"USD\",\n \"description\": \"ELECTRIC UTILITY CO\",\n \"date_offset_days\": -10,\n \"category_hint\": \"Utilities\",\n },\n {\n \"external_id\": \"mock-txn-005\",\n \"amount\": -8.99,\n \"currency\": \"USD\",\n \"description\": \"SPOTIFY PREMIUM\",\n \"date_offset_days\": -14,\n \"category_hint\": \"Entertainment\",\n },\n]\n\n\nclass MockBankConnector(BaseConnector):\n \"\"\"\n In-memory mock connector.\n\n Config keys (all optional):\n ``raise_on_import`` (bool) — force ConnectorError on import\n ``raise_on_refresh`` (bool) — force ConnectorError on refresh\n ``extra_transactions`` (list) — additional RawTransaction dicts\n to include in results\n \"\"\"\n\n CONNECTOR_ID = \"mock\"\n DISPLAY_NAME = \"Mock Bank (Test)\"\n SUPPORTS_REFRESH = True\n SUPPORTS_PAGINATION = False\n\n def __init__(self, config: Optional[Dict[str, Any]] = None):\n super().__init__(config or {})\n\n # ------------------------------------------------------------------\n\n def authenticate(self) -> None:\n \"\"\"No-op — mock never needs real auth.\"\"\"\n self._authenticated = True\n\n def validate_credentials(self) -> bool:\n return True\n\n # ------------------------------------------------------------------\n\n def import_transactions(\n self,\n account_id: str,\n start_date: datetime,\n end_date: datetime,\n cursor: Optional[str] = None,\n ) -> ImportResult:\n self._ensure_authenticated()\n\n if self.config.get(\"raise_on_import\"):\n raise ConnectorError(\n \"Mock import error (raise_on_import=True)\", code=\"MOCK_ERROR\"\n )\n\n now = datetime.utcnow()\n txns: List[RawTransaction] = []\n\n for raw in _MOCK_TRANSACTIONS:\n txn_date = now + timedelta(days=raw[\"date_offset_days\"])\n if not (start_date <= txn_date <= end_date):\n continue\n txns.append(\n RawTransaction(\n external_id=f\"{account_id}_{raw['external_id']}\",\n amount=raw[\"amount\"],\n currency=raw[\"currency\"],\n description=raw[\"description\"],\n date=txn_date,\n category_hint=raw.get(\"category_hint\"),\n metadata={\"source\": \"mock\"},\n )\n )\n\n # Support injecting extra transactions via config\n for extra in self.config.get(\"extra_transactions\", []):\n txns.append(\n RawTransaction(\n external_id=extra.get(\"external_id\", \"mock-extra\"),\n amount=extra[ \ No newline at end of file