Files @ 5a8da108b983
Branch filter:

Location: NPO-Accounting/conservancy_beancount/tests/test_filters.py

bsturmfels
statement_reconciler: Add initial Chase bank CSV statement matching

We currently don't have many examples to work with, so haven't done any
significant testing of the matching accuracy between statement and books.
"""test_filters - Unit tests for filter functions"""
# Copyright © 2020  Brett Smith
# License: AGPLv3-or-later WITH Beancount-Plugin-Additional-Permission-1.0
#
# Full copyright and licensing details can be found at toplevel file
# LICENSE.txt in the repository.

import itertools

import pytest

from . import testutil

from datetime import date

from beancount.core import data as bc_data

from conservancy_beancount import data
from conservancy_beancount import filters

MISSING_POSTING = testutil.Posting('<Missing Posting>', 0)

@pytest.fixture
def cc_txn_pair():
    dates = testutil.date_seq()
    txn_meta = {
        'payee': 'Smith-Dakota',
        'rt-id': 'rt:550',
    }
    return [
        testutil.Transaction(
            **txn_meta,
            date=next(dates),
            receipt='CCReceipt.pdf',
            metadate=next(dates),
            postings=[
                ('Liabilities:CreditCard', -36),
                ('Expenses:Other', 35),
                ('Expenses:Tax:Sales', 1),
            ],
        ),
        testutil.Transaction(
            **txn_meta,
            date=next(dates),
            receipt='CCPayment.pdf',
            metadate=next(dates),
            postings=[
                ('Liabilities:CreditCard', 36),
                ('Assets:Checking', -36, {'statement': 'CheckingStatement.pdf'}),
            ],
        ),
    ]

def check_filter(actual, entries, expected_indexes):
    postings = [post for txn in entries for post in txn.postings]
    expected = (postings[ii] for ii in expected_indexes)
    for actual_post, expected_post in itertools.zip_longest(
            actual, expected, fillvalue=MISSING_POSTING,
    ):
        assert actual_post[:-1] == expected_post[:-1]

@pytest.mark.parametrize('key,value,expected_indexes', [
    ('entity', 'Smith-Dakota', range(5)),
    ('receipt', 'CCReceipt.pdf', range(3)),
    ('receipt', 'CCPayment.pdf', range(3, 5)),
    ('receipt', 'CC', ()),
    ('statement', 'CheckingStatement.pdf', [4]),
    ('metadate', date(2020, 9, 2), range(3)),
    ('metadate', date(2020, 9, 4), range(3, 5)),
    ('BadKey', '', ()),
    ('emptykey', '', ()),
])
def test_filter_meta_equal(cc_txn_pair, key, value, expected_indexes):
    postings = data.Posting.from_entries(cc_txn_pair)
    actual = filters.filter_meta_equal(postings, key, value)
    check_filter(actual, cc_txn_pair, expected_indexes)

@pytest.mark.parametrize('key,regexp,expected_indexes', [
    ('entity', '^Smith-', range(5)),
    ('receipt', r'\.pdf$', range(5)),
    ('receipt', 'Receipt', range(3)),
    ('statement', '.', [4]),
    ('metadate', 'foo', ()),
    ('BadKey', '.', ()),
    ('emptykey', '.', ()),
])
def test_filter_meta_match(cc_txn_pair, key, regexp, expected_indexes):
    postings = data.Posting.from_entries(cc_txn_pair)
    actual = filters.filter_meta_match(postings, key, regexp)
    check_filter(actual, cc_txn_pair, expected_indexes)

@pytest.mark.parametrize('ticket_id,expected_indexes', [
    (550, range(5)),
    ('550', range(5)),
    (55, ()),
    ('55', ()),
    (50, ()),
    ('.', ()),
])
def test_filter_for_rt_id(cc_txn_pair, ticket_id, expected_indexes):
    postings = data.Posting.from_entries(cc_txn_pair)
    actual = filters.filter_for_rt_id(postings, ticket_id)
    check_filter(actual, cc_txn_pair, expected_indexes)

@pytest.mark.parametrize('rt_id', [
    'rt:450/',
    ' rt:450 rt:540',
    'rt://ticket/450',
    'rt://ticket/450/',
    ' rt://ticket/450',
    'rt://ticket/450 rt://ticket/540',
])
def test_filter_for_rt_id_syntax_variations(rt_id):
    entries = [testutil.Transaction(**{'rt-id': rt_id}, postings=[
        ('Income:Donations', -10),
        ('Assets:Cash', 10),
    ])]
    postings = data.Posting.from_entries(entries)
    actual = filters.filter_for_rt_id(postings, 450)
    check_filter(actual, entries, range(2))

def test_filter_for_rt_id_uses_first_link_only():
    entries = [testutil.Transaction(postings=[
        ('Income:Donations', -10, {'rt-id': 'rt:1 rt:350'}),
        ('Assets:Cash', 10, {'rt-id': 'rt://ticket/2 rt://ticket/350'}),
    ])]
    postings = data.Posting.from_entries(entries)
    actual = filters.filter_for_rt_id(postings, 350)
    check_filter(actual, entries, ()),

@pytest.mark.parametrize('opening_txn', [
    testutil.OpeningBalance(),
    None,
])
def test_remove_opening_balance_txn(opening_txn):
    entries = [
        testutil.Transaction(postings=[
            (account, amount),
            ('Assets:Checking', -amount),
        ])
        for account, amount in [
                ('Income:Donations', -50),
                ('Expenses:Other', 75),
        ]]
    if opening_txn is not None:
        entries.insert(1, opening_txn)
    actual = filters.remove_opening_balance_txn(entries)
    assert actual is opening_txn
    assert opening_txn not in entries
    assert not any(
        post.account.startswith('Equity:')
        for entry in entries
        for post in getattr(entry, 'postings', ())
    )

@pytest.mark.parametrize('entry', [
    bc_data.Custom({}, testutil.FY_START_DATE, 'conservancy_beancount_audit', []),
    None,
])
def test_audit_date(entry):
    dates = testutil.date_seq()
    entries = [
        bc_data.Open({}, next(dates), 'Income:Donations', ['USD'], None),
        bc_data.Open({}, next(dates), 'Assets:Cash', ['USD'], None),
        testutil.Transaction(postings=[
            ('Income:Donations', -10),
            ('Assets:Cash', 10),
        ]),
    ]
    if entry is not None:
        entries.append(entry)
    actual = filters.audit_date(entries)
    if entry is None:
        assert actual is None
    else:
        assert actual == entry.date

def test_iter_unique():
    assert list(filters.iter_unique('1213231')) == list('123')