From 7cf564ae19780879e3ae74772d253acf29c4860c Mon Sep 17 00:00:00 2001 From: apromessi Date: Tue, 14 May 2024 20:56:10 -0700 Subject: [PATCH 1/9] create new files for constants and utils --- oldabe/constants.py | 16 ++++ oldabe/money_in.py | 152 ++++-------------------------------- oldabe/money_out.py | 9 +-- oldabe/utils.py | 84 ++++++++++++++++++++ tests/unit/money_in_test.py | 100 +----------------------- tests/unit/utils_test.py | 126 ++++++++++++++++++++++++++++++ 6 files changed, 245 insertions(+), 242 deletions(-) create mode 100644 oldabe/constants.py create mode 100644 oldabe/utils.py create mode 100644 tests/unit/utils_test.py diff --git a/oldabe/constants.py b/oldabe/constants.py new file mode 100644 index 0000000..5a4040a --- /dev/null +++ b/oldabe/constants.py @@ -0,0 +1,16 @@ +import os + +ABE_ROOT = 'abe' +PAYOUTS_DIR = os.path.join(ABE_ROOT, 'payouts') +TRANSACTIONS_FILE = os.path.join(ABE_ROOT, 'transactions.txt') +DEBTS_FILE = os.path.join(ABE_ROOT, 'debts.txt') +ADVANCES_FILE = os.path.join(ABE_ROOT, 'advances.txt') +NONATTRIBUTABLE_PAYMENTS_DIR = os.path.join( + ABE_ROOT, 'payments', 'nonattributable' +) +UNPAYABLE_CONTRIBUTORS_FILE = 'unpayable_contributors.txt' +ITEMIZED_PAYMENTS_FILE = 'itemized_payments.txt' +PRICE_FILE = 'price.txt' +VALUATION_FILE = 'valuation.txt' +ATTRIBUTIONS_FILE = 'attributions.txt' +INSTRUMENTS_FILE = 'instruments.txt' diff --git a/oldabe/money_in.py b/oldabe/money_in.py index f913fc7..b4eacf4 100755 --- a/oldabe/money_in.py +++ b/oldabe/money_in.py @@ -10,78 +10,24 @@ from .models import ( Advance, Attribution, Debt, Payment, ItemizedPayment, Transaction ) - -ABE_ROOT = './abe' -# ABE_ROOT = '.' -PAYMENTS_DIR = os.path.join(ABE_ROOT, 'payments') -NONATTRIBUTABLE_PAYMENTS_DIR = os.path.join( - ABE_ROOT, 'payments', 'nonattributable' +from .utils. import ( + parse_percentage, serialize_proportion, get_rounding_difference, + correct_rounding_error, get_git_revision_short_hash +) +from .constants import ( + ABE_ROOT, PAYOUTS_DIR, TRANSACTIONS_FILE, DEBTS_FILE, ADVANCES_FILE + NONATTRIBUTABLE_PAYMENTS_DIR, UNPAYABLE_CONTRIBUTORS_FILE, + ITEMIZED_PAYMENTS_FILE, PRICE_FILE, VALUATION_FILE, ATTRIBUTIONS_FILE, + INSTRUMENTS_FILE ) -UNPAYABLE_CONTRIBUTORS_FILE = 'unpayable_contributors.txt' -TRANSACTIONS_FILE = 'transactions.txt' -DEBTS_FILE = 'debts.txt' -ADVANCES_FILE = 'advances.txt' -ITEMIZED_PAYMENTS_FILE = 'itemized_payments.txt' -PRICE_FILE = 'price.txt' -VALUATION_FILE = 'valuation.txt' -ATTRIBUTIONS_FILE = 'attributions.txt' -INSTRUMENTS_FILE = 'instruments.txt' + ROUNDING_TOLERANCE = Decimal("0.000001") ACCOUNTING_ZERO = Decimal("0.01") -# TODO - move to utils -def parse_percentage(value): - """ - Translates values expressed in percentage format (75.234) into - their decimal equivalents (0.75234). This effectively divides - the value by 100 without losing precision. - """ - value = re.sub("[^0-9.]", "", value) - value = "00" + value - if "." not in value: - value = value + ".0" - value = re.sub( - r"(?P
\d{2})\.(?P\d+)", r".\g
\g", value
-    )
-    value = Decimal(value)
-    return value
-
-
-# TODO - move to utils
-def serialize_proportion(value):
-    """
-    Translates values expressed in decimal format (0.75234) into
-    their percentage equivalents (75.234). This effectively multiplies
-    the value by 100 without losing precision.
-    """
-    # otherwise, decimal gets translated '2E-7.0'
-    value = format(value, "f")
-    if "." in value:
-        value = value + "00"
-    else:
-        value = value + ".00"
-    value = re.sub(
-        r"(?P
\d)\.(?P\d{2})(?P\d*)",
-        # match a number followed by a decimal point
-        # followed by at least two digits
-        r"\g
\g.\g",
-        # move the decimal point two places to the right
-        value,
-    )
-    # strip leading insignificant zeroes
-    value = value.lstrip("0")
-    # ensure there's a single leading zero if it is
-    # a decimal value less than 1
-    value = re.sub(r"^\.", r"0.", value)
-    if "." in value:
-        # strip trailing insignificant zeroes
-        value = value.rstrip("0")
-        # remove decimal point if whole number
-        value = re.sub(r"\.$", r"", value)
-    return value
-
+# TODO standardize the parsing from text into python objects
+# e.g. Decimal and DateTime
 
 def read_payment(payment_file, attributable=True):
     """
@@ -176,8 +122,6 @@ def find_unprocessed_payments():
     except FileNotFoundError:
         pass
     all_payments = get_all_payments()
-    print("all payments")
-    print(all_payments)
     return [p for p in all_payments if p.file not in recorded_payments]
 
 
@@ -196,7 +140,9 @@ def generate_transactions(amount, attributions, payment_file, commit_hash):
 
 
 def get_existing_itemized_payments():
-    # TODO
+    """
+    Reads itemized payment files and returns all itemized payment objects.
+    """
     itemized_payments = []
     itemized_payments_file = os.path.join(ABE_ROOT, ITEMIZED_PAYMENTS_FILE)
     try:
@@ -270,19 +216,6 @@ def _get_attributions_total(attributions):
     return sum(attributions.values())
 
 
-# TODO - move to utils
-def get_rounding_difference(attributions):
-    """
-    Get the difference of the total of the attributions from 1, which is
-    expected to occur due to finite precision. If the difference exceeds the
-    expected error tolerance, an error is signaled.
-    """
-    total = _get_attributions_total(attributions)
-    difference = total - Decimal("1")
-    assert abs(difference) <= ROUNDING_TOLERANCE
-    return difference
-
-
 def normalize(attributions):
     total_share = sum(share for _, share in attributions.items())
     target_proportion = Decimal("1") / total_share
@@ -290,17 +223,6 @@ def normalize(attributions):
         attributions[email] *= target_proportion
 
 
-# TODO - move to utils
-def correct_rounding_error(attributions, incoming_attribution):
-    """Due to finite precision, the Decimal module will round up or down
-    on the last decimal place. This could result in the aggregate value not
-    quite totaling to 1. This corrects that total by either adding or
-    subtracting the difference from the incoming attribution (by convention).
-    """
-    difference = get_rounding_difference(attributions)
-    attributions[incoming_attribution.email] -= difference
-
-
 def write_attributions(attributions):
     # don't write attributions if they aren't normalized
     assert _get_attributions_total(attributions) == Decimal("1")
@@ -340,18 +262,6 @@ def write_append_advances(advances):
             writer.writerow(astuple(row))
 
 
-# [[ OLD NOTE - still relevant?? ]]
-# TODO - when we write debts
-# 1) read all from the file and transform into a hash, where the key is a 
-#   unique identifier constructed from the email + payment file and the value
-#   is a debt object
-# 2) take the newly created/modified debt objects (all in one list) and iterate
-#   through - searching the full hash for each new debt object - modify if it
-#   was found, and add to the hash if not found
-# 3) convert the hash into a list, ordered by created_at field, then write to
-#   the debts file (completely replacing existing contents)
-
-
 def write_valuation(valuation):
     rounded_valuation = f"{valuation:.2f}"
     valuation_file = os.path.join(ABE_ROOT, VALUATION_FILE)
@@ -394,18 +304,6 @@ def inflate_valuation(valuation, amount):
     return valuation + amount
 
 
-# TODO - move to utils
-# and standardize the parsing from text into python objects
-# e.g. Decimal and DateTime
-def get_git_revision_short_hash() -> str:
-    """From https://stackoverflow.com/a/21901260"""
-    return (
-        subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'])
-        .decode('ascii')
-        .strip()
-    )
-
-
 def read_debts():
     debts_file = os.path.join(ABE_ROOT, DEBTS_FILE)
     debts = []
