Changeset - 6658696d0687
[Not reviewed]
0 2 0
Brett Smith - 4 years ago 2020-04-04 14:54:08
brettcsmith@brettcsmith.org
meta_receipt: Use check-id as fallback metadata for outgoing checks.

When we send checks, we don't have a check document anywhere (for
security reasons), we just note the check number. Update the
validation to match. RT#10507.
2 files changed with 123 insertions and 13 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/plugin/meta_receipt.py
Show inline comments
...
 
@@ -16,2 +16,4 @@
 

	
 
from decimal import Decimal
 

	
 
from . import core
...
 
@@ -21,2 +23,3 @@ from .. import errors as errormod
 
from ..beancount_types import (
 
    MetaKey,
 
    Transaction,
...
 
@@ -24,2 +27,8 @@ from ..beancount_types import (
 

	
 
from typing import (
 
    Callable,
 
)
 

	
 
_CheckMethod = Callable[[Transaction, data.Posting, MetaKey], None]
 

	
 
class MetaReceipt(core._RequireLinksPostingMetadataHook):
...
 
@@ -37,2 +46,9 @@ class MetaReceipt(core._RequireLinksPostingMetadataHook):
 

	
 
    def _check_check_id(self, txn: Transaction, post: data.Posting, key: MetaKey) -> None:
 
        value = post.meta.get(key)
 
        if (not isinstance(value, Decimal)
 
            or value < 1
 
            or value % 1):
 
            raise errormod.InvalidMetadataError(txn, key, value, post, Decimal)
 

	
 
    def post_run(self, txn: Transaction, post: data.Posting) -> errormod.Iter:
...
 
@@ -52,4 +68,9 @@ class MetaReceipt(core._RequireLinksPostingMetadataHook):
 

	
 
        check_method: _CheckMethod = self._check_links
 
        if post.account.is_checking():
 
            fallback_key = 'check'
 
            if post_amount == -1:
 
                check_method = self._check_check_id
 
                fallback_key = 'check-id'
 
            else:
 
                fallback_key = 'check'
 
        elif post.account.is_credit_card() and post_amount == -1:
...
 
@@ -63,3 +84,3 @@ class MetaReceipt(core._RequireLinksPostingMetadataHook):
 
        try:
 
            self._check_links(txn, post, fallback_key)
 
            check_method(txn, post, fallback_key)
 
        except errormod.InvalidMetadataError as fallback_error:
tests/test_meta_receipt.py
Show inline comments
...
 
@@ -50,5 +50,7 @@ class AccountForTesting(typing.NamedTuple):
 
    def wrong_type_message(self, wrong_value, key=TEST_KEY):
 
        return  "{} has wrong type of {}: expected str but is a {}".format(
 
        expect_type = 'Decimal' if key == 'check-id' else 'str'
 
        return  "{} has wrong type of {}: expected {} but is a {}".format(
 
            self.name,
 
            key,
 
            expect_type,
 
            type(wrong_value).__name__,
...
 
@@ -58,5 +60,7 @@ class AccountForTesting(typing.NamedTuple):
 
ACCOUNTS = [AccountForTesting._make(t) for t in [
 
    ('Assets:Bank:CheckCard', PostType.BOTH, 'check'),
 
    ('Assets:Bank:CheckCard', PostType.CREDIT, 'check'),
 
    ('Assets:Bank:CheckCard', PostType.DEBIT, 'check-id'),
 
    ('Assets:Cash', PostType.BOTH, None),
 
    ('Assets:Checking', PostType.BOTH, 'check'),
 
    ('Assets:Checking', PostType.CREDIT, 'check'),
 
    ('Assets:Checking', PostType.DEBIT, 'check-id'),
 
    ('Assets:PayPal', PostType.CREDIT, 'paypal-id'),
...
 
@@ -68,5 +72,8 @@ ACCOUNTS = [AccountForTesting._make(t) for t in [
 

	
 
ACCOUNTS_WITH_FALLBACKS = [acct for acct in ACCOUNTS if acct.fallback_meta]
 
ACCOUNTS_WITH_LINK_FALLBACK = [acct for acct in ACCOUNTS
 
                               if acct.fallback_meta and acct.fallback_meta != 'check-id']
 
ACCOUNTS_WITH_CHECK_ID_FALLBACK = [acct for acct in ACCOUNTS
 
                                   if acct.fallback_meta == 'check-id']
 
ACCOUNTS_WITHOUT_FALLBACKS = [acct for acct in ACCOUNTS if not acct.fallback_meta]
 
KNOWN_FALLBACKS = {acct.fallback_meta for acct in ACCOUNTS_WITH_FALLBACKS}
 
KNOWN_FALLBACKS = {acct.fallback_meta for acct in ACCOUNTS if acct.fallback_meta}
 

	
...
 
@@ -88,2 +95,10 @@ NOT_REQUIRED_ACCOUNTS = itertools.cycle([
 

	
 
CHECK_IDS = (decimal.Decimal(n) for n in itertools.count(1))
 
def BAD_CHECK_IDS():
 
    # Valid check-id values are positive integers
 
    yield decimal.Decimal(0)
 
    yield -next(CHECK_IDS)
 
    yield next(CHECK_IDS) * decimal.Decimal('1.1')
 
BAD_CHECK_IDS = BAD_CHECK_IDS()
 

	
 
def check(hook, test_acct, other_acct, expected, *,
...
 
@@ -169,3 +184,3 @@ def test_bad_type_receipt_on_txn(hook, test_acct, other_acct, value):
 
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
 
    ACCOUNTS_WITH_FALLBACKS,
 
    ACCOUNTS_WITH_LINK_FALLBACK,
 
    NOT_REQUIRED_ACCOUNTS,
...
 
@@ -178,3 +193,3 @@ def test_valid_fallback_on_post(hook, test_acct, other_acct, value):
 
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
 
    ACCOUNTS_WITH_FALLBACKS,
 
    ACCOUNTS_WITH_LINK_FALLBACK,
 
    NOT_REQUIRED_ACCOUNTS,
...
 
@@ -187,3 +202,3 @@ def test_invalid_fallback_on_post(hook, test_acct, other_acct, value):
 
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
 
    ACCOUNTS_WITH_FALLBACKS,
 
    ACCOUNTS_WITH_LINK_FALLBACK,
 
    NOT_REQUIRED_ACCOUNTS,
...
 
@@ -200,3 +215,3 @@ def test_bad_type_fallback_on_post(hook, test_acct, other_acct, value):
 
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
 
    ACCOUNTS_WITH_FALLBACKS,
 
    ACCOUNTS_WITH_LINK_FALLBACK,
 
    NOT_REQUIRED_ACCOUNTS,
...
 
@@ -209,3 +224,3 @@ def test_valid_fallback_on_txn(hook, test_acct, other_acct, value):
 
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
 
    ACCOUNTS_WITH_FALLBACKS,
 
    ACCOUNTS_WITH_LINK_FALLBACK,
 
    NOT_REQUIRED_ACCOUNTS,
...
 
@@ -218,3 +233,3 @@ def test_invalid_fallback_on_txn(hook, test_acct, other_acct, value):
 
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
 
    ACCOUNTS_WITH_FALLBACKS,
 
    ACCOUNTS_WITH_LINK_FALLBACK,
 
    NOT_REQUIRED_ACCOUNTS,
...
 
@@ -230,2 +245,76 @@ def test_bad_type_fallback_on_txn(hook, test_acct, other_acct, value):
 

	
 
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
 
    ACCOUNTS_WITH_CHECK_ID_FALLBACK,
 
    NOT_REQUIRED_ACCOUNTS,
 
    CHECK_IDS,
 
))
 
def test_valid_check_id_on_post(hook, test_acct, other_acct, value):
 
    check(hook, test_acct, other_acct, None,
 
          post_meta={test_acct.fallback_meta: value})
 

	
 
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
 
    ACCOUNTS_WITH_CHECK_ID_FALLBACK,
 
    NOT_REQUIRED_ACCOUNTS,
 
    BAD_CHECK_IDS,
 
))
 
def test_invalid_check_id_on_post(hook, test_acct, other_acct, value):
 
    expected = {
 
        test_acct.missing_message(False),
 
        f"{test_acct.name} has invalid {test_acct.fallback_meta}: {value}",
 
    }
 
    check(hook, test_acct, other_acct, expected,
 
          post_meta={test_acct.fallback_meta: value})
 

	
 
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
 
    ACCOUNTS_WITH_CHECK_ID_FALLBACK,
 
    NOT_REQUIRED_ACCOUNTS,
 
    testutil.NON_STRING_METADATA_VALUES,
 
))
 
def test_bad_type_check_id_on_post(hook, test_acct, other_acct, value):
 
    if isinstance(value, decimal.Decimal):
 
        value = ''
 
    expected = {
 
        test_acct.missing_message(False),
 
        test_acct.wrong_type_message(value, test_acct.fallback_meta),
 
    }
 
    check(hook, test_acct, other_acct, expected,
 
          post_meta={test_acct.fallback_meta: value})
 

	
 
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
 
    ACCOUNTS_WITH_CHECK_ID_FALLBACK,
 
    NOT_REQUIRED_ACCOUNTS,
 
    CHECK_IDS,
 
))
 
def test_valid_check_id_on_txn(hook, test_acct, other_acct, value):
 
    check(hook, test_acct, other_acct, None,
 
          txn_meta={test_acct.fallback_meta: value})
 

	
 
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
 
    ACCOUNTS_WITH_CHECK_ID_FALLBACK,
 
    NOT_REQUIRED_ACCOUNTS,
 
    BAD_CHECK_IDS,
 
))
 
def test_invalid_check_id_on_txn(hook, test_acct, other_acct, value):
 
    expected = {
 
        test_acct.missing_message(False),
 
        f"{test_acct.name} has invalid {test_acct.fallback_meta}: {value}",
 
    }
 
    check(hook, test_acct, other_acct, expected,
 
          txn_meta={test_acct.fallback_meta: value})
 

	
 
@pytest.mark.parametrize('test_acct,other_acct,value', testutil.combine_values(
 
    ACCOUNTS_WITH_CHECK_ID_FALLBACK,
 
    NOT_REQUIRED_ACCOUNTS,
 
    testutil.NON_STRING_METADATA_VALUES,
 
))
 
def test_bad_type_check_id_on_txn(hook, test_acct, other_acct, value):
 
    if isinstance(value, decimal.Decimal):
 
        value = ''
 
    expected = {
 
        test_acct.missing_message(False),
 
        test_acct.wrong_type_message(value, test_acct.fallback_meta),
 
    }
 
    check(hook, test_acct, other_acct, expected,
 
          txn_meta={test_acct.fallback_meta: value})
 

	
 
@pytest.mark.parametrize('test_acct,other_acct,key,value', testutil.combine_values(
0 comments (0 inline, 0 general)