def setUp(self): """ Sets up class attributes for convenience. """ super().setUp() # We use caps because this is a type. # pylint: disable=invalid-name self.AccountType = Debt # pylint: enable=invalid-name # It's important to synchronize the initial years of related # objects, so store it here: self.initial_year = 2000 # Every init requires an owner, so store that here: self.owner = Person(self.initial_year, "test", 2000, raise_rate={year: 1 for year in range(2000, 2066)}, retirement_date=2065) # We'll also need a timing value for various tests. # Use two inflows, at the start and end, evenly weighted: self.timing = {Decimal(0): 1, Decimal(1): 1} # Basic Debt account: self.debt = Debt(self.owner, balance=-1000, minimum_payment=Money(10), accelerated_payment=Money('Infinity'))
def setUp_decimal(self): initial_year = 2000 person = Person(initial_year, 'Testy McTesterson', 1980, retirement_date=2045) self.timing = Timing({Decimal(0.5): Decimal(1)}) # These accounts have different rates: self.debt_big_high_interest = Debt( person, balance=Decimal(1000), rate=Decimal(1), minimum_payment=Decimal(100), accelerated_payment=Decimal('Infinity'), high_precision=Decimal) self.debt_small_low_interest = Debt( person, balance=Decimal(100), rate=Decimal(0), minimum_payment=Decimal(10), accelerated_payment=Decimal('Infinity'), high_precision=Decimal) self.debt_medium = Debt(person, balance=Decimal(500), rate=Decimal(0.5), minimum_payment=Decimal(50), accelerated_payment=Decimal('Infinity'), high_precision=Decimal) self.debts = { self.debt_big_high_interest, self.debt_medium, self.debt_small_low_interest } self.max_payments = { debt: self.max_payment({debt}) for debt in self.debts } self.min_payments = { debt: self.min_payment({debt}) for debt in self.debts } self.strategy_avalanche = DebtPaymentStrategy( DebtPaymentStrategy.strategy_avalanche, high_precision=Decimal) self.strategy_snowball = DebtPaymentStrategy( DebtPaymentStrategy.strategy_snowball, high_precision=Decimal) self.excess = Decimal(10)
def setUp(self): initial_year = 2000 person = Person(initial_year, 'Testy McTesterson', 1980, retirement_date=2045) self.timing = Timing({0.5: 1}) # These accounts have different rates: self.debt_big_high_interest = Debt(person, balance=1000, rate=1, minimum_payment=100, accelerated_payment=float('inf')) self.debt_small_low_interest = Debt(person, balance=100, rate=0, minimum_payment=10, accelerated_payment=float('inf')) self.debt_medium = Debt(person, balance=500, rate=0.5, minimum_payment=50, accelerated_payment=float('inf')) self.debts = { self.debt_big_high_interest, self.debt_medium, self.debt_small_low_interest } self.max_payments = { debt: self.max_payment({debt}) for debt in self.debts } self.min_payments = { debt: self.min_payment({debt}) for debt in self.debts } self.strategy_avalanche = DebtPaymentStrategy( DebtPaymentStrategy.strategy_avalanche) self.strategy_snowball = DebtPaymentStrategy( DebtPaymentStrategy.strategy_snowball) self.excess = 10
def setUp_decimal(self): """ Sets up variables based on Decimal inputs. """ self.initial_year = 2000 self.person = Person(self.initial_year, "Test", "1 January 1980", retirement_date="1 January 2030", high_precision=Decimal) # An RRSP with a $1000 balance and $100 in contribution room: self.rrsp = RRSP(initial_year=self.initial_year, owner=self.person, balance=Decimal(1000), contribution_room=Decimal(100), high_precision=Decimal) # Another RRSP, linked to the first one: # (For testing LinkedLimitAccount nodes) self.rrsp2 = RRSP(initial_year=self.initial_year, owner=self.person, high_precision=Decimal) # A TFSA with a $100 balance and $1000 in contribution room: self.tfsa = TFSA(initial_year=self.initial_year, owner=self.person, balance=Decimal(100), contribution_room=Decimal(1000), high_precision=Decimal) # Another TFSA, linked to the first one: self.tfsa2 = TFSA(initial_year=self.initial_year, owner=self.person, high_precision=Decimal) # A taxable account with $0 balance (and no contribution limit) self.taxable_account = TaxableAccount(initial_year=self.initial_year, owner=self.person, balance=Decimal(0), high_precision=Decimal) # A $100 debt with no interest and a $10 min. payment: self.debt = Debt(initial_year=self.initial_year, owner=self.person, balance=Decimal(100), minimum_payment=Decimal(10), high_precision=Decimal) # Contribute to RRSP, then TFSA, then taxable self.priority_ordered = [self.rrsp, self.tfsa, self.taxable_account] # Contribute 50% to RRSP, 25% to TFSA, and 25% to taxable self.priority_weighted = { self.rrsp: Decimal(0.5), self.tfsa: Decimal(0.25), self.taxable_account: Decimal(0.25) } # Contribute to RRSP and TFSA (50-50) until both are full, with # the remainder to taxable accounts. self.priority_nested = [{ self.rrsp: Decimal(0.5), self.tfsa: Decimal(0.5) }, self.taxable_account]
def setUp_decimal(self): """ Builds default strategies/persons/etc. with Decimal inputs. """ # pylint: disable=invalid-name # This name is based on `setUp`, which doesn't follow Pylint's rules # pylint: enable=invalid-name # Use a default settings object: # (This is conditional so that subclasses can assign their own # settings object before calling super().setUp()) if not hasattr(self, 'settings'): self.settings = Settings() # To simplify tests, modify Settings so that forecasts are # just 2 years with easy-to-predict contributions ($1000/yr) self.settings.num_years = 2 self.settings.living_expenses_strategy = ( LivingExpensesStrategy.strategy_const_contribution) self.settings.living_expenses_base_amount = Decimal(1000) # Allow subclasses to use subclasses of Forecaster by assigning # to forecaster_type if not hasattr(self, 'forecaster_type'): self.forecaster_type = Forecaster # Build default `SubForecast` inputs based on `settings`: self.initial_year = self.settings.initial_year self.scenario = Scenario( inflation=Decimal(self.settings.inflation), stock_return=Decimal(self.settings.stock_return), bond_return=Decimal(self.settings.bond_return), other_return=Decimal(self.settings.other_return), management_fees=Decimal(self.settings.management_fees), initial_year=self.settings.initial_year, num_years=self.settings.num_years) self.living_expenses_strategy = LivingExpensesStrategy( strategy=self.settings.living_expenses_strategy, base_amount=Decimal(self.settings.living_expenses_base_amount), rate=Decimal(self.settings.living_expenses_rate), inflation_adjust=self.scenario.inflation_adjust) self.saving_strategy = TransactionStrategy( strategy=self.settings.saving_strategy, weights={ year: Decimal(val) for (year, val) in self.settings.saving_weights.items() }) self.withdrawal_strategy = TransactionStrategy( strategy=self.settings.withdrawal_strategy, weights={ year: Decimal(val) for (year, val) in self.settings.withdrawal_weights.items() }) self.allocation_strategy = AllocationStrategy( strategy=self.settings.allocation_strategy, min_equity=Decimal(self.settings.allocation_min_equity), max_equity=Decimal(self.settings.allocation_max_equity), target=Decimal(self.settings.allocation_target), standard_retirement_age=( self.settings.allocation_std_retirement_age), risk_transition_period=self.settings.allocation_risk_trans_period, adjust_for_retirement_plan=( self.settings.allocation_adjust_retirement)) self.debt_payment_strategy = DebtPaymentStrategy( strategy=self.settings.debt_payment_strategy, high_precision=Decimal) self.tax_treatment = Tax( tax_brackets={ year: { Decimal(lower): Decimal(upper) } for (year, vals) in self.settings.tax_brackets.items() for (lower, upper) in vals.items() }, personal_deduction={ year: Decimal(val) for (year, val) in self.settings.tax_personal_deduction.items() }, credit_rate={ year: Decimal(val) for (year, val) in self.settings.tax_credit_rate.items() }, inflation_adjust=self.scenario.inflation_adjust, high_precision=Decimal) # Now build some Ledger objects to test against: # A person making $10,000/yr self.person = Person(initial_year=self.initial_year, name="Test 1", birth_date="1 January 1980", retirement_date="31 December 2040", gross_income=Decimal(10000), raise_rate=Decimal(0), spouse=None, tax_treatment=self.tax_treatment, high_precision=Decimal) # An account with $1000 in it (and no interest) self.account = Account(owner=self.person, balance=Decimal(1000), high_precision=Decimal) # A debt with a $100 balance (and no interest) self.debt = Debt(owner=self.person, balance=Decimal(100), high_precision=Decimal) # Init a Forecaster object here for convenience: self.forecaster = self.forecaster_type(settings=self.settings, high_precision=Decimal)
def setUp(self): """ Builds default strategies, persons, etc. """ # Use a default settings object: # (This is conditional so that subclasses can assign their own # settings object before calling super().setUp()) if not hasattr(self, 'settings'): self.settings = Settings() # To simplify tests, modify Settings so that forecasts are # just 2 years with easy-to-predict contributions ($1000/yr) self.settings.num_years = 2 self.settings.living_expenses_strategy = ( LivingExpensesStrategy.strategy_const_contribution) self.settings.living_expenses_base_amount = 1000 # Allow subclasses to use subclasses of Forecaster by assigning # to forecaster_type if not hasattr(self, 'forecaster_type'): self.forecaster_type = Forecaster # Build default `SubForecast` inputs based on `settings`: self.initial_year = self.settings.initial_year self.scenario = Scenario(inflation=self.settings.inflation, stock_return=self.settings.stock_return, bond_return=self.settings.bond_return, other_return=self.settings.other_return, management_fees=self.settings.management_fees, initial_year=self.settings.initial_year, num_years=self.settings.num_years) self.living_expenses_strategy = LivingExpensesStrategy( strategy=self.settings.living_expenses_strategy, base_amount=self.settings.living_expenses_base_amount, rate=self.settings.living_expenses_rate, inflation_adjust=self.scenario.inflation_adjust) self.saving_strategy = TransactionStrategy( strategy=self.settings.saving_strategy, weights=self.settings.saving_weights) self.withdrawal_strategy = TransactionStrategy( strategy=self.settings.withdrawal_strategy, weights=self.settings.withdrawal_weights) self.allocation_strategy = AllocationStrategy( strategy=self.settings.allocation_strategy, min_equity=self.settings.allocation_min_equity, max_equity=self.settings.allocation_max_equity, target=self.settings.allocation_target, standard_retirement_age=( self.settings.allocation_std_retirement_age), risk_transition_period=self.settings.allocation_risk_trans_period, adjust_for_retirement_plan=( self.settings.allocation_adjust_retirement)) self.debt_payment_strategy = DebtPaymentStrategy( strategy=self.settings.debt_payment_strategy) self.tax_treatment = Tax( tax_brackets=self.settings.tax_brackets, personal_deduction=self.settings.tax_personal_deduction, credit_rate=self.settings.tax_credit_rate, inflation_adjust=self.scenario.inflation_adjust) # Now build some Ledger objects to test against: # A person making $10,000/yr self.person = Person(initial_year=self.initial_year, name="Test 1", birth_date="1 January 1980", retirement_date="31 December 2040", gross_income=10000, raise_rate=0, spouse=None, tax_treatment=self.tax_treatment) # An account with $1000 in it (and no interest) self.account = Account(owner=self.person, balance=1000) # A debt with a $100 balance (and no interest) self.debt = Debt(owner=self.person, balance=100) # Init a Forecaster object here for convenience: self.forecaster = self.forecaster_type(settings=self.settings)
class TestDebtPaymentStrategies(TestCaseTransactions): """ Tests the strategies of the `DebtPaymentStrategy` class. In particular, this class tests various payments using various strategies on a stock set of (multiple) debts. """ def setUp(self): initial_year = 2000 person = Person(initial_year, 'Testy McTesterson', 1980, retirement_date=2045) self.timing = Timing({Decimal(0.5): 1}) # These accounts have different rates: self.debt_big_high_interest = Debt( person, balance=Money(1000), rate=1, minimum_payment=Money(100), accelerated_payment=Money('Infinity')) self.debt_small_low_interest = Debt( person, balance=Money(100), rate=0, minimum_payment=Money(10), accelerated_payment=Money('Infinity')) self.debt_medium = Debt(person, balance=Money(500), rate=0.5, minimum_payment=Money(50), accelerated_payment=Money('Infinity')) self.debts = { self.debt_big_high_interest, self.debt_medium, self.debt_small_low_interest } self.max_payments = { debt: self.max_payment({debt}) for debt in self.debts } self.min_payments = { debt: self.min_payment({debt}) for debt in self.debts } self.strategy_avalanche = DebtPaymentStrategy( DebtPaymentStrategy.strategy_avalanche) self.strategy_snowball = DebtPaymentStrategy( DebtPaymentStrategy.strategy_snowball) self.excess = Money(10) def min_payment(self, debts, timing=None): """ Finds the minimum payment *from savings* for `accounts`. """ if timing is None: timing = self.timing return sum( sum(debt.min_inflows(timing=timing).values()) for debt in debts) def max_payment(self, debts, timing=None): """ Finds the maximum payment *from savings* for `debts`. """ if timing is None: timing = self.timing return sum( sum(debt.max_inflows(timing=timing).values()) for debt in debts) def make_available(self, total, timing=None): """ Generates an `available` dict of cashflows. """ if timing is None: timing = self.timing normalization = sum(timing.values()) return { when: total * weight / normalization for when, weight in timing.items() } def test_snowball_min_payment(self): """ Test strategy_snowball with the minimum payment only. """ # Inflows will exactly match minimum payments: total = self.min_payment(self.debts) available = self.make_available(total) results = self.strategy_snowball(self.debts, available) for debt in self.debts: self.assertTransactions(results[debt], debt.minimum_payment) def test_snowball_less_than_min(self): """ Test strategy_snowball with less than the minimum payments. """ # DebtPaymentStrategy (like TransactionStrategy) always # allocates no more than the amount available. # This test confirms that: # 1) The sum of amounts allocated is equal to `total` # 2) The order of accounts is maintained (i.e. smallest # debts repaid first) # To do this, we set `total` to a non-zero smaller than the # smallest debt's minimum payment. total = self.min_payments[self.debt_small_low_interest] / 2 available = self.make_available(total) results = self.strategy_snowball(self.debts, available) # Entire repayment should go to the smallest debt: self.assertTransactions(results[self.debt_small_low_interest], total) # Remaining debts should receive no payments: if self.debt_medium in results: self.assertTransactions(results[self.debt_medium], Money(0)) if self.debt_medium in results: self.assertTransactions(results[self.debt_big_high_interest], Money(0)) def test_snowball_basic(self): """ Test strategy_snowball with a little more than min payments. """ # The smallest debt should be paid first. total = self.min_payment(self.debts) + self.excess available = self.make_available(total) results = self.strategy_snowball(self.debts, available) # The smallest debt should be partially repaid: self.assertTransactions( results[self.debt_small_low_interest], self.debt_small_low_interest.minimum_payment + self.excess) # The medium-sized debt should get its minimum payments: self.assertTransactions(results[self.debt_medium], self.min_payments[self.debt_medium]) # The largest debt should get its minimum payments: self.assertTransactions(results[self.debt_big_high_interest], self.min_payments[self.debt_big_high_interest]) def test_snowball_close_one(self): """ Test strategy_snowball payments to close one debt. """ # Pay more than the first-paid debt will accomodate. # The excess should go to the next-paid debt (medium). total = self.min_payment(self.debts - {self.debt_small_low_interest}) total += self.max_payment({self.debt_small_low_interest}) total += self.excess available = self.make_available(total) results = self.strategy_snowball(self.debts, available) # The smallest debt should be fully repaid: self.assertTransactions( results[self.debt_small_low_interest], self.max_payments[self.debt_small_low_interest]) # The medium-sized debt should be partially repaid: self.assertTransactions(results[self.debt_medium], self.debt_medium.minimum_payment + self.excess) # The largest debt should get its minimum payments: self.assertTransactions(results[self.debt_big_high_interest], self.min_payments[self.debt_big_high_interest]) def test_snowball_close_two(self): """ Test strategy_snowball with payments to close 2 debts. """ # Pay more than the first and second-paid debts will accomodate. # The self.excess should go to the next-paid debt. total = self.min_payment({self.debt_big_high_interest}) total += self.max_payment(self.debts - {self.debt_big_high_interest}) total += self.excess available = self.make_available(total) results = self.strategy_snowball(self.debts, available) # The smallest debt should be fully repaid: self.assertTransactions( results[self.debt_small_low_interest], self.max_payments[self.debt_small_low_interest]) # The medium-size debt should be fully repaid: self.assertTransactions(results[self.debt_medium], self.max_payments[self.debt_medium]) # The largest debt should be partially repaid: self.assertTransactions( results[self.debt_big_high_interest], self.debt_big_high_interest.minimum_payment + self.excess) def test_snowball_close_all(self): """ Test strategy_snowball with payments to close all debts. """ # Contribute more than the total max. total = self.max_payment(self.debts) + self.excess available = self.make_available(total) results = self.strategy_snowball(self.debts, available) # Each debt should be fully repaid: for debt in self.debts: self.assertTransactions(results[debt], self.max_payments[debt]) def test_avalanche_min_payment(self): """ Test strategy_avalanche with minimum payments only. """ total = self.min_payment(self.debts) available = self.make_available(total) results = self.strategy_avalanche(self.debts, available) for debt in self.debts: self.assertTransactions(results[debt], debt.minimum_payment) def test_avalanche_less_than_min(self): """ Test strategy_avalanche with less than the minimum payments. """ # DebtPaymentStrategy (like TransactionStrategy) always # allocates no more than the amount available. # This test confirms that: # 1) The sum of amounts allocated is equal to `total` # 2) The order of accounts is maintained (i.e. # highest-interest debts repaid first) # To do this, we set `total` to a non-zero smaller than the # highest-interest debt's minimum payment. total = self.min_payments[self.debt_big_high_interest] / 2 available = self.make_available(total) results = self.strategy_avalanche(self.debts, available) # Entire repayment should go to the highest-interest debt: self.assertTransactions(results[self.debt_big_high_interest], total) # Remaining debts should receive no payments: if self.debt_medium in results: self.assertTransactions(results[self.debt_medium], Money(0)) if self.debt_medium in results: self.assertTransactions(results[self.debt_small_low_interest], Money(0)) def test_avalanche_basic(self): """ Test strategy_avalanche with a bit more than min payments. """ # The highest-interest debt should be paid first. total = self.min_payment(self.debts) + self.excess available = self.make_available(total) results = self.strategy_avalanche(self.debts, available) self.assertTransactions( results[self.debt_big_high_interest], self.debt_big_high_interest.minimum_payment + self.excess) self.assertTransactions(results[self.debt_medium], self.debt_medium.minimum_payment) self.assertTransactions(results[self.debt_small_low_interest], self.debt_small_low_interest.minimum_payment) def test_avalanche_close_one(self): """ Test strategy_avalanche with payments to close one debt. """ # Pay more than the first-paid debt will accomodate. # The excess should go to the next-paid debt (medium). total = self.min_payment(self.debts - {self.debt_big_high_interest}) total += self.max_payment({self.debt_big_high_interest}) total += self.excess available = self.make_available(total) results = self.strategy_avalanche(self.debts, available) # The high interest debt should be fully repaid: self.assertTransactions(results[self.debt_big_high_interest], self.max_payments[self.debt_big_high_interest]) # The medium-interest debt should be partially repaid: self.assertTransactions(results[self.debt_medium], self.debt_medium.minimum_payment + self.excess) # The low-interest debt should receive the minimum payment: self.assertTransactions(results[self.debt_small_low_interest], self.debt_small_low_interest.minimum_payment) def test_avalanche_close_two(self): """ Test strategy_avalanche with payments to close two debts. """ # Pay more than the first and second-paid debts will accomodate. # The excess should go to the next-paid debt. total = self.min_payment({self.debt_small_low_interest}) total += self.max_payment(self.debts - {self.debt_small_low_interest}) total += self.excess available = self.make_available(total) results = self.strategy_avalanche(self.debts, available) # The high interest debt should be fully repaid: self.assertTransactions(results[self.debt_big_high_interest], self.max_payments[self.debt_big_high_interest]) # The medium interest debt should be fully repaid: self.assertTransactions(results[self.debt_medium], self.max_payments[self.debt_medium]) # The low interest debt should be partially repaid: self.assertTransactions( results[self.debt_small_low_interest], self.debt_small_low_interest.minimum_payment + self.excess) def test_avalanche_close_all(self): """ Test strategy_avalanche with payments to close all debts. """ # Contribute more than the total max. total = self.max_payment(self.debts) + self.excess available = self.make_available(total) results = self.strategy_avalanche(self.debts, available) # All debts should be fully repaid: for debt in self.debts: self.assertTransactions(results[debt], self.max_payments[debt]) def test_accel_payment_none(self): """ Tests payments where `accelerate_payment=Money(0)`. """ # Don't allow accelerated payments and try to contribute more # than the minimum. self.debt_medium.accelerated_payment = Money(0) available = self.make_available(self.debt_medium.minimum_payment * 2) results = self.strategy_avalanche({self.debt_medium}, available) # If there's no acceleration, only the minimum is paid. self.assertTransactions(results[self.debt_medium], self.debt_medium.minimum_payment) def test_accel_payment_finite(self): """ Tests payments with finite, non-zero `accelerate_payment`. """ # Allow only $20 in accelerated payments and try to contribute # even more than that. self.debt_medium.accelerated_payment = Money(20) available = self.make_available(self.debt_medium.minimum_payment + Money(40)) results = self.strategy_avalanche({self.debt_medium}, available) # Payment should be $20 more than the minimum: self.assertTransactions(results[self.debt_medium], self.debt_medium.minimum_payment + Money(20)) def test_accel_payment_infinity(self): """ Tests payments where `accelerate_payment=Money('Infinity')`. """ # No limit on accelerated payments. Try to contribute more than # the debt requires to be fully repaid: self.debt_medium.accelerated_payment = Money("Infinity") available = self.make_available( 2 * sum(self.debt_medium.max_inflows().values())) results = self.strategy_avalanche({self.debt_medium}, available) # Payments should max out money available: self.assertTransactions(results[self.debt_medium], self.max_payments[self.debt_medium])
class TestDebtMethods(unittest.TestCase): """ Test Debt. """ def setUp(self): """ Sets up class attributes for convenience. """ super().setUp() # We use caps because this is a type. # pylint: disable=invalid-name self.AccountType = Debt # pylint: enable=invalid-name # It's important to synchronize the initial years of related # objects, so store it here: self.initial_year = 2000 # Every init requires an owner, so store that here: self.owner = Person(self.initial_year, "test", 2000, raise_rate={year: 1 for year in range(2000, 2066)}, retirement_date=2065) # We'll also need a timing value for various tests. # Use two inflows, at the start and end, evenly weighted: self.timing = {Decimal(0): 1, Decimal(1): 1} # Basic Debt account: self.debt = Debt(self.owner, balance=-1000, minimum_payment=Money(10), accelerated_payment=Money('Infinity')) def test_init_default(self): """ Test Debt.__init__ with default args. """ account = self.AccountType(self.owner) self.assertEqual(account.minimum_payment, Money(0)) self.assertEqual(account.accelerated_payment, Money('Infinity')) def test_init_explicit(self, *args, **kwargs): """ Test Debt.__init__ with explicit args of expected types. """ minimum_payment = Money(100) accelerated_payment = Money(0) account = self.AccountType(self.owner, *args, minimum_payment=minimum_payment, accelerated_payment=accelerated_payment, **kwargs) self.assertEqual(account.minimum_payment, minimum_payment) self.assertEqual(account.accelerated_payment, accelerated_payment) def test_init_convert(self, *args, **kwargs): """ Test Debt.__init__ with args needing conversion. """ minimum_payment = 100 accelerated_payment = 10 account = self.AccountType(self.owner, *args, minimum_payment=minimum_payment, accelerated_payment=accelerated_payment, **kwargs) self.assertEqual(account.minimum_payment, minimum_payment) self.assertEqual(account.accelerated_payment, Money(accelerated_payment)) def test_init_invalid(self, *args, **kwargs): """ Test Debt.__init__ with non-convertible args. """ with self.assertRaises(decimal.InvalidOperation): _ = self.AccountType(self.owner, *args, minimum_payment='invalid', **kwargs) with self.assertRaises(decimal.InvalidOperation): _ = self.AccountType(self.owner, *args, accelerated_payment='invalid', **kwargs) def test_max_inflows_large_balance(self): """ Test `max_inflows` with balance greater than min. payment. """ self.debt.minimum_payment = 100 self.debt.balance = Money(-1000) result = self.debt.max_inflows(self.timing) # Test result by adding those transactions to the account # and confirming that it brings the balance to $0: for when, value in result.items(): self.debt.add_transaction(value, when=when) balance = self.debt.balance_at_time('end') self.assertAlmostEqual(balance, Money(0)) def test_max_inflows_small_balance(self): """ Test `max_inflows` with balance less than minimum payment. """ self.debt.minimum_payment = 1000 self.debt.balance = Money(-100) result = self.debt.max_inflows(self.timing) for when, value in result.items(): self.debt.add_transaction(value, when=when) # Result of `max_outflows` should bring balance to $0 if # applied as transactions: self.assertAlmostEqual(self.debt.balance_at_time('end'), Money(0)) def test_max_inflows_zero_balance(self): """ Test `max_inflows` with zero balance. """ self.debt.minimum_payment = 100 self.debt.balance = Money(0) result = self.debt.max_inflows(self.timing) for value in result.values(): self.assertEqual(value, Money(0)) def test_max_inflows_no_accel(self): """ Test `max_inflows` with zero `accelerated_payment`. """ self.debt.minimum_payment = 100 self.debt.balance = Money(-200) self.debt.accelerated_payment = 0 result = self.debt.max_inflows(self.timing) # Total inflows should be limited to minimum_payment: self.assertEqual(sum(result.values()), self.debt.minimum_payment) def test_max_inflows_finite_accel(self): """ Test `max_inflows` with finite `accelerated_payment`. """ self.debt.minimum_payment = 100 self.debt.balance = Money(-200) self.debt.accelerated_payment = 50 result = self.debt.max_inflows(self.timing) # Total inflows should be limited to min. payment + accel: self.assertEqual( sum(result.values()), self.debt.minimum_payment + self.debt.accelerated_payment) def test_max_inflows_small_inflow(self): """ Test `max_inflows` with small pre-existing inflows. """ self.debt.minimum_payment = 100 self.debt.balance = Money(-200) self.debt.accelerated_payment = 50 # Add an inflow that's less than the total that we could pay: self.debt.add_transaction(60) result = self.debt.max_inflows(self.timing) target = (self.debt.minimum_payment + self.debt.accelerated_payment - Money(60)) # Amount already added # Total inflows should be limited to amount remaining after # existing transactions, up to min. payment + accel: self.assertEqual(sum(result.values()), target) def test_max_inflows_large_inflow(self): """ Test `max_inflows` with inflows greater than the total max. """ self.debt.minimum_payment = 100 self.debt.balance = Money(-200) self.debt.accelerated_payment = 0 # Add an inflow that's more than the total that we can pay: self.debt.add_transaction(170) result = self.debt.max_inflows(self.timing) target = Money(0) # We can't add any more # The result should be $0, not a negative value: self.assertEqual(sum(result.values()), target) def test_min_inflows_large_balance(self): """ Test `min_inflows` with balance greater than min. payment. """ self.debt.minimum_payment = 100 self.debt.balance = Money(-1000) result = self.debt.min_inflows(self.timing) # Inflows should be capped at minimum payment: self.assertEqual(sum(result.values()), self.debt.minimum_payment) def test_min_inflows_small_balance(self): """ Test `min_inflows` with balance less than min. payment. """ self.debt.minimum_payment = 1000 self.debt.balance = Money(-100) # The resuls will be impacted by the timing of outflows, so # pick a specific timing here: a lump sum at end of year. timing = {Decimal(1): 1} result = self.debt.min_inflows(timing) # Inflows should be capped at the balance at the time the # transaction was made (i.e. $100): self.assertEqual(-sum(result.values()), self.debt.balance_at_time(1)) def test_min_inflows_zero_balance(self): """ Test `min_inflows` with zero balance. """ self.debt.minimum_payment = 100 self.debt.balance = Money(0) result = self.debt.min_inflows(self.timing) # No inflows should be made to a fully-paid debt: self.assertEqual(sum(result.values()), Money(0)) def test_min_inflows_small_inflow(self): """ Test `min_inflows` with small pre-existing inflows. """ self.debt.minimum_payment = 10 self.debt.balance = Money(-100) # Add inflow less than the min. payment: self.debt.add_transaction(5) result = self.debt.min_inflows(self.timing) # We only need to add another $5 to reach the min. payment: self.assertEqual(sum(result.values()), Money(5)) def test_min_inflows_large_inflow(self): """ Test `min_inflows` with inflows more than the min. payment. """ self.debt.minimum_payment = 10 self.debt.balance = Money(-100) # Add inflow greater than the min. payment: self.debt.add_transaction(20) result = self.debt.min_inflows(self.timing) # No need to add any more payments to reach the minimum: self.assertEqual(sum(result.values()), Money(0))