Skip to content

Commit

Permalink
fix: Integrate goCardless.
Browse files Browse the repository at this point in the history
  • Loading branch information
bvanelli committed Jun 18, 2024
1 parent 29a374b commit 105188d
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 43 deletions.
69 changes: 45 additions & 24 deletions actual/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,38 @@ def commit(self):
# sync all changes to the server
self.sync_sync(req)

def run_bank_sync(self, account: str | Accounts | None = None) -> list[Transactions]:
"""Runs the bank synchronization for the selected account. If missing, all accounts are synchronized."""
def _run_bank_sync_account(self, acct: Accounts, start_date: datetime.date) -> list[Transactions]:
sync_method = acct.account_sync_source
account_id = acct.account_id
requisition_id = acct.bank.bank_id if sync_method == "goCardless" else None
new_transactions_data = self.bank_sync_transactions(

Check warning on line 369 in actual/__init__.py

View check run for this annotation

Codecov / codecov/patch

actual/__init__.py#L366-L369

Added lines #L366 - L369 were not covered by tests
sync_method.lower(), account_id, start_date, requisition_id=requisition_id
)
new_transactions = new_transactions_data.data.transactions.all
imported_transactions = []
for transaction in new_transactions:
note = transaction.remittance_information_unstructured
payee = transaction.payee or "" if sync_method == "goCardless" else note
reconciled = reconcile_transaction(

Check warning on line 377 in actual/__init__.py

View check run for this annotation

Codecov / codecov/patch

actual/__init__.py#L372-L377

Added lines #L372 - L377 were not covered by tests
self.session,
transaction.date,
acct,
payee,
note,
amount=transaction.transaction_amount.amount,
imported_id=transaction.transaction_id,
)
imported_transactions.append(reconciled)
return imported_transactions

Check warning on line 387 in actual/__init__.py

View check run for this annotation

Codecov / codecov/patch

actual/__init__.py#L386-L387

Added lines #L386 - L387 were not covered by tests

def run_bank_sync(
self, account: str | Accounts | None = None, start_date: datetime.date | None = None
) -> list[Transactions]:
"""
Runs the bank synchronization for the selected account. If missing, all accounts are synchronized. If a
start_date is provided, is used as a reference, otherwise, the last timestamp of each account will be used. If
the account does not have any transaction, the last 90 days are considered instead.
"""
# if no account is provided, sync all of them, otherwise just the account provided
if account is None:
accounts = get_accounts(self.session)

Check warning on line 399 in actual/__init__.py

View check run for this annotation

Codecov / codecov/patch

actual/__init__.py#L398-L399

Added lines #L398 - L399 were not covered by tests
Expand All @@ -374,26 +404,17 @@ def run_bank_sync(self, account: str | Accounts | None = None) -> list[Transacti
for acct in accounts:
sync_method = acct.account_sync_source
account_id = acct.account_id
if account_id and sync_method:
status = self.bank_sync_status(sync_method.lower())
if status.data.configured:
all_transactions = get_transactions(self.session, account=acct)
if all_transactions:
start_date = all_transactions[0].get_date()
else:
start_date = datetime.date.today() - datetime.timedelta(days=90)
new_transactions_data = self.bank_sync_transactions(sync_method.lower(), account_id, start_date)
new_transactions = new_transactions_data.data.transactions.all
for transaction in new_transactions:
note = transaction.remittance_information_unstructured
reconciled = reconcile_transaction(
self.session,
transaction.date,
acct,
note,
note,
amount=transaction.transaction_amount.amount,
imported_id=transaction.transaction_id,
)
imported_transactions.append(reconciled)
if not (account_id and sync_method):
continue
status = self.bank_sync_status(sync_method.lower())
if not status.data.configured:
continue
if start_date is None:
all_transactions = get_transactions(self.session, account=acct)
if all_transactions:
default_start_date = all_transactions[0].get_date()
elif start_date is None:
default_start_date = datetime.date.today() - datetime.timedelta(days=90)
transactions = self._run_bank_sync_account(acct, start_date or default_start_date)
imported_transactions.extend(transactions)
return imported_transactions

Check warning on line 420 in actual/__init__.py

View check run for this annotation

Codecov / codecov/patch

actual/__init__.py#L401-L420

Added lines #L401 - L420 were not covered by tests
23 changes: 16 additions & 7 deletions actual/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
ValidateDTO,
)
from actual.crypto import create_key_buffer, make_test_message
from actual.exceptions import AuthorizationError, UnknownFileId
from actual.exceptions import (
ActualInvalidOperationError,
AuthorizationError,
UnknownFileId,
)
from actual.protobuf_models import SyncRequest, SyncResponse


Expand Down Expand Up @@ -230,12 +234,17 @@ def bank_sync_accounts(self, bank_sync: Literal["gocardless", "simplefin"]) -> B
return BankSyncAccountResponseDTO.model_validate(response.json())

Check warning on line 234 in actual/api/__init__.py

View check run for this annotation

Codecov / codecov/patch