@@ -601,25 +499,19 @@ def distribute_payment(payment, attributions):
     # 2. pay them off in chronological order (maybe partially)
     # 3. (if leftover) identify unpayable people in the relevant attributions file
     # 4. record debt for each of them according to their attribution
-    print("Listing directory files...")
-    print(os.listdir(ABE_ROOT))
     commit_hash = get_git_revision_short_hash()
     unpayable_contributors = get_unpayable_contributors()
     payable_debts = get_payable_debts(unpayable_contributors, attributions)
     updated_debts, debt_transactions = pay_debts(payable_debts, payment)
     # The "available" amount is what is left over after paying off debts
-    print(f"new payment amount is {payment.amount}")
-    print(f"sum of debt transactions is {sum(t.amount for t in debt_transactions)}")
     available_amount = payment.amount - sum(t.amount for t in debt_transactions)
 
     fresh_debts = []
     equity_transactions = []
     negative_advances = []
     fresh_advances = []
-    print(f"available amount is {available_amount}")
     if available_amount > ACCOUNTING_ZERO:
         amounts_owed = get_amounts_owed(available_amount, attributions)
-        print(f"total of amounts owed is {sum(v for v in amounts_owed.values())}")
         fresh_debts = create_debts(amounts_owed,
                                    unpayable_contributors,
                                    payment.file)
@@ -629,12 +521,10 @@ def distribute_payment(payment, attributions):
         amounts_payable = {email: amount
                            for email, amount in amounts_owed.items()
                            if email not in unpayable_contributors}
-        print(f"total of amounts payable is {sum(v for v in amounts_payable.values())}")
 
         # use the amount owed to each contributor to draw down any advances
         # they may already have and then decrement their amount payable accordingly
         advance_totals = get_sum_of_advances_by_contributor(attributions)
-        print(f"advance totals is {advance_totals}")
         for email, advance_total in advance_totals.items():
             amount_payable = amounts_payable.get(email, 0)
             drawdown_amount = min(advance_total, amount_payable)
@@ -649,7 +539,6 @@ def distribute_payment(payment, attributions):
         # note that these are drawn down amounts and therefore have negative amounts
         # and that's why we take the absolute value here
         redistribution_pot += sum(abs(a.amount) for a in negative_advances)
-        print(f"redistribution_pot is {redistribution_pot}")
 
         # redistribute the pot over all payable contributors - produce fresh advances and add to amounts payable
         if redistribution_pot > ACCOUNTING_ZERO:
@@ -666,8 +555,6 @@ def distribute_payment(payment, attributions):
                                                  commit_hash=commit_hash)
             equity_transactions.append(new_equity_transaction)
 
-    print(f"sum of equity transactions is {sum(t.amount for t in equity_transactions)}")
-    print(f"sum of debt transactions is {sum(t.amount for t in debt_transactions)}")
     debts = updated_debts + fresh_debts
     transactions = equity_transactions + debt_transactions
     advances = negative_advances + fresh_advances
@@ -724,23 +611,16 @@ def process_payments(instruments, attributions):
     new_transactions = []
     new_itemized_payments = []
     unprocessed_payments = find_unprocessed_payments()
-    print("Unprocessed payments", unprocessed_payments)
     for payment in unprocessed_payments:
         # first, process instruments (i.e. pay fees)
         debts, transactions, advances = distribute_payment(payment, instruments)
-        print(f"payment amount BEFORE deducting fees is {payment.amount}")
         new_transactions += transactions
         new_debts += debts
         new_advances += advances
         fees_paid_out = sum(t.amount for t in transactions)
-        print(f"fees paid out are {fees_paid_out}")
-        print(f"total transaction amounts {sum(t.amount for t in transactions)}")
-        print(f"total debt amounts {sum(d.amount for d in debts)}")
-        print(f"total advance amounts {sum(a.amount for a in advances)}")
         # deduct the amount paid out to instruments before
         # processing it for attributions
         payment.amount -= fees_paid_out
-        print(f"payment amount AFTER deducting fees is {payment.amount}")
         new_itemized_payments.append(
             _create_itemized_payment(payment, fees_paid_out)
         )
