Skip to content

Commit

Permalink
fix: Handle account sync errors when account is not properly setup. (#74
Browse files Browse the repository at this point in the history
)
  • Loading branch information
bvanelli authored Sep 14, 2024
1 parent 3913ced commit a05baca
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 9 deletions.
15 changes: 13 additions & 2 deletions actual/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from sqlmodel import MetaData, Session, create_engine, select

from actual.api import ActualServer
from actual.api.models import RemoteFileListDTO
from actual.api.models import BankSyncErrorDTO, RemoteFileListDTO
from actual.crypto import create_key_buffer, decrypt_from_meta, encrypt, make_salt
from actual.database import (
Accounts,
Expand All @@ -28,7 +28,12 @@
reflect_model,
strong_reference_session,
)
from actual.exceptions import ActualError, InvalidZipFile, UnknownFileId
from actual.exceptions import (
ActualBankSyncError,
ActualError,
InvalidZipFile,
UnknownFileId,
)
from actual.migrations import js_migration_statements
from actual.protobuf_models import HULC_Client, Message, SyncRequest
from actual.queries import (
Expand Down Expand Up @@ -425,6 +430,12 @@ def _run_bank_sync_account(self, acct: Accounts, start_date: datetime.date) -> l
new_transactions_data = self.bank_sync_transactions(
sync_method.lower(), account_id, start_date, requisition_id=requisition_id
)
if isinstance(new_transactions_data, BankSyncErrorDTO):
raise ActualBankSyncError(
new_transactions_data.data.error_type,
new_transactions_data.data.status,
new_transactions_data.data.reason,
)
new_transactions = new_transactions_data.data.transactions.all
imported_transactions = []
for transaction in new_transactions:
Expand Down
6 changes: 3 additions & 3 deletions actual/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

from actual.api.models import (
BankSyncAccountResponseDTO,
BankSyncResponseDTO,
BankSyncStatusDTO,
BankSyncTransactionResponseDTO,
BootstrapInfoDTO,
Endpoints,
GetUserFileInfoDTO,
Expand Down Expand Up @@ -284,12 +284,12 @@ def bank_sync_transactions(
account_id: str,
start_date: datetime.date,
requisition_id: str = None,
) -> BankSyncTransactionResponseDTO:
) -> BankSyncResponseDTO:
if bank_sync == "gocardless" and requisition_id is None:
raise ActualInvalidOperationError("Retrieving transactions with goCardless requires `requisition_id`")
endpoint = Endpoints.BANK_SYNC_TRANSACTIONS.value.format(bank_sync=bank_sync)
payload = {"accountId": account_id, "startDate": start_date.strftime("%Y-%m-%d")}
if requisition_id:
payload["requisitionId"] = requisition_id
response = requests.post(f"{self.api_url}/{endpoint}", headers=self.headers(), json=payload, verify=self.cert)
return BankSyncTransactionResponseDTO.model_validate(response.json())
return BankSyncResponseDTO.validate_python(response.json())
7 changes: 7 additions & 0 deletions actual/api/bank_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,10 @@ class BankSyncTransactionData(BaseModel):
# goCardless specific
iban: Optional[str] = None
institution_id: Optional[str] = Field(None, alias="institutionId")


class BankSyncErrorData(BaseModel):
error_type: str
error_code: str
status: Optional[str] = None
reason: Optional[str] = None
17 changes: 14 additions & 3 deletions actual/api/models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from __future__ import annotations

import enum
from typing import List, Optional
from typing import List, Optional, Union

from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, TypeAdapter

from actual.api.bank_sync import BankSyncAccountData, BankSyncTransactionData
from actual.api.bank_sync import (
BankSyncAccountData,
BankSyncErrorData,
BankSyncTransactionData,
)


class Endpoints(enum.Enum):
Expand Down Expand Up @@ -158,3 +162,10 @@ class BankSyncAccountResponseDTO(StatusDTO):

class BankSyncTransactionResponseDTO(StatusDTO):
data: BankSyncTransactionData


class BankSyncErrorDTO(StatusDTO):
data: BankSyncErrorData


BankSyncResponseDTO = TypeAdapter(Union[BankSyncErrorDTO, BankSyncTransactionResponseDTO])
5 changes: 5 additions & 0 deletions actual/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,8 @@ class ActualDecryptionError(ActualError):

class ActualSplitTransactionError(ActualError):
pass


class ActualBankSyncError(ActualError):
def __init__(self, error_type: str, status: str = None, reason: str = None):
self.error_type, self.status, self.reason = error_type, status, reason
24 changes: 23 additions & 1 deletion tests/test_bank_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from actual import Actual
from actual import Actual, ActualBankSyncError
from actual.database import Banks
from actual.queries import create_account
from tests.conftest import RequestsMock
Expand Down Expand Up @@ -54,6 +54,12 @@
},
}

fail_response = {
"error_type": "ACCOUNT_NEEDS_ATTENTION",
"error_code": "ACCOUNT_NEEDS_ATTENTION",
"reason": "The account needs your attention.",
}


def create_accounts(session, protocol: str):
bank = create_account(session, "Bank")
Expand Down Expand Up @@ -140,3 +146,19 @@ def test_bank_sync_unconfigured(mocker, session):
actual._session = session
create_accounts(session, "simplefin")
assert actual.run_bank_sync() == []


def test_bank_sync_exception(session, mocker):
mocker.patch("requests.get").return_value = RequestsMock({"status": "ok", "data": {"validated": True}})
main_mock = mocker.patch("requests.post")
main_mock.side_effect = [
RequestsMock({"status": "ok", "data": {"configured": True}}),
RequestsMock({"status": "ok", "data": fail_response}),
]
with Actual(token="foo") as actual:
actual._session = session
create_accounts(session, "simplefin")

# now try to run the bank sync
with pytest.raises(ActualBankSyncError):
actual.run_bank_sync()

0 comments on commit a05baca

Please sign in to comment.