move everything into src/catnab and add more tests

This commit is contained in:
2025-12-09 08:39:04 -05:00
parent 2509c4dd70
commit 761c7eeda4
7 changed files with 351 additions and 26 deletions

View File

@@ -1,10 +1,102 @@
from datetime import date
from unittest.mock import MagicMock
import pytest
from ynab import TransactionFlagColor
from ai import ItemClassification
import main
from main import TxCategory
from catnab.ai import ItemClassification
import catnab
from catnab import Context, TxCategory
from catnab.amazon import AmazonAccount
def test_is_matchable_ynab(mk_ynab_tx):
matchable = mk_ynab_tx()
assert catnab.is_matchable_ynab(matchable)
wrong_payee = mk_ynab_tx(payee_name='Walmart')
assert not catnab.is_matchable_ynab(wrong_payee)
already_matched = mk_ynab_tx(flag_color=TransactionFlagColor.PURPLE)
assert not catnab.is_matchable_ynab(already_matched)
already_approved = mk_ynab_tx(approved=True)
assert not catnab.is_matchable_ynab(already_approved)
# fortunately we only need to check the length of the subtransactions list
already_split = mk_ynab_tx(subtransactions=[1, 2, 3])
assert not catnab.is_matchable_ynab(already_split)
def test_is_matchable_amz(mk_amz_tx):
matchable = mk_amz_tx()
assert catnab.is_matchable_amz(matchable)
refund = mk_amz_tx(is_refund=True)
assert not catnab.is_matchable_amz(refund)
empty_order_number = mk_amz_tx(order_number='')
assert not catnab.is_matchable_amz(empty_order_number)
gift_card_order=mk_amz_tx(payment_method='Amazon Gift Card')
assert not catnab.is_matchable_amz(gift_card_order)
points_order = mk_amz_tx(payment_method='Amazon Visa points')
assert not catnab.is_matchable_amz(points_order)
def test_tx_matches(mk_amz_tx, mk_ynab_tx):
amz_tx = mk_amz_tx(
grand_total=123.45,
completed_date=date(2025, 1, 1),
)
ynab_tx=mk_ynab_tx(
amount=123450,
var_date=date(2025, 1, 1),
)
matches = catnab.tx_matches([amz_tx], [ynab_tx])
assert len(matches) == 1
def test_tx_matches_max_datediff(mk_amz_tx, mk_ynab_tx):
# with a 7-day difference, transactions match
amz_tx = mk_amz_tx(
grand_total=123.45,
completed_date=date(2025, 1, 1),
)
ynab_tx=mk_ynab_tx(
amount=123450,
var_date=date(2025, 1, 8),
)
matches = catnab.tx_matches([amz_tx], [ynab_tx])
assert len(matches) == 1
# but with an 8-day difference, they don't
ynab_tx.var_date = date(2026, 1, 9)
matches = catnab.tx_matches([amz_tx], [ynab_tx])
assert len(matches) == 0
def test_tx_matches_tiebreak(mk_amz_tx, mk_ynab_tx):
amz_tx = mk_amz_tx(
grand_total=123.45,
completed_date=date(2025, 1, 1),
)
ynab_1 = mk_ynab_tx(
amount=123450,
var_date=date(2025, 1, 5),
)
ynab_2 = mk_ynab_tx(
amount=123450,
var_date=date(2025, 1, 3),
)
matches = catnab.tx_matches([amz_tx], [ynab_1, ynab_2])
assert len(matches) == 1
matched_amz, matched_ynab = matches[0]
assert matched_ynab is ynab_2
def test_get_tx_categories(monkeypatch):
@@ -27,9 +119,9 @@ def test_get_tx_categories(monkeypatch):
def classify(_names, _categories):
return item_details
monkeypatch.setattr('ai.classify', classify)
monkeypatch.setattr('catnab.ai.classify', classify)
res = main.get_tx_categories(order, item_details)
res = catnab.get_tx_categories(order, item_details)
assert len(res) == 1
assert res[0].category_name == 'Example category'
assert res[0].description == 'example'
@@ -65,9 +157,9 @@ def test_get_categories_multi_items(monkeypatch):
def classify(_names, _categories):
return item_details
monkeypatch.setattr('ai.classify', classify)
monkeypatch.setattr('catnab.ai.classify', classify)
res = main.get_tx_categories(order, item_details)
res = catnab.get_tx_categories(order, item_details)
assert len(res) == 1
assert res[0].category_name == 'Example category'
assert res[0].description == 'example, second example'
@@ -103,9 +195,9 @@ def test_get_categories_multi_categories(monkeypatch):
def classify(_names, _categories):
return item_details
monkeypatch.setattr('ai.classify', classify)
monkeypatch.setattr('catnab.ai.classify', classify)
res = main.get_tx_categories(order, item_details)
res = catnab.get_tx_categories(order, item_details)
assert len(res) == 2
assert res[0].category_name == 'Example category'
assert res[0].description == 'example'
@@ -141,8 +233,8 @@ def test_build_tx_update_multi():
amount=23760,
)
res = main.build_tx_update(tx_categories, category_ids, tx)
assert len(res.subtransactions) == 2
res = catnab.build_tx_update(tx_categories, category_ids, tx)
assert res.subtransactions is not None and len(res.subtransactions) == 2
sub0 = res.subtransactions[0]
assert sub0.category_id == '123'
@@ -153,3 +245,49 @@ def test_build_tx_update_multi():
assert sub1.category_id == '456'
assert sub1.memo == 'second example'
assert sub1.amount == 7603
def test_sync_acct_basic(basic_order, ynab_creds):
ctx = Context(sync_days=30)
acct = AmazonAccount('test@example.com', 'password123')
catnab.sync_account(ctx, acct)
mock_update = ctx.ynab_client.update_transactions
mock_update.assert_called_once()
tx_update = mock_update.call_args.args[0][0]
assert tx_update.id == '1'
assert tx_update.category_id == '0'
assert tx_update.flag_color == 'purple'
def test_sync_acct_multi_items(multi_item_order, ynab_creds):
ctx = Context(sync_days=30)
acct = AmazonAccount('test@example.com', 'password123')
catnab.sync_account(ctx, acct)
mock_update = ctx.ynab_client.update_transactions
mock_update.assert_called_once()
tx_update = mock_update.call_args.args[0][0]
assert tx_update.id == '1'
assert tx_update.category_id == '0'
assert tx_update.flag_color == 'purple'
def test_sync_acct_multi_categories(multi_category_order, ynab_creds):
ctx = Context(sync_days=30)
acct = AmazonAccount('test@example.com', 'password123')
catnab.sync_account(ctx, acct)
mock_update = ctx.ynab_client.update_transactions
mock_update.assert_called_once()
tx_update = mock_update.call_args.args[0][0]
assert tx_update.id == '1'
assert tx_update.category_id == None
assert tx_update.flag_color == 'purple'
assert len(tx_update.subtransactions) == 2
# we don't care about order, but both categories need to be present
assert set(st.category_id for st in tx_update.subtransactions) == {'0', '1'}