actual/api/__init__.py#L232-L234

Added lines #L232 - L234 were not covered by tests

def bank_sync_transactions(
self, bank_sync: Literal["gocardless", "simplefin"] | str, account_id: str, start_date: datetime.date
self,
bank_sync: Literal["gocardless", "simplefin"] | str,
account_id: str,
start_date: datetime.date,
requisition_id: str = None,
) -> BankSyncTransactionResponseDTO:
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)
response = requests.post(
f"{self.api_url}/{endpoint}",
headers=self.headers(),
json={"accountId": account_id, "startDate": start_date.strftime("%Y-%m-%d")},
)
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)
return BankSyncTransactionResponseDTO.model_validate(response.json())

Check warning on line 250 in actual/api/__init__.py

View check run for this annotation

Codecov / codecov/patch

actual/api/__init__.py#L243-L250

Added lines #L243 - L250 were not covered by tests
19 changes: 11 additions & 8 deletions actual/api/bank_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import enum
from typing import List, Optional

from pydantic import BaseModel, Field
from pydantic import AliasChoices, BaseModel, Field


class BankSyncTransactionDTO(BaseModel):
Expand Down Expand Up @@ -58,20 +58,20 @@ class Balance(BaseModel):


class TransactionItem(BaseModel):
booked: bool
booking_date: str = Field(..., alias="bookingDate")
date: datetime.date
debtor_name: str = Field(..., alias="debtorName")
remittance_information_unstructured: str = Field(..., alias="remittanceInformationUnstructured")
transaction_amount: BankSyncAmount = Field(..., alias="transactionAmount")
transaction_id: str = Field(..., alias="transactionId")
booking_date: str = Field(..., alias="bookingDate")
value_date: str = Field(..., alias="valueDate")
transaction_amount: BankSyncAmount = Field(..., alias="transactionAmount")
# this field will come as either debtorName or creditorName, depending on if it's a debt or credit
payee: str = Field(None, validation_alias=AliasChoices("debtorName", "creditorName"))
date: datetime.date
remittance_information_unstructured: str = Field(None, alias="remittanceInformationUnstructured")


class Transactions(BaseModel):
all: List[TransactionItem]
booked: List[TransactionItem]
pending: List
pending: List[TransactionItem]


class BankSyncAccountData(BaseModel):
Expand All @@ -82,3 +82,6 @@ class BankSyncTransactionData(BaseModel):
balances: List[Balance]
starting_balance: int = Field(..., alias="startingBalance")
transactions: Transactions
# goCardless specific
iban: Optional[str] = None
institution_id: Optional[str] = Field(None, alias="institutionId")
11 changes: 10 additions & 1 deletion actual/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ class Accounts(BaseModel, table=True):
mask: Optional[str] = Field(default=None, sa_column=Column("mask", Text))
official_name: Optional[str] = Field(default=None, sa_column=Column("official_name", Text))
subtype: Optional[str] = Field(default=None, sa_column=Column("subtype", Text))
bank: Optional[str] = Field(default=None, sa_column=Column("bank", Text))
bank_id: Optional[str] = Field(default=None, sa_column=Column("bank", Text, ForeignKey("banks.id")))
offbudget: Optional[int] = Field(default=None, sa_column=Column("offbudget", Integer, server_default=text("0")))
closed: Optional[int] = Field(default=None, sa_column=Column("closed", Integer, server_default=text("0")))
tombstone: Optional[int] = Field(default=None, sa_column=Column("tombstone", Integer, server_default=text("0")))
Expand All @@ -183,6 +183,13 @@ class Accounts(BaseModel, table=True):
)
},
)
bank: "Banks" = Relationship(
back_populates="account",
sa_relationship_kwargs={
"uselist": False,
"primaryjoin": "and_(Accounts.bank_id == Banks.id,Banks.tombstone == 0)",
},
)

@property
def balance(self) -> decimal.Decimal:
Expand All @@ -203,6 +210,8 @@ class Banks(BaseModel, table=True):
name: Optional[str] = Field(default=None, sa_column=Column("name", Text))
tombstone: Optional[int] = Field(default=None, sa_column=Column("tombstone", Integer, server_default=text("0")))

account: "Accounts" = Relationship(back_populates="bank")


class Categories(BaseModel, table=True):
hidden: bool = Field(sa_column=Column("hidden", Boolean, nullable=False, server_default=text("0")))
Expand Down
7 changes: 4 additions & 3 deletions actual/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,10 @@ def reconcile_transaction(
# try to update fields
match.acct = account.id
match.notes = notes
match.category = get_or_create_category(s, category).id
match.date = date
return match[0]
if category:
match.category = get_or_create_category(s, category).id
match.set_date(date)
return match
return create_transaction(s, date, account, payee, notes, category, amount, imported_id)

Check warning on line 256 in actual/queries.py

View check run for this annotation

Codecov / codecov/patch

actual/queries.py#L250-L256

Added lines #L250 - L256 were not covered by tests


Expand Down

0 comments on commit 105188d

Please sign in to comment.