Skip to content

Commit

Permalink
Refactor by separating cli and logic
Browse files Browse the repository at this point in the history
  • Loading branch information
axeoman committed Aug 13, 2023
1 parent 3fa8726 commit d2b7c90
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 86 deletions.
97 changes: 24 additions & 73 deletions ynab_sync/cli.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import logging
import os
import sys
import uuid
from collections import defaultdict
from datetime import date
from uuid import UUID

import appeal
from requests import HTTPError

from .gocardless.api import GoCardLessAPI
from .ynab.api import YnabAPI
from .ynab.models import YNABTransaction, YNABTransactions

app = appeal.Appeal()

import logging

logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s",
format="%(asctime)s %(name)s [%(levelname)s] %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)

from .logic import (get_gocardless_transactions, prepare_ynab_transactions,
upload_to_ynab)


@app.command()
def upload(
Expand All @@ -33,7 +32,7 @@ def upload(
date_from: str = "",
date_to: str = "",
):
log = logging.getLogger("upload")
log = logging.getLogger("cli.upload")
# TODO: Get this from appeal?
params = {
"ynab_budget_id": ynab_budget_id,
Expand Down Expand Up @@ -63,67 +62,19 @@ def upload(
if error:
return

try:
gocardless_api = GoCardLessAPI(
secret_id=gocardless_secret_id, secret_key=gocardless_secret_key
)

gocardless_transactions = gocardless_api.get_transactions(
account_id=gocardless_account_id, date_from=date_from, date_to=date_to
)
except HTTPError as exc:
log.exception("GoCardless returned HTTPError", exc_info=exc)
return

transactions = []
occurances = defaultdict(int)
for gocardless_transaction in gocardless_transactions.transactions.booked:
amount = int(gocardless_transaction.transaction_amount.amount * 1000)
ynab_import_key = f"YNAB:{amount}:{gocardless_transaction.booking_date}"

memo = (
gocardless_transaction.remittance_information_unstructured
or gocardless_transaction.proprietary_bank_transaction_code
or ""
)
occurances[ynab_import_key] += 1
transactions.append(
YNABTransaction(
account_id=ynab_account_id,
date=gocardless_transaction.booking_date,
amount=amount,
payee_name=gocardless_transaction.creditor_name
or gocardless_transaction.debtor_name
or "",
memo=memo,
cleared="cleared",
approved=False,
# import_id=gocardless_transaction.transaction_id,
import_id=f"{ynab_import_key}:{occurances[ynab_import_key]}",
)
)

if not transactions:
log.info("No transactions reported by GoCardless, nothing to upload")
return

log.info("%s transactions reported by GoCardless", len(transactions))
log.debug("transactions: %s", transactions)
ynab_transactions = YNABTransactions(transactions=transactions)

ynab_api = YnabAPI(access_token=ynab_token)
transactions_json = ynab_transactions.model_dump_json()

try:
response = ynab_api.post_transactions(
budget_id=ynab_budget_id, json_data=transactions_json
)
except HTTPError as exc:
log.exception(
"YNAB returned HTTPError: payload: %s",
transactions_json,
exc_info=exc,
)
return

log.debug("YNAB response: %s", response)
gocardless_bank_account_data = get_gocardless_transactions(
secret_key=gocardless_secret_key,
secret_id=gocardless_secret_id,
account_id=UUID(gocardless_account_id),
date_from=date.fromisoformat(date_from) if date_from else None,
date_to=date.fromisoformat(date_to) if date_to else None,
)

ynab_transactions = prepare_ynab_transactions(
gocardless_bank_data=gocardless_bank_account_data,
ynab_account_id=UUID(ynab_account_id),
)

upload_to_ynab(
transactions=ynab_transactions, token=ynab_token, budget_id=UUID(ynab_budget_id)
)
98 changes: 98 additions & 0 deletions ynab_sync/logic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import logging
from collections import defaultdict
from datetime import date
from uuid import UUID

from requests import HTTPError

from ynab_sync.gocardless.models import GoCardlessBankAccountData

from .gocardless.api import GoCardLessAPI
from .ynab.api import YnabAPI
from .ynab.models import YNABTransaction, YNABTransactions


def get_gocardless_transactions(
secret_id: str,
secret_key: str,
account_id: UUID,
date_from: date | None = None,
date_to: date | None = None,
) -> GoCardlessBankAccountData:
log = logging.getLogger("logic.get_gocardless_transactions")

try:
gocardless_api = GoCardLessAPI(secret_id=secret_id, secret_key=secret_key)

return gocardless_api.get_transactions(
account_id=account_id, date_from=date_from, date_to=date_to
)
except HTTPError as exc:
log.exception("GoCardless returned HTTPError", exc_info=exc)
raise


def prepare_ynab_transactions(
gocardless_bank_data: GoCardlessBankAccountData, ynab_account_id: UUID
) -> YNABTransactions:
log = logging.getLogger("logic.prepare_ynab_transactions")
transactions = []
occurances = defaultdict(int)
for gocardless_transaction in gocardless_bank_data.transactions.booked:
amount = int(gocardless_transaction.transaction_amount.amount * 1000)
ynab_import_key = f"YNAB:{amount}:{gocardless_transaction.booking_date}"

memo = (
gocardless_transaction.remittance_information_unstructured
or gocardless_transaction.proprietary_bank_transaction_code
or ""
)
occurances[ynab_import_key] += 1
transactions.append(
YNABTransaction(
account_id=ynab_account_id,
date=gocardless_transaction.booking_date,
amount=amount,
payee_name=gocardless_transaction.creditor_name
or gocardless_transaction.debtor_name
or "",
memo=memo,
cleared="cleared",
approved=False,
# import_id=gocardless_transaction.transaction_id,
import_id=f"{ynab_import_key}:{occurances[ynab_import_key]}",
)
)

log.info("%s transactions reported by GoCardless", len(transactions))
log.debug("transactions: %s", transactions)
return YNABTransactions(transactions=transactions)


def upload_to_ynab(
transactions: YNABTransactions,
token: str,
budget_id: UUID,
) -> None:
log = logging.getLogger("logic.upload_to_ynab")

if not transactions:
log.info("Transactions are empty, nothing to upload")
return

ynab_api = YnabAPI(access_token=token)
transactions_json = transactions.model_dump_json()

try:
response = ynab_api.post_transactions(
budget_id=budget_id, json_data=transactions_json
)
except HTTPError as exc:
log.exception(
"YNAB returned HTTPError: payload: %s",
transactions_json,
exc_info=exc,
)
raise

log.debug("YNAB response: %s", response)
16 changes: 8 additions & 8 deletions ynab_sync/tests/data/ynab.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
TEST_YNAB_REQUEST_TRANSACTIONS = """{
"transactions": [
{
"account_id": "TEST_GOCARDLESS_ACCOUNT_ID",
"account_id": "93ec6d1f-7d75-48de-85b4-d9caec4807c8",
"date": "2023-08-01",
"amount": 400000,
"payee_name": "XXXX XXXXX",
Expand All @@ -11,7 +11,7 @@
"import_id": "YNAB:400000:2023-08-01:1"
},
{
"account_id": "TEST_GOCARDLESS_ACCOUNT_ID",
"account_id": "93ec6d1f-7d75-48de-85b4-d9caec4807c8",
"date": "2023-08-01",
"amount": -25470,
"payee_name": "LIDL HELSINKI HELSINKI",
Expand All @@ -21,7 +21,7 @@
"import_id": "YNAB:-25470:2023-08-01:1"
},
{
"account_id": "TEST_GOCARDLESS_ACCOUNT_ID",
"account_id": "93ec6d1f-7d75-48de-85b4-d9caec4807c8",
"date": "2023-08-03",
"amount": -15000,
"payee_name": "",
Expand All @@ -31,7 +31,7 @@
"import_id": "YNAB:-15000:2023-08-03:1"
},
{
"account_id": "TEST_GOCARDLESS_ACCOUNT_ID",
"account_id": "93ec6d1f-7d75-48de-85b4-d9caec4807c8",
"date": "2023-08-03",
"amount": 45000,
"payee_name": "MON MOTHMA",
Expand All @@ -41,7 +41,7 @@
"import_id": "YNAB:45000:2023-08-03:1"
},
{
"account_id": "TEST_GOCARDLESS_ACCOUNT_ID",
"account_id": "93ec6d1f-7d75-48de-85b4-d9caec4807c8",
"date": "2023-08-03",
"amount": -15000,
"payee_name": "",
Expand All @@ -51,7 +51,7 @@
"import_id": "YNAB:-15000:2023-08-03:2"
},
{
"account_id": "TEST_GOCARDLESS_ACCOUNT_ID",
"account_id": "93ec6d1f-7d75-48de-85b4-d9caec4807c8",
"date": "2023-08-04",
"amount": 45000,
"payee_name": "MON MOTHMA",
Expand All @@ -61,7 +61,7 @@
"import_id": "YNAB:45000:2023-08-04:1"
},
{
"account_id": "TEST_GOCARDLESS_ACCOUNT_ID",
"account_id": "93ec6d1f-7d75-48de-85b4-d9caec4807c8",
"date": "2023-08-11",
"amount": -15000,
"payee_name": "",
Expand All @@ -71,7 +71,7 @@
"import_id": "YNAB:-15000:2023-08-11:1"
},
{
"account_id": "TEST_GOCARDLESS_ACCOUNT_ID",
"account_id": "93ec6d1f-7d75-48de-85b4-d9caec4807c8",
"date": "2023-08-11",
"amount": 45000,
"payee_name": "MON MOTHMA",
Expand Down
9 changes: 5 additions & 4 deletions ynab_sync/tests/test_e2e.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
from http import HTTPStatus
from uuid import uuid4

import pytest
import responses
Expand All @@ -12,11 +13,11 @@
from ..ynab.api import BASE_URL as YNAB_BASE_URL

TEST_YNAB_TOKEN = "TEST_YNAB_TOKEN"
TEST_YNAB_BUDGET_ID = "TEST_YNAB_BUDGET_ID"
TEST_YNAB_ACCOUNT_ID = "TEST_YNAB_ACCOUNT_ID"
TEST_YNAB_BUDGET_ID = "49c714fa-fa82-4e11-b145-1e1d87a61c1f"
TEST_YNAB_ACCOUNT_ID = "93ec6d1f-7d75-48de-85b4-d9caec4807c8"
TEST_GOCARDLESS_SECRET_ID = "TEST_GOCARDLESS_SECRET_ID"
TEST_GOCARDLESS_SECRET_KEY = "TEST_GOCARDLESS_SECRET_KEY"
TEST_GOCARDLESS_ACCOUNT_ID = "TEST_GOCARDLESS_ACCOUNT_ID"
TEST_GOCARDLESS_ACCOUNT_ID = "dcefb08b-bb77-4a16-ab9f-e8f509de8ec6"
TEST_GOCARDLESS_ACCESS_TOKEN = "TEST_GOCARDLESS_ACCESS_TOKEN"
TEST_GOCARDLESS_REFRESH_TOKEN = "TEST_GOCARDLESS_REFRESH_TOKEN"

Expand Down Expand Up @@ -83,7 +84,7 @@ def test_upload_e2e(date_from: str, date_to: str):
upload(
ynab_token=TEST_YNAB_TOKEN,
ynab_budget_id=TEST_YNAB_BUDGET_ID,
ynab_account_id=TEST_GOCARDLESS_ACCOUNT_ID,
ynab_account_id=TEST_YNAB_ACCOUNT_ID,
gocardless_secret_id=TEST_GOCARDLESS_SECRET_ID,
gocardless_account_id=TEST_GOCARDLESS_ACCOUNT_ID,
gocardless_secret_key=TEST_GOCARDLESS_SECRET_KEY,
Expand Down
3 changes: 2 additions & 1 deletion ynab_sync/ynab/models.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from datetime import date
from uuid import UUID

from pydantic import BaseModel, Field


class YNABTransaction(BaseModel):
account_id: str
account_id: UUID
date: date
amount: int
payee_name: str | None = Field(default="")
Expand Down

0 comments on commit d2b7c90

Please sign in to comment.