@@ -766,7 +646,6 @@ def process_payments_and_record_updates():
     """
     instruments = read_attributions(INSTRUMENTS_FILE, validate=False)
     attributions = read_attributions(ATTRIBUTIONS_FILE)
-    print("Attributions are:", attributions)
 
     (
         debts,
@@ -776,7 +655,6 @@ def process_payments_and_record_updates():
         advances,
     ) = process_payments(instruments, attributions)
 
-    print("Transactions are:", transactions)
     # we only write the changes to disk at the end
     # so that if any errors are encountered, no
     # changes are made.
diff --git a/oldabe/money_out.py b/oldabe/money_out.py
index 41c7406..542ed8a 100755
--- a/oldabe/money_out.py
+++ b/oldabe/money_out.py
@@ -7,12 +7,9 @@
 import re
 from collections import defaultdict
 from decimal import Decimal, getcontext
-
-ABE_ROOT = 'abe'
-PAYOUTS_DIR = os.path.join(ABE_ROOT, 'payouts')
-TRANSACTIONS_FILE = os.path.join(ABE_ROOT, 'transactions.txt')
-DEBTS_FILE = os.path.join(ABE_ROOT, 'debts.txt')
-ADVANCES_FILE = os.path.join(ABE_ROOT, 'advances.txt')
+from .constants import (
+    ABE_ROOT, PAYOUTS_DIR, TRANSACTIONS_FILE, DEBTS_FILE, ADVANCES_FILE
+)
 
 
 def read_transaction_amounts():
diff --git a/oldabe/utils.py b/oldabe/utils.py
new file mode 100644
index 0000000..6f2b7f1
--- /dev/null
+++ b/oldabe/utils.py
@@ -0,0 +1,84 @@
+from decimal import Decimal
+import re
+import subprocess
+
+
+def parse_percentage(value):
+    """
+    Translates values expressed in percentage format (75.234) into
+    their decimal equivalents (0.75234). This effectively divides
+    the value by 100 without losing precision.
+    """
+    value = re.sub("[^0-9.]", "", value)
+    value = "00" + value
+    if "." not in value:
+        value = value + ".0"
+    value = re.sub(
+        r"(?P
\d{2})\.(?P\d+)", r".\g
\g", value
+    )
+    value = Decimal(value)
+    return value
+
+
+def serialize_proportion(value):
+    """
+    Translates values expressed in decimal format (0.75234) into
+    their percentage equivalents (75.234). This effectively multiplies
+    the value by 100 without losing precision.
+    """
+    # otherwise, decimal gets translated '2E-7.0'
+    value = format(value, "f")
+    if "." in value:
+        value = value + "00"
+    else:
+        value = value + ".00"
+    value = re.sub(
+        r"(?P
\d)\.(?P\d{2})(?P\d*)",
+        # match a number followed by a decimal point
+        # followed by at least two digits
+        r"\g
\g.\g",
+        # move the decimal point two places to the right
+        value,
+    )
+    # strip leading insignificant zeroes
+    value = value.lstrip("0")
+    # ensure there's a single leading zero if it is
+    # a decimal value less than 1
+    value = re.sub(r"^\.", r"0.", value)
+    if "." in value:
+        # strip trailing insignificant zeroes
+        value = value.rstrip("0")
+        # remove decimal point if whole number
+        value = re.sub(r"\.$", r"", value)
+    return value
+
+
+def get_rounding_difference(attributions):
+    """
+    Get the difference of the total of the attributions from 1, which is
+    expected to occur due to finite precision. If the difference exceeds the
+    expected error tolerance, an error is signaled.
+    """
+    total = _get_attributions_total(attributions)
+    difference = total - Decimal("1")
+    assert abs(difference) <= ROUNDING_TOLERANCE
+    return difference
+
+
+def correct_rounding_error(attributions, incoming_attribution):
+    """Due to finite precision, the Decimal module will round up or down
+    on the last decimal place. This could result in the aggregate value not
+    quite totaling to 1. This corrects that total by either adding or
+    subtracting the difference from the incoming attribution (by convention).
+    """
+    difference = get_rounding_difference(attributions)
+    attributions[incoming_attribution.email] -= difference
+
+
+def get_git_revision_short_hash() -> str:
+    """From https://stackoverflow.com/a/21901260"""
+    return (
+        subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'])
+        .decode('ascii')
+        .strip()
+    )
diff --git a/tests/unit/money_in_test.py b/tests/unit/money_in_test.py
index 4fb1c90..44120c7 100644
--- a/tests/unit/money_in_test.py
+++ b/tests/unit/money_in_test.py
@@ -1,18 +1,15 @@
 from decimal import Decimal
 from oldabe.money_in import (
     calculate_incoming_investment,
-    parse_percentage,
-    serialize_proportion,
     total_amount_paid_to_project,
     calculate_incoming_attribution,
-    correct_rounding_error,
     generate_transactions,
-    get_rounding_difference,
     ROUNDING_TOLERANCE,
     renormalize,
     inflate_valuation,
     process_payments,
 )
+from oldabe.utils import correct_rounding_error
 from oldabe.models import Attribution, Payment
 import pytest
 from unittest.mock import patch
@@ -29,61 +26,6 @@
 )  # noqa
 
 
-class TestParsePercentage:
-    def test_an_integer(self):
-        assert parse_percentage('75') == Decimal('0.75')
-
-    def test_non_integer_greater_than_1(self):
-        assert parse_percentage('75.334455') == Decimal('0.75334455')
-
-    def test_non_integer_less_than_1(self):
-        assert parse_percentage('0.334455') == Decimal('0.00334455')
-
-    def test_decimal_places_at_precision_context(self):
-        assert parse_percentage('5.1234567891') == Decimal('0.051234567891')
-
-    def test_very_small_number(self):
-        assert parse_percentage('0.000001') == Decimal('0.00000001')
-
-    def test_0(self):
-        assert parse_percentage('0') == Decimal('0')
-
-    def test_0_point_0(self):
-        assert parse_percentage('0.0') == Decimal('0')
-
-    def test_100(self):
-        assert parse_percentage('100') == Decimal('1')
-
-    def test_100_point_0(self):
-        assert parse_percentage('100.0') == Decimal('1')
-
-
-class TestSerializeProportion:
-    def test_0(self):
-        assert serialize_proportion(Decimal('0')) == '0'
-
-    def test_0_point_0(self):
-        assert serialize_proportion(Decimal('0.0')) == '0'
-
-    def test_1(self):
-        assert serialize_proportion(Decimal('1')) == '100'
-
-    def test_1_point_0(self):
-        assert serialize_proportion(Decimal('1.0')) == '100'
-
-    def test_almost_1(self):
-        assert serialize_proportion(Decimal('0.9523452')) == '95.23452'
-
-    def test_low_precision_number(self):
-        assert serialize_proportion(Decimal('0.2')) == '20'
-
-    def test_very_small_number(self):
-        assert serialize_proportion(Decimal('0.0000002')) == '0.00002'
-
-    def test_decimal_places_at_precision_context(self):
-        assert serialize_proportion(Decimal('0.1234567891')) == '12.34567891'
-
-
 class TestGenerateTransactions:
     def test_zero_amount(self, normalized_attributions):
         with pytest.raises(AssertionError):
@@ -239,46 +181,6 @@ def test_large_incoming_investment(self, normalized_attributions):
         )
 
 
