Files @ 5784068904e8
Branch filter:

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

bkuhn
payroll-type — US:403b:Employee:Roth — needed separate since taxable

Since Roth contributions are taxable, there are some reports that
need to include these amounts in total salary (i.e., when running a
report that seeks to show total taxable income for an employee). As
such, we need a `payroll-type` specifically for Roth 403(b)
contributions.
"""Test validation of approval metadata"""
# 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 pytest

from . import testutil

from conservancy_beancount.plugin import meta_approval

REQUIRED_ACCOUNTS = {
    'Assets:Bank:Checking',
    'Assets:Cash',
    'Assets:Savings',
}

NON_REQUIRED_ACCOUNTS = {
    'Assets:Prepaid:Expenses',
    # Receivables are checked by meta_receivable_documentation
    'Assets:Receivable:Accounts',
    'Equity:QpeningBalance',
    'Expenses:Other',
    'Income:Other',
    # Payables are checked by meta_payable_documentation
    'Liabilities:Payable:Accounts',
    'Liabilities:Payable:Vacation',
    'Liabilities:UnearnedIncome:Donations',
}

CREDITCARD_ACCOUNT = 'Liabilities:CreditCard'

TEST_KEY = 'approval'

@pytest.fixture(scope='module')
def hook():
    config = testutil.TestConfig()
    return meta_approval.MetaApproval(config)

@pytest.mark.parametrize('acct1,acct2,value', testutil.combine_values(
    REQUIRED_ACCOUNTS,
    NON_REQUIRED_ACCOUNTS,
    testutil.LINK_METADATA_STRINGS,
))
def test_valid_values_on_postings(hook, acct1, acct2, value):
    txn = testutil.Transaction(postings=[
        (acct2, 25),
        (acct1, -25, {TEST_KEY: value}),
    ])
    assert not list(hook.run(txn))

@pytest.mark.parametrize('acct1,acct2,value', testutil.combine_values(
    REQUIRED_ACCOUNTS,
    NON_REQUIRED_ACCOUNTS,
    testutil.NON_LINK_METADATA_STRINGS,
))
def test_invalid_values_on_postings(hook, acct1, acct2, value):
    txn = testutil.Transaction(postings=[
        (acct2, 25),
        (acct1, -25, {TEST_KEY: value}),
    ])
    actual = {error.message for error in hook.run(txn)}
    assert actual == {"{} missing {}".format(acct1, TEST_KEY)}

@pytest.mark.parametrize('acct1,acct2,value', testutil.combine_values(
    REQUIRED_ACCOUNTS,
    NON_REQUIRED_ACCOUNTS,
    testutil.NON_STRING_METADATA_VALUES,
))
def test_bad_type_values_on_postings(hook, acct1, acct2, value):
    txn = testutil.Transaction(postings=[
        (acct2, 25),
        (acct1, -25, {TEST_KEY: value}),
    ])
    expected_msg = "{} has wrong type of {}: expected str but is a {}".format(
        acct1,
        TEST_KEY,
        type(value).__name__,
    )
    actual = {error.message for error in hook.run(txn)}
    assert expected_msg in actual

@pytest.mark.parametrize('acct1,acct2,value', testutil.combine_values(
    REQUIRED_ACCOUNTS,
    NON_REQUIRED_ACCOUNTS,
    testutil.LINK_METADATA_STRINGS,
))
def test_valid_values_on_transaction(hook, acct1, acct2, value):
    txn = testutil.Transaction(**{TEST_KEY: value}, postings=[
        (acct2, 25),
        (acct1, -25),
    ])
    assert not list(hook.run(txn))

@pytest.mark.parametrize('acct1,acct2,value', testutil.combine_values(
    REQUIRED_ACCOUNTS,
    NON_REQUIRED_ACCOUNTS,
    testutil.NON_LINK_METADATA_STRINGS,
))
def test_invalid_values_on_transaction(hook, acct1, acct2, value):
    txn = testutil.Transaction(**{TEST_KEY: value}, postings=[
        (acct2, 25),
        (acct1, -25),
    ])
    actual = {error.message for error in hook.run(txn)}
    assert actual == {"{} missing {}".format(acct1, TEST_KEY)}

@pytest.mark.parametrize('acct1,acct2,value', testutil.combine_values(
    REQUIRED_ACCOUNTS,
    NON_REQUIRED_ACCOUNTS,
    testutil.NON_STRING_METADATA_VALUES,
))
def test_bad_type_values_on_transaction(hook, acct1, acct2, value):
    txn = testutil.Transaction(**{TEST_KEY: value}, postings=[
        (acct2, 25),
        (acct1, -25),
    ])
    expected_msg = "{} has wrong type of {}: expected str but is a {}".format(
        acct1,
        TEST_KEY,
        type(value).__name__,
    )
    actual = {error.message for error in hook.run(txn)}
    assert expected_msg in actual

@pytest.mark.parametrize('acct1,acct2', testutil.combine_values(
    REQUIRED_ACCOUNTS,
    NON_REQUIRED_ACCOUNTS,
))
def test_approval_not_required_on_credits(hook, acct1, acct2):
    txn = testutil.Transaction(postings=[
        (acct2, -25),
        (acct1, 25),
    ])
    assert not list(hook.run(txn))

def test_approval_not_required_to_charge_credit_card(hook):
    txn = testutil.Transaction(postings=[
        ('Expenses:Other', 25),
        (CREDITCARD_ACCOUNT, 25),
    ])
    assert not list(hook.run(txn))

def test_approval_not_required_to_pay_credit_card(hook):
    txn = testutil.Transaction(postings=[
        ('Assets:Checking', -25),
        (CREDITCARD_ACCOUNT, 25),
    ])
    assert not list(hook.run(txn))

@pytest.mark.parametrize('tax_implication,other_acct', [
    ('Bank-Transfer', 'Assets:Savings'),
    ('Chargeback', 'Income:Donations'),
])
def test_approval_not_required_by_tax_implication(hook, tax_implication, other_acct):
    txn = testutil.Transaction(postings=[
        ('Assets:Checking', -250, {'tax-implication': tax_implication}),
        (other_acct, 245),
        ('Expenses:BankingFees', 5),
    ])
    assert not list(hook.run(txn))

def test_not_required_on_flagged(hook):
    txn = testutil.Transaction(flag='!', postings=[
        ('Assets:Checking', -25),
        ('Liabilities:Payable:Accounts', 25),
    ])
    assert not list(hook.run(txn))