Files
catnab/tests/test_main.py

294 lines
8.2 KiB
Python

from datetime import date
from unittest.mock import MagicMock
import pytest
from ynab import TransactionFlagColor
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):
order = MagicMock(
items=[
MagicMock(
title='An example item',
price=17.49
)
]
)
item_details={
'An example item': ItemClassification(
name='Example item',
category='Example category',
description='example',
)
}
def classify(_names, _categories):
return item_details
monkeypatch.setattr('catnab.ai.classify', classify)
res = catnab.get_tx_categories(order, item_details)
assert len(res) == 1
assert res[0].category_name == 'Example category'
assert res[0].description == 'example'
assert res[0].ratio == 1.0
def test_get_categories_multi_items(monkeypatch):
order = MagicMock(
items=[
MagicMock(
title='An example item',
price=17.49
),
MagicMock(
title='Another example',
price=12.34,
)
]
)
item_details={
'An example item': ItemClassification(
name='Example item 1',
category='Example category',
description='example',
),
'Another example': ItemClassification(
name='Example item 2',
category='Example category',
description='second example',
)
}
def classify(_names, _categories):
return item_details
monkeypatch.setattr('catnab.ai.classify', classify)
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'
assert res[0].ratio == 1.0
def test_get_categories_multi_categories(monkeypatch):
order = MagicMock(
items=[
MagicMock(
title='An example item',
price=17.49
),
MagicMock(
title='Another example',
price=12.34,
)
]
)
item_details={
'An example item': ItemClassification(
name='Example item 1',
category='Example category',
description='example',
),
'Another example': ItemClassification(
name='Example item 2',
category='Other category',
description='second example',
)
}
def classify(_names, _categories):
return item_details
monkeypatch.setattr('catnab.ai.classify', classify)
res = catnab.get_tx_categories(order, item_details)
assert len(res) == 2
assert res[0].category_name == 'Example category'
assert res[0].description == 'example'
assert res[0].ratio == pytest.approx(0.59, abs=0.01)
assert res[1].category_name == 'Other category'
assert res[1].description == 'second example'
assert res[1].ratio == pytest.approx(0.41, abs=0.01)
def test_build_tx_update_multi():
tx_categories = [
TxCategory(
category_name='Example category',
description='example',
ratio=0.68,
),
TxCategory(
category_name='Other category',
description='second example',
ratio=0.32,
)
]
category_ids = {
'Example category': '123',
'Other category': '456',
}
tx = MagicMock(
id='958e13e6-7bfd-465e-956a-887f15c8c456',
# ynab uses tenths of a cent as its unit
amount=23760,
)
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'
assert sub0.memo == 'example'
assert sub0.amount == 16157
sub1 = res.subtransactions[1]
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'}