-class TestGetRoundingDifference:
-    def test_attributions_already_one(self, normalized_attributions):
-        attributions = normalized_attributions
-        test_diff = Decimal("0")
-        difference = get_rounding_difference(attributions)
-        assert difference == test_diff
-
-    def test_attributions_exceed_below_tolerance(
-        self, normalized_attributions
-    ):
-        attributions = normalized_attributions
-        test_diff = ROUNDING_TOLERANCE / Decimal("2")
-        attributions['c@d.com'] = test_diff
-        difference = get_rounding_difference(attributions)
-        assert difference == test_diff
-
-    def test_attributions_exceed_above_tolerance(
-        self, normalized_attributions
-    ):
-        attributions = normalized_attributions
-        test_diff = ROUNDING_TOLERANCE * Decimal("2")
-        attributions['c@d.com'] = test_diff
-        with pytest.raises(AssertionError):
-            _ = get_rounding_difference(attributions)
-
-    def test_attributions_exceed_by_tolerance(self, normalized_attributions):
-        attributions = normalized_attributions
-        test_diff = ROUNDING_TOLERANCE
-        attributions['c@d.com'] = test_diff
-        difference = get_rounding_difference(attributions)
-        assert difference == test_diff
-
-    def test_attributions_below_one(self, normalized_attributions):
-        attributions = normalized_attributions
-        test_diff = ROUNDING_TOLERANCE / Decimal("2")
-        attributions['b@c.com'] -= test_diff
-        difference = get_rounding_difference(attributions)
-        assert difference == -test_diff
-
-
 class TestRenormalize:
     @pytest.mark.parametrize(
         "incoming_attribution, renormalized_attributions",
diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py
new file mode 100644
index 0000000..008ec97
--- /dev/null
+++ b/tests/unit/utils_test.py
@@ -0,0 +1,126 @@
+from decimal import Decimal
+from oldabe.money_in import (
+    calculate_incoming_investment,
+    total_amount_paid_to_project,
+    calculate_incoming_attribution,
+    generate_transactions,
+    ROUNDING_TOLERANCE,
+    renormalize,
+    inflate_valuation,
+    process_payments,
+)
+from oldabe.utils import (
+    parse_percentage,
+    serialize_proportion,
+    get_rounding_difference,
+    correct_rounding_error,
+)
+from oldabe.models import Attribution, Payment
+import pytest
+from unittest.mock import patch
+from .utils import call_sequence
+from .fixtures import (
+    instruments,
+    normalized_attributions,
+    excess_attributions,
+    shortfall_attributions,
+    empty_attributions,
+    single_contributor_attributions,
+    itemized_payments,
+    new_itemized_payments,
+)  # noqa
+
+
+class TestParsePercentage:
+    def test_an_integer(self):
+        assert parse_percentage('75') == Decimal('0.75')
+
+    def test_non_integer_greater_than_1(self):
+        assert parse_percentage('75.334455') == Decimal('0.75334455')
+
+    def test_non_integer_less_than_1(self):
+        assert parse_percentage('0.334455') == Decimal('0.00334455')
+
+    def test_decimal_places_at_precision_context(self):
+        assert parse_percentage('5.1234567891') == Decimal('0.051234567891')
+
+    def test_very_small_number(self):
+        assert parse_percentage('0.000001') == Decimal('0.00000001')
+
+    def test_0(self):
+        assert parse_percentage('0') == Decimal('0')
+
+    def test_0_point_0(self):
+        assert parse_percentage('0.0') == Decimal('0')
+
+    def test_100(self):
+        assert parse_percentage('100') == Decimal('1')
+
+    def test_100_point_0(self):
+        assert parse_percentage('100.0') == Decimal('1')
+
+
+class TestSerializeProportion:
+    def test_0(self):
+        assert serialize_proportion(Decimal('0')) == '0'
+
+    def test_0_point_0(self):
+        assert serialize_proportion(Decimal('0.0')) == '0'
+
+    def test_1(self):
+        assert serialize_proportion(Decimal('1')) == '100'
+
+    def test_1_point_0(self):
+        assert serialize_proportion(Decimal('1.0')) == '100'
+
+    def test_almost_1(self):
+        assert serialize_proportion(Decimal('0.9523452')) == '95.23452'
+
+    def test_low_precision_number(self):
+        assert serialize_proportion(Decimal('0.2')) == '20'
+
+    def test_very_small_number(self):
+        assert serialize_proportion(Decimal('0.0000002')) == '0.00002'
+
+    def test_decimal_places_at_precision_context(self):
+        assert serialize_proportion(Decimal('0.1234567891')) == '12.34567891'
+
+
+class TestGetRoundingDifference:
+    def test_attributions_already_one(self, normalized_attributions):
+        attributions = normalized_attributions
+        test_diff = Decimal("0")
+        difference = get_rounding_difference(attributions)
+        assert difference == test_diff
+
+    def test_attributions_exceed_below_tolerance(
+        self, normalized_attributions
+    ):
+        attributions = normalized_attributions
+        test_diff = ROUNDING_TOLERANCE / Decimal("2")
+        attributions['c@d.com'] = test_diff
+        difference = get_rounding_difference(attributions)
+        assert difference == test_diff
+
+    def test_attributions_exceed_above_tolerance(
+        self, normalized_attributions
+    ):
+        attributions = normalized_attributions
+        test_diff = ROUNDING_TOLERANCE * Decimal("2")
+        attributions['c@d.com'] = test_diff
+        with pytest.raises(AssertionError):
+            _ = get_rounding_difference(attributions)
+
+    def test_attributions_exceed_by_tolerance(self, normalized_attributions):
+        attributions = normalized_attributions
+        test_diff = ROUNDING_TOLERANCE
+        attributions['c@d.com'] = test_diff
+        difference = get_rounding_difference(attributions)
+        assert difference == test_diff
+
+    def test_attributions_below_one(self, normalized_attributions):
+        attributions = normalized_attributions
+        test_diff = ROUNDING_TOLERANCE / Decimal("2")
+        attributions['b@c.com'] -= test_diff
+        difference = get_rounding_difference(attributions)
+        assert difference == -test_diff

From 14dc797544f0571a6a5d664e5dfb53f17121e62d Mon Sep 17 00:00:00 2001
From: apromessi 
Date: Tue, 14 May 2024 21:09:23 -0700
Subject: [PATCH 2/9] fix syntax

---
 oldabe/money_in.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/oldabe/money_in.py b/oldabe/money_in.py
index b4eacf4..3cbf076 100755
--- a/oldabe/money_in.py
+++ b/oldabe/money_in.py
@@ -10,7 +10,7 @@
 from .models import (
     Advance, Attribution, Debt, Payment, ItemizedPayment, Transaction
 )
-from .utils. import (
+from .utils import (
     parse_percentage, serialize_proportion, get_rounding_difference,
     correct_rounding_error, get_git_revision_short_hash
 )

From c9d62b6d9f2465af37454425141cdeff61a9f5fc Mon Sep 17 00:00:00 2001
From: apromessi 
Date: Tue, 14 May 2024 21:10:56 -0700
Subject: [PATCH 3/9] fix syntax

---
 oldabe/money_in.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/oldabe/money_in.py b/oldabe/money_in.py
index 3cbf076..db9dd67 100755
--- a/oldabe/money_in.py
+++ b/oldabe/money_in.py
@@ -15,7 +15,7 @@
     correct_rounding_error, get_git_revision_short_hash
 )
 from .constants import (
-    ABE_ROOT, PAYOUTS_DIR, TRANSACTIONS_FILE, DEBTS_FILE, ADVANCES_FILE
+    ABE_ROOT, PAYOUTS_DIR, TRANSACTIONS_FILE, DEBTS_FILE, ADVANCES_FILE,
     NONATTRIBUTABLE_PAYMENTS_DIR, UNPAYABLE_CONTRIBUTORS_FILE,
     ITEMIZED_PAYMENTS_FILE, PRICE_FILE, VALUATION_FILE, ATTRIBUTIONS_FILE,
     INSTRUMENTS_FILE

From 35e492442cc73f6b6b9cfaf4ed718b46c2d07b44 Mon Sep 17 00:00:00 2001
From: apromessi 
Date: Tue, 14 May 2024 21:12:25 -0700
Subject: [PATCH 4/9] fix syntax

---
 oldabe/money_in.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/oldabe/money_in.py b/oldabe/money_in.py
index db9dd67..c23c477 100755
--- a/oldabe/money_in.py
+++ b/oldabe/money_in.py
@@ -15,8 +15,8 @@
     correct_rounding_error, get_git_revision_short_hash
 )
 from .constants import (
-    ABE_ROOT, PAYOUTS_DIR, TRANSACTIONS_FILE, DEBTS_FILE, ADVANCES_FILE,
-    NONATTRIBUTABLE_PAYMENTS_DIR, UNPAYABLE_CONTRIBUTORS_FILE,
+    ABE_ROOT, PAYMENTS_DIR, PAYOUTS_DIR, TRANSACTIONS_FILE, DEBTS_FILE,
+    ADVANCES_FILE, NONATTRIBUTABLE_PAYMENTS_DIR, UNPAYABLE_CONTRIBUTORS_FILE,
     ITEMIZED_PAYMENTS_FILE, PRICE_FILE, VALUATION_FILE, ATTRIBUTIONS_FILE,
     INSTRUMENTS_FILE
 )

From e28fb9cb5d51c619846cc59042fc426274c023a9 Mon Sep 17 00:00:00 2001
From: apromessi 
Date: Tue, 14 May 2024 21:13:53 -0700
Subject: [PATCH 5/9] fix syntax

---
 oldabe/constants.py | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/oldabe/constants.py b/oldabe/constants.py
index 5a4040a..4c6d988 100644
--- a/oldabe/constants.py
+++ b/oldabe/constants.py
@@ -2,12 +2,14 @@
 
 ABE_ROOT = 'abe'
 PAYOUTS_DIR = os.path.join(ABE_ROOT, 'payouts')
-TRANSACTIONS_FILE = os.path.join(ABE_ROOT, 'transactions.txt')
-DEBTS_FILE = os.path.join(ABE_ROOT, 'debts.txt')
-ADVANCES_FILE = os.path.join(ABE_ROOT, 'advances.txt')
+PAYMENTS_DIR = os.path.join(ABE_ROOT, 'payments')
 NONATTRIBUTABLE_PAYMENTS_DIR = os.path.join(
     ABE_ROOT, 'payments', 'nonattributable'
 )
+
+TRANSACTIONS_FILE = os.path.join(ABE_ROOT, 'transactions.txt')
+DEBTS_FILE = os.path.join(ABE_ROOT, 'debts.txt')
+ADVANCES_FILE = os.path.join(ABE_ROOT, 'advances.txt')
 UNPAYABLE_CONTRIBUTORS_FILE = 'unpayable_contributors.txt'
 ITEMIZED_PAYMENTS_FILE = 'itemized_payments.txt'
 PRICE_FILE = 'price.txt'

From d0d6d9693eb69cc2a70024254e2af1edc32929c3 Mon Sep 17 00:00:00 2001
From: apromessi 
Date: Tue, 14 May 2024 21:22:46 -0700
Subject: [PATCH 6/9] use money_in value of ABE_ROOT for constants

---
 oldabe/constants.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/oldabe/constants.py b/oldabe/constants.py
index 4c6d988..5a7210d 100644
--- a/oldabe/constants.py
+++ b/oldabe/constants.py
@@ -1,6 +1,6 @@
 import os
 
-ABE_ROOT = 'abe'
+ABE_ROOT = './abe'
 PAYOUTS_DIR = os.path.join(ABE_ROOT, 'payouts')
 PAYMENTS_DIR = os.path.join(ABE_ROOT, 'payments')
 NONATTRIBUTABLE_PAYMENTS_DIR = os.path.join(

From aa9dd3a7879e149130a1341d44d10014beb70a2a Mon Sep 17 00:00:00 2001
From: apromessi 
Date: Tue, 14 May 2024 23:45:08 -0700
Subject: [PATCH 7/9] standardize file construction for constants

---
 oldabe/constants.py |  8 ++++----
 oldabe/money_in.py  | 39 +++++++++++++--------------------------
 2 files changed, 17 insertions(+), 30 deletions(-)

diff --git a/oldabe/constants.py b/oldabe/constants.py
index 5a7210d..a680e8e 100644
--- a/oldabe/constants.py
+++ b/oldabe/constants.py
@@ -10,9 +10,9 @@
 TRANSACTIONS_FILE = os.path.join(ABE_ROOT, 'transactions.txt')
 DEBTS_FILE = os.path.join(ABE_ROOT, 'debts.txt')
 ADVANCES_FILE = os.path.join(ABE_ROOT, 'advances.txt')
-UNPAYABLE_CONTRIBUTORS_FILE = 'unpayable_contributors.txt'
-ITEMIZED_PAYMENTS_FILE = 'itemized_payments.txt'
-PRICE_FILE = 'price.txt'
-VALUATION_FILE = 'valuation.txt'
+UNPAYABLE_CONTRIBUTORS_FILE = os.path.join(ABE_ROOT, 'unpayable_contributors.txt')
+ITEMIZED_PAYMENTS_FILE = os.path.join(ABE_ROOT, 'itemized_payments.txt')
+PRICE_FILE = os.path.join(ABE_ROOT, 'price.txt')
+VALUATION_FILE = os.path.join(ABE_ROOT, 'valuation.txt')
 ATTRIBUTIONS_FILE = 'attributions.txt'
 INSTRUMENTS_FILE = 'instruments.txt'
diff --git a/oldabe/money_in.py b/oldabe/money_in.py
index c23c477..b57ad7d 100755
--- a/oldabe/money_in.py
+++ b/oldabe/money_in.py
@@ -44,8 +44,7 @@ def read_payment(payment_file, attributable=True):
 
 
 def read_price():
-    price_file = os.path.join(ABE_ROOT, PRICE_FILE)
-    with open(price_file) as f:
+    with open(PRICE_FILE) as f:
         price = f.readline()
         price = Decimal(re.sub("[^0-9.]", "", price))
         return price
@@ -54,8 +53,7 @@ def read_price():
 # note that commas are used as a decimal separator in some languages
 # (e.g. Spain Spanish), so that would need to be handled at some point
 def read_valuation():
-    valuation_file = os.path.join(ABE_ROOT, VALUATION_FILE)
-    with open(valuation_file) as f:
+    with open(VALUATION_FILE) as f:
         valuation = f.readline()
         valuation = Decimal(re.sub("[^0-9.]", "", valuation))
         return valuation
@@ -108,9 +106,8 @@ def find_unprocessed_payments():
     Return type: list of Payment objects
     """
     recorded_payments = set()
-    transactions_file = os.path.join(ABE_ROOT, TRANSACTIONS_FILE)
     try:
-        with open(transactions_file) as f:
+        with open(TRANSACTIONS_FILE) as f:
             for (
                 _email,
                 _amount,
@@ -144,9 +141,8 @@ def get_existing_itemized_payments():
     Reads itemized payment files and returns all itemized payment objects.
     """
     itemized_payments = []
-    itemized_payments_file = os.path.join(ABE_ROOT, ITEMIZED_PAYMENTS_FILE)
     try:
-        with open(itemized_payments_file) as f:
+        with open(ITEMIZED_PAYMENTS_FILE) as f:
             for (
                 email,
                 fee_amount,
@@ -231,32 +227,28 @@ def write_attributions(attributions):
         (email, serialize_proportion(share))
         for email, share in attributions.items()
     ]
-    attributions_file = os.path.join(ABE_ROOT, ATTRIBUTIONS_FILE)
-    with open(attributions_file, 'w') as f:
+    with open(ATTRIBUTIONS_FILE, 'w') as f:
         writer = csv.writer(f)
         for row in attributions:
             writer.writerow(row)
 
 
 def write_append_transactions(transactions):
-    transactions_file = os.path.join(ABE_ROOT, TRANSACTIONS_FILE)
-    with open(transactions_file, 'a') as f:
+    with open(TRANSACTIONS_FILE, 'a') as f:
         writer = csv.writer(f)
         for row in transactions:
             writer.writerow(astuple(row))
 
 
 def write_append_itemized_payments(itemized_payments):
-    itemized_payments_file = os.path.join(ABE_ROOT, ITEMIZED_PAYMENTS_FILE)
-    with open(itemized_payments_file, 'a') as f:
+    with open(ITEMIZED_PAYMENTS_FILE, 'a') as f:
         writer = csv.writer(f)
         for row in itemized_payments:
             writer.writerow(astuple(row))
 
 
 def write_append_advances(advances):
-    advances_file = os.path.join(ABE_ROOT, ADVANCES_FILE)
-    with open(advances_file, 'a') as f:
+    with open(ADVANCES_FILE, 'a') as f:
         writer = csv.writer(f)
         for row in advances:
             writer.writerow(astuple(row))
@@ -264,8 +256,7 @@ def write_append_advances(advances):
 
 def write_valuation(valuation):
     rounded_valuation = f"{valuation:.2f}"
-    valuation_file = os.path.join(ABE_ROOT, VALUATION_FILE)
-    with open(valuation_file, 'w') as f:
+    with open(VALUATION_FILE, 'w') as f:
         writer = csv.writer(f)
         writer.writerow((rounded_valuation,))
 
@@ -305,10 +296,9 @@ def inflate_valuation(valuation, amount):
 
 
 def read_debts():
-    debts_file = os.path.join(ABE_ROOT, DEBTS_FILE)
     debts = []
     try:
-        with open(debts_file) as f:
+        with open(DEBTS_FILE) as f:
             for (
                 email,
                 amount,
@@ -325,10 +315,9 @@ def read_debts():
 
 
 def read_advances(attributions):
-    advances_file = os.path.join(ABE_ROOT, ADVANCES_FILE)
     advances = defaultdict(list)
     try:
-        with open(advances_file) as f:
+        with open(ADVANCES_FILE) as f:
             for (
                 email,
                 amount,
@@ -395,10 +384,9 @@ def get_unpayable_contributors():
     Read the unpayable_contributors file to get the list of contributors who
     are unpayable.
     """
-    unpayable_contributors_file = os.path.join(ABE_ROOT, UNPAYABLE_CONTRIBUTORS_FILE)
     contributors = []
     try:
-        with open(unpayable_contributors_file) as f:
+        with open(UNPAYABLE_CONTRIBUTORS_FILE) as f:
             for contributor in f:
                 contributor = contributor.strip()
                 if contributor:
@@ -436,8 +424,7 @@ def write_debts(processed_debts):
     """
     existing_debts = read_debts()
     processed_debts_hash = {debt.key(): debt for debt in processed_debts}
-    debts_file = os.path.join(ABE_ROOT, DEBTS_FILE)
-    with open(debts_file, 'w') as f:
+    with open(DEBTS_FILE, 'w') as f:
         writer = csv.writer(f)
         for existing_debt in existing_debts:
             # if the existing debt has been processed, write the processed version

From 8b0c1bd3492f1e3978e1ebb62aef7789323003f6 Mon Sep 17 00:00:00 2001
From: apromessi 
Date: Wed, 15 May 2024 00:10:17 -0700
Subject: [PATCH 8/9] fix white space in test files, fix attributions file
 name, move attributions validation to utils

---
 oldabe/money_in.py            | 16 ++++++----------
 oldabe/utils.py               |  9 +++++++++
 tests/integration/fixtures.py | 18 +++++++++---------
 3 files changed, 24 insertions(+), 19 deletions(-)

diff --git a/oldabe/money_in.py b/oldabe/money_in.py
index b57ad7d..c342774 100755
--- a/oldabe/money_in.py
+++ b/oldabe/money_in.py
@@ -12,7 +12,8 @@
 )
 from .utils import (
     parse_percentage, serialize_proportion, get_rounding_difference,
-    correct_rounding_error, get_git_revision_short_hash
+    correct_rounding_error, get_git_revision_short_hash,
+    assert_attributions_normalized
 )
 from .constants import (
     ABE_ROOT, PAYMENTS_DIR, PAYOUTS_DIR, TRANSACTIONS_FILE, DEBTS_FILE,
@@ -22,10 +23,8 @@
 )
 
 
-ROUNDING_TOLERANCE = Decimal("0.000001")
 ACCOUNTING_ZERO = Decimal("0.01")
 
-
 # TODO standardize the parsing from text into python objects
 # e.g. Decimal and DateTime
 
@@ -69,7 +68,7 @@ def read_attributions(attributions_filename, validate=True):
                 email = email.strip()
                 attributions[email] = parse_percentage(percentage)
     if validate:
-        assert _get_attributions_total(attributions) == Decimal("1")
+        assert_attributions_normalized(attributions)
     return attributions
 
 
@@ -208,10 +207,6 @@ def calculate_incoming_attribution(
         return None
 
 
-def _get_attributions_total(attributions):
-    return sum(attributions.values())
-
-
 def normalize(attributions):
     total_share = sum(share for _, share in attributions.items())
     target_proportion = Decimal("1") / total_share
@@ -221,13 +216,14 @@ def normalize(attributions):
 
 def write_attributions(attributions):
     # don't write attributions if they aren't normalized
-    assert _get_attributions_total(attributions) == Decimal("1")
+    assert_attributions_normalized(attributions)
     # format for output as percentages
     attributions = [
         (email, serialize_proportion(share))
         for email, share in attributions.items()
     ]
-    with open(ATTRIBUTIONS_FILE, 'w') as f:
+    attributions_file = os.path.join(ABE_ROOT, ATTRIBUTIONS_FILE)
+    with open(attributions_file, 'w') as f:
         writer = csv.writer(f)
         for row in attributions:
             writer.writerow(row)
diff --git a/oldabe/utils.py b/oldabe/utils.py
index 6f2b7f1..99a62dd 100644
--- a/oldabe/utils.py
+++ b/oldabe/utils.py
@@ -2,6 +2,7 @@
 import re
 import subprocess
 
+ROUNDING_TOLERANCE = Decimal("0.000001")
 
 def parse_percentage(value):
     """
@@ -82,3 +83,11 @@ def get_git_revision_short_hash() -> str:
         .decode('ascii')
         .strip()
     )
+
+
+def assert_attributions_normalized(attributions):
+    assert _get_attributions_total(attributions) == Decimal("1")
+
+
+def _get_attributions_total(attributions):
+    return sum(attributions.values())
diff --git a/tests/integration/fixtures.py b/tests/integration/fixtures.py
index 2f6c1e8..7cda069 100644
--- a/tests/integration/fixtures.py
+++ b/tests/integration/fixtures.py
@@ -5,13 +5,13 @@
 def abe_fs(fs):
     fs.create_file("./abe/price.txt", contents="10")
     fs.create_file("./abe/valuation.txt", contents="100000")
-    fs.create_file("./abe/instruments.txt", contents="""
-        old abe,1
-        DIA,5
-        """)
-    fs.create_file("./abe/attributions.txt", contents="""
-       sid,50
-       jair,30
-       ariana,20
-       """)
+    fs.create_file("./abe/instruments.txt", contents=(
+        "old abe,1\n"
+        "DIA,5\n"
+        ))
+    fs.create_file("./abe/attributions.txt", contents=(
+       "sid,50\n"
+       "jair,30\n"
+       "ariana,20\n"
+       ))
     return fs

From 1b41f957e28bde165a1dd5d46e9f7b5832de0adf Mon Sep 17 00:00:00 2001
From: apromessi 
Date: Thu, 16 May 2024 20:38:06 -0700
Subject: [PATCH 9/9] put utils into separate modules

---
 oldabe/accounting_utils.py          |  33 ++++++++
 oldabe/git.py                       |   9 ++
 oldabe/money_in.py                  |   7 +-
 oldabe/{utils.py => parsing.py}     |  42 ----------
 tests/unit/accounting_utils_test.py |  48 +++++++++++
 tests/unit/money_in_test.py         |   3 +-
 tests/unit/parsing_test.py          |  61 ++++++++++++++
 tests/unit/utils_test.py            | 126 ----------------------------
 8 files changed, 156 insertions(+), 173 deletions(-)
 create mode 100644 oldabe/accounting_utils.py
 create mode 100644 oldabe/git.py
 rename oldabe/{utils.py => parsing.py} (53%)
 create mode 100644 tests/unit/accounting_utils_test.py
 create mode 100644 tests/unit/parsing_test.py
 delete mode 100644 tests/unit/utils_test.py

diff --git a/oldabe/accounting_utils.py b/oldabe/accounting_utils.py
new file mode 100644
index 0000000..7275b67
--- /dev/null
+++ b/oldabe/accounting_utils.py
@@ -0,0 +1,33 @@
+from decimal import Decimal
+
+ROUNDING_TOLERANCE = Decimal("0.000001")
+
+
+def get_rounding_difference(attributions):
+    """
+    Get the difference of the total of the attributions from 1, which is
+    expected to occur due to finite precision. If the difference exceeds the
+    expected error tolerance, an error is signaled.
+    """
+    total = _get_attributions_total(attributions)
+    difference = total - Decimal("1")
+    assert abs(difference) <= ROUNDING_TOLERANCE
+    return difference
+
+
+def correct_rounding_error(attributions, incoming_attribution):
+    """Due to finite precision, the Decimal module will round up or down
+    on the last decimal place. This could result in the aggregate value not
+    quite totaling to 1. This corrects that total by either adding or
+    subtracting the difference from the incoming attribution (by convention).
+    """
+    difference = get_rounding_difference(attributions)
+    attributions[incoming_attribution.email] -= difference
+
+
+def assert_attributions_normalized(attributions):
+    assert _get_attributions_total(attributions) == Decimal("1")
+
+
+def _get_attributions_total(attributions):
+    return sum(attributions.values())
diff --git a/oldabe/git.py b/oldabe/git.py
new file mode 100644
index 0000000..3c6638e
--- /dev/null
+++ b/oldabe/git.py
@@ -0,0 +1,9 @@
+import subprocess
+
+def get_git_revision_short_hash() -> str:
+    """From https://stackoverflow.com/a/21901260"""
+    return (
+        subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'])
+        .decode('ascii')
+        .strip()
+    )
diff --git a/oldabe/money_in.py b/oldabe/money_in.py
index c342774..01713c6 100755
--- a/oldabe/money_in.py
+++ b/oldabe/money_in.py
@@ -10,9 +10,10 @@
 from .models import (
     Advance, Attribution, Debt, Payment, ItemizedPayment, Transaction
 )
-from .utils import (
-    parse_percentage, serialize_proportion, get_rounding_difference,
-    correct_rounding_error, get_git_revision_short_hash,
+from .parsing import parse_percentage, serialize_proportion
+from .git import get_git_revision_short_hash
+from .accounting_utils import (
+    get_rounding_difference, correct_rounding_error,
     assert_attributions_normalized
 )
 from .constants import (
diff --git a/oldabe/utils.py b/oldabe/parsing.py
similarity index 53%
rename from oldabe/utils.py
rename to oldabe/parsing.py
index 99a62dd..c79ddc8 100644
--- a/oldabe/utils.py
+++ b/oldabe/parsing.py
@@ -1,8 +1,5 @@
 from decimal import Decimal
 import re
-import subprocess
-
-ROUNDING_TOLERANCE = Decimal("0.000001")
 
 def parse_percentage(value):
     """
@@ -52,42 +49,3 @@ def serialize_proportion(value):
         # remove decimal point if whole number
         value = re.sub(r"\.$", r"", value)
     return value
-
-
-def get_rounding_difference(attributions):
-    """
-    Get the difference of the total of the attributions from 1, which is
-    expected to occur due to finite precision. If the difference exceeds the
-    expected error tolerance, an error is signaled.
-    """
-    total = _get_attributions_total(attributions)
-    difference = total - Decimal("1")
-    assert abs(difference) <= ROUNDING_TOLERANCE
-    return difference
-
-
-def correct_rounding_error(attributions, incoming_attribution):
-    """Due to finite precision, the Decimal module will round up or down
-    on the last decimal place. This could result in the aggregate value not
-    quite totaling to 1. This corrects that total by either adding or
-    subtracting the difference from the incoming attribution (by convention).
-    """
-    difference = get_rounding_difference(attributions)
-    attributions[incoming_attribution.email] -= difference
-
-
-def get_git_revision_short_hash() -> str:
-    """From https://stackoverflow.com/a/21901260"""
-    return (
-        subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'])
-        .decode('ascii')
-        .strip()
-    )
-
-
-def assert_attributions_normalized(attributions):
-    assert _get_attributions_total(attributions) == Decimal("1")
-
-
-def _get_attributions_total(attributions):
-    return sum(attributions.values())
diff --git a/tests/unit/accounting_utils_test.py b/tests/unit/accounting_utils_test.py
new file mode 100644
index 0000000..8fde629
--- /dev/null
+++ b/tests/unit/accounting_utils_test.py
@@ -0,0 +1,48 @@
+import pytest
+from decimal import Decimal
+from oldabe.accounting_utils import (
+    get_rounding_difference,
+    correct_rounding_error,
+    ROUNDING_TOLERANCE,
+)
+from .fixtures import normalized_attributions  # noqa
+
+
+class TestGetRoundingDifference:
+    def test_attributions_already_one(self, normalized_attributions):
+        attributions = normalized_attributions
+        test_diff = Decimal("0")
+        difference = get_rounding_difference(attributions)
+        assert difference == test_diff
+
+    def test_attributions_exceed_below_tolerance(
+        self, normalized_attributions
+    ):
+        attributions = normalized_attributions
+        test_diff = ROUNDING_TOLERANCE / Decimal("2")
+        attributions['c@d.com'] = test_diff
+        difference = get_rounding_difference(attributions)
+        assert difference == test_diff
+
+    def test_attributions_exceed_above_tolerance(
+        self, normalized_attributions
+    ):
+        attributions = normalized_attributions
+        test_diff = ROUNDING_TOLERANCE * Decimal("2")
+        attributions['c@d.com'] = test_diff
+        with pytest.raises(AssertionError):
+            _ = get_rounding_difference(attributions)
+
+    def test_attributions_exceed_by_tolerance(self, normalized_attributions):
+        attributions = normalized_attributions
+        test_diff = ROUNDING_TOLERANCE
+        attributions['c@d.com'] = test_diff
+        difference = get_rounding_difference(attributions)
+        assert difference == test_diff
+
+    def test_attributions_below_one(self, normalized_attributions):
+        attributions = normalized_attributions
+        test_diff = ROUNDING_TOLERANCE / Decimal("2")
+        attributions['b@c.com'] -= test_diff
+        difference = get_rounding_difference(attributions)
+        assert difference == -test_diff
diff --git a/tests/unit/money_in_test.py b/tests/unit/money_in_test.py
index 44120c7..0984b6f 100644
--- a/tests/unit/money_in_test.py
+++ b/tests/unit/money_in_test.py
@@ -4,12 +4,11 @@
     total_amount_paid_to_project,
     calculate_incoming_attribution,
     generate_transactions,
-    ROUNDING_TOLERANCE,
     renormalize,
     inflate_valuation,
     process_payments,
 )
-from oldabe.utils import correct_rounding_error
+from oldabe.accounting_utils import correct_rounding_error
 from oldabe.models import Attribution, Payment
 import pytest
 from unittest.mock import patch
diff --git a/tests/unit/parsing_test.py b/tests/unit/parsing_test.py
new file mode 100644
index 0000000..31328e5
--- /dev/null
+++ b/tests/unit/parsing_test.py
@@ -0,0 +1,61 @@
+import pytest
+from decimal import Decimal
+from oldabe.parsing import (
+    parse_percentage,
+    serialize_proportion,
+)
+
+
+class TestParsePercentage:
+    def test_an_integer(self):
+        assert parse_percentage('75') == Decimal('0.75')
+
+    def test_non_integer_greater_than_1(self):
+        assert parse_percentage('75.334455') == Decimal('0.75334455')
+
+    def test_non_integer_less_than_1(self):
+        assert parse_percentage('0.334455') == Decimal('0.00334455')
+
+    def test_decimal_places_at_precision_context(self):
+        assert parse_percentage('5.1234567891') == Decimal('0.051234567891')
+
+    def test_very_small_number(self):
+        assert parse_percentage('0.000001') == Decimal('0.00000001')
+
+    def test_0(self):
+        assert parse_percentage('0') == Decimal('0')
+
+    def test_0_point_0(self):
+        assert parse_percentage('0.0') == Decimal('0')
+
+    def test_100(self):
+        assert parse_percentage('100') == Decimal('1')
+
+    def test_100_point_0(self):
+        assert parse_percentage('100.0') == Decimal('1')
+
+
+class TestSerializeProportion:
+    def test_0(self):
+        assert serialize_proportion(Decimal('0')) == '0'
+
+    def test_0_point_0(self):
+        assert serialize_proportion(Decimal('0.0')) == '0'
+
+    def test_1(self):
+        assert serialize_proportion(Decimal('1')) == '100'
+
+    def test_1_point_0(self):
+        assert serialize_proportion(Decimal('1.0')) == '100'
+
+    def test_almost_1(self):
+        assert serialize_proportion(Decimal('0.9523452')) == '95.23452'
+
+    def test_low_precision_number(self):
+        assert serialize_proportion(Decimal('0.2')) == '20'
+
+    def test_very_small_number(self):
+        assert serialize_proportion(Decimal('0.0000002')) == '0.00002'
+
+    def test_decimal_places_at_precision_context(self):
+        assert serialize_proportion(Decimal('0.1234567891')) == '12.34567891'
diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py
deleted file mode 100644
index 008ec97..0000000
--- a/tests/unit/utils_test.py
+++ /dev/null
@@ -1,126 +0,0 @@
-from decimal import Decimal
-from oldabe.money_in import (
-    calculate_incoming_investment,
-    total_amount_paid_to_project,
-    calculate_incoming_attribution,
-    generate_transactions,
-    ROUNDING_TOLERANCE,
-    renormalize,
-    inflate_valuation,
-    process_payments,
-)
-from oldabe.utils import (
-    parse_percentage,
-    serialize_proportion,
-    get_rounding_difference,
-    correct_rounding_error,
-)
-from oldabe.models import Attribution, Payment
-import pytest
-from unittest.mock import patch
-from .utils import call_sequence
-from .fixtures import (
-    instruments,
-    normalized_attributions,
-    excess_attributions,
-    shortfall_attributions,
-    empty_attributions,
-    single_contributor_attributions,
-    itemized_payments,
-    new_itemized_payments,
-)  # noqa
-
-
-class TestParsePercentage:
-    def test_an_integer(self):
-        assert parse_percentage('75') == Decimal('0.75')
-
-    def test_non_integer_greater_than_1(self):
-        assert parse_percentage('75.334455') == Decimal('0.75334455')
-
-    def test_non_integer_less_than_1(self):
-        assert parse_percentage('0.334455') == Decimal('0.00334455')
-
-    def test_decimal_places_at_precision_context(self):
-        assert parse_percentage('5.1234567891') == Decimal('0.051234567891')
-
-    def test_very_small_number(self):
-        assert parse_percentage('0.000001') == Decimal('0.00000001')
-
-    def test_0(self):
-        assert parse_percentage('0') == Decimal('0')
-
-    def test_0_point_0(self):
-        assert parse_percentage('0.0') == Decimal('0')
-
-    def test_100(self):
-        assert parse_percentage('100') == Decimal('1')
-
-    def test_100_point_0(self):
-        assert parse_percentage('100.0') == Decimal('1')
-
-
-class TestSerializeProportion:
-    def test_0(self):
-        assert serialize_proportion(Decimal('0')) == '0'
-
-    def test_0_point_0(self):
-        assert serialize_proportion(Decimal('0.0')) == '0'
-
-    def test_1(self):
-        assert serialize_proportion(Decimal('1')) == '100'
-
-    def test_1_point_0(self):
-        assert serialize_proportion(Decimal('1.0')) == '100'
-
-    def test_almost_1(self):
-        assert serialize_proportion(Decimal('0.9523452')) == '95.23452'
-
-    def test_low_precision_number(self):
-        assert serialize_proportion(Decimal('0.2')) == '20'
-
-    def test_very_small_number(self):
-        assert serialize_proportion(Decimal('0.0000002')) == '0.00002'
-
-    def test_decimal_places_at_precision_context(self):
-        assert serialize_proportion(Decimal('0.1234567891')) == '12.34567891'
-
-
-class TestGetRoundingDifference:
-    def test_attributions_already_one(self, normalized_attributions):
-        attributions = normalized_attributions
-        test_diff = Decimal("0")
-        difference = get_rounding_difference(attributions)
-        assert difference == test_diff
-
-    def test_attributions_exceed_below_tolerance(
-        self, normalized_attributions
-    ):
-        attributions = normalized_attributions
-        test_diff = ROUNDING_TOLERANCE / Decimal("2")
-        attributions['c@d.com'] = test_diff
-        difference = get_rounding_difference(attributions)
-        assert difference == test_diff
-
-    def test_attributions_exceed_above_tolerance(
-        self, normalized_attributions
-    ):
-        attributions = normalized_attributions
-        test_diff = ROUNDING_TOLERANCE * Decimal("2")
-        attributions['c@d.com'] = test_diff
-        with pytest.raises(AssertionError):
-            _ = get_rounding_difference(attributions)
-
-    def test_attributions_exceed_by_tolerance(self, normalized_attributions):
-        attributions = normalized_attributions
-        test_diff = ROUNDING_TOLERANCE
-        attributions['c@d.com'] = test_diff
-        difference = get_rounding_difference(attributions)
-        assert difference == test_diff
-
-    def test_attributions_below_one(self, normalized_attributions):
-        attributions = normalized_attributions
-        test_diff = ROUNDING_TOLERANCE / Decimal("2")
-        attributions['b@c.com'] -= test_diff
-        difference = get_rounding_difference(attributions)
-        assert difference == -test_diff