Example #1
0
    def setUp(self):
        """ Sets up variables for testing. """

        # Vars for building accounts:
        initial_year = 2000
        person = Person(initial_year,
                        'Testy McTesterson',
                        1980,
                        retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(person, balance=200, rate=0, contribution_room=200)
        self.tfsa = TFSA(person, balance=100, rate=0, contribution_room=100)
        self.taxable_account = TaxableAccount(person, balance=1000, rate=0)
        self.accounts = {self.rrsp, self.tfsa, self.taxable_account}

        # Define a simple timing for transactions:
        self.timing = {0.5: 1}

        self.max_outflow = sum(
            sum(account.max_outflows(timing=self.timing).values())
            for account in self.accounts)
        self.max_inflows = sum(
            sum(account.max_inflows(timing=self.timing).values())
            for account in self.accounts)

        # Build strategy for testing (in non-init tests):
        self.weights = {'RRSP': 0.4, 'TFSA': 0.3, 'TaxableAccount': 0.3}
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_weighted, self.weights)
Example #2
0
    def setUp(self):
        """ Sets up variables for testing. """
        # Vars for building accounts:
        initial_year = 2000
        person = Person(
            initial_year, 'Testy McTesterson', 1980, retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(
            person,
            balance=Money(200), rate=0, contribution_room=Money(200))
        self.rrsp2 = RRSP(person, balance=Money(100), rate=0)
        self.tfsa = TFSA(
            person,
            balance=Money(100), rate=0, contribution_room=Money(100))
        self.taxable_account = TaxableAccount(
            person, balance=Money(1000), rate=0)
        self.accounts = {
            self.rrsp, self.rrsp2, self.tfsa, self.taxable_account
        }

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): 1}

        # Build strategy for testing (in non-init tests):
        self.weights = {
            'RRSP': Decimal('0.4'),
            'TFSA': Decimal('0.3'),
            'TaxableAccount': Decimal('0.3')
        }
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_weighted, self.weights)
Example #3
0
    def setUp(self):
        """ Sets up variables for testing. """
        # Different groups of tests use different groups of variables.
        # We could split this class up into multiple classes (perhaps
        # one for ordered strategies and one for weighted strategies?),
        # but for now this project's practice is one test case for each
        # custom class.
        # pylint: disable=too-many-instance-attributes

        initial_year = 2000
        person = Person(
            initial_year, 'Testy McTesterson', 1980, retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(
            person,
            balance=Money(200), rate=0, contribution_room=Money(200))
        self.tfsa = TFSA(
            person,
            balance=Money(100), rate=0, contribution_room=Money(100))
        self.taxable_account = TaxableAccount(
            person, balance=Money(1000), rate=0)
        self.accounts = {self.rrsp, self.tfsa, self.taxable_account}

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): 1}

        # Build strategy for testing (in non-init tests):
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_ordered, {
                'RRSP': 1,
                'TFSA': 2,
                'TaxableAccount': 3
            })
Example #4
0
    def setUp_decimal(self):
        """ Sets up variables for testing. """
        # pylint: disable=invalid-name
        # Pylint doesn't like `setUp_decimal`, but it's not our naming
        # convention, so don't complain to us!
        # pylint: enable=invalid-name
        initial_year = 2000
        person = Person(initial_year,
                        'Testy McTesterson',
                        1980,
                        retirement_date=2045,
                        high_precision=Decimal)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(person,
                         balance=Decimal(200),
                         rate=Decimal(0),
                         contribution_room=Decimal(200),
                         high_precision=Decimal)
        self.rrsp2 = RRSP(person,
                          balance=Decimal(100),
                          rate=Decimal(0),
                          high_precision=Decimal)
        self.tfsa = TFSA(person,
                         balance=Decimal(100),
                         rate=Decimal(0),
                         contribution_room=Decimal(100),
                         high_precision=Decimal)
        self.taxable_account = TaxableAccount(person,
                                              balance=Decimal(1000),
                                              rate=Decimal(0),
                                              high_precision=Decimal)
        self.accounts = {
            self.rrsp, self.rrsp2, self.tfsa, self.taxable_account
        }

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): Decimal(1)}

        self.max_outflow = sum(
            sum(account.max_outflows(timing=self.timing).values())
            for account in self.accounts)
        self.max_inflows = sum(
            sum(account.max_inflows(timing=self.timing).values())
            for account in self.accounts)

        # Build strategies for testing (in non-init tests):
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_ordered, {
                'RRSP': Decimal(1),
                'TFSA': Decimal(2),
                'TaxableAccount': Decimal(3)
            },
            high_precision=Decimal)
Example #5
0
    def setUp_decimal(self):
        """ Sets up variables based on Decimal inputs. """
        # pylint: disable=invalid-name
        # Pylint doesn't like `setUp_decimal`, but it's not our naming
        # convention, so don't complain to us!
        # pylint: enable=invalid-name

        # Vars for building accounts:
        initial_year = 2000
        person = Person(initial_year,
                        'Testy McTesterson',
                        1980,
                        retirement_date=2045,
                        high_precision=Decimal)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(person,
                         balance=Decimal(200),
                         rate=Decimal(0),
                         contribution_room=Decimal(200),
                         high_precision=Decimal)
        self.rrsp2 = RRSP(person,
                          balance=Decimal(100),
                          rate=Decimal(0),
                          high_precision=Decimal)
        self.tfsa = TFSA(person,
                         balance=Decimal(100),
                         rate=Decimal(0),
                         contribution_room=Decimal(100),
                         high_precision=Decimal)
        self.taxable_account = TaxableAccount(person,
                                              balance=Decimal(1000),
                                              rate=Decimal(0),
                                              high_precision=Decimal)
        self.accounts = {
            self.rrsp, self.rrsp2, self.tfsa, self.taxable_account
        }

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): Decimal(1)}

        # Build strategy for testing (in non-init tests):
        self.weights = {
            'RRSP': Decimal(0.4),
            'TFSA': Decimal(0.3),
            'TaxableAccount': Decimal(0.3)
        }
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_weighted,
            self.weights,
            high_precision=Decimal)
Example #6
0
    def setUp_decimal(self):
        """ Sets up variables based on Decimal inputs. """
        # pylint: disable=invalid-name
        # Pylint doesn't like `setUp_decimal`, but it's not our naming
        # convention, so don't complain to us!
        # pylint: enable=invalid-name

        # Different groups of tests use different groups of variables.
        # We could split this class up into multiple classes (perhaps
        # one for ordered strategies and one for weighted strategies?),
        # but for now this project's practice is one test case for each
        # custom class.
        # pylint: disable=too-many-instance-attributes

        initial_year = 2000
        person = Person(initial_year,
                        'Testy McTesterson',
                        1980,
                        retirement_date=2045,
                        high_precision=Decimal)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(person,
                         balance=Decimal(200),
                         rate=Decimal(0),
                         contribution_room=Decimal(200),
                         high_precision=Decimal)
        self.tfsa = TFSA(person,
                         balance=Decimal(100),
                         rate=Decimal(0),
                         contribution_room=Decimal(100),
                         high_precision=Decimal)
        self.taxable_account = TaxableAccount(person,
                                              balance=Decimal(1000),
                                              rate=Decimal(0),
                                              high_precision=Decimal)
        self.accounts = {self.rrsp, self.tfsa, self.taxable_account}

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): Decimal(1)}

        # Build strategy for testing (in non-init tests):
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_ordered, {
                'RRSP': 1,
                'TFSA': 2,
                'TaxableAccount': 3
            },
            high_precision=Decimal)
    def setUp(self):
        """ Sets up class to use Canadian default values. """
        # Override settings/forecaster types to use Canadian subclasses.
        # (This is conditional so that subclasses can assign their own
        # objects before calling super().setUp())
        if not hasattr(self, 'settings'):
            self.settings = SettingsCanada()
        if not hasattr(self, 'forecaster_type'):
            self.forecaster_type = ForecasterCanada
        # Let the superclass handle setup:
        super().setUp()

        self.constants = constants.ConstantsCanada()

        # Override tax_treatment to use TaxCanada object:
        self.tax_treatment = TaxCanada(
            inflation_adjust=self.scenario.inflation_adjust,
            province=self.settings.tax_province, constants=self.constants)
        # The AccountTransactionStrategy settings for ForecasterCanada
        # don't include an Account object; replace it with an
        # otherwise-identical TaxableAccount, which is represented in
        # the settings.
        self.account = TaxableAccount(
            owner=self.person,
            balance=self.account.balance,
            rate=self.account.rate,
            nper=self.account.nper)
Example #8
0
 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]
Example #9
0
    def setUp(self):
        """ Sets up variables for testing. """
        initial_year = 2000
        person = Person(
            initial_year, 'Testy McTesterson', 1980, retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(
            person,
            balance=Money(200), rate=0, contribution_room=Money(200))
        self.rrsp2 = RRSP(person, balance=Money(100), rate=0)
        self.tfsa = TFSA(
            person,
            balance=Money(100), rate=0, contribution_room=Money(100))
        self.taxable_account = TaxableAccount(
            person, balance=Money(1000), rate=0)
        self.accounts = {
            self.rrsp, self.rrsp2, self.tfsa, self.taxable_account
        }

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): 1}

        self.max_outflow = sum(
            sum(account.max_outflows(timing=self.timing).values())
            for account in self.accounts)
        self.max_inflows = sum(
            sum(account.max_inflows(timing=self.timing).values())
            for account in self.accounts)

        # Build strategies for testing (in non-init tests):
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_ordered, {
                'RRSP': 1,
                'TFSA': 2,
                'TaxableAccount': 3
            })
Example #10
0
    def setUp(self):
        """ Sets up mutable variables for each test call. """
        # Set to default province:
        self.province = 'BC'
        self.tax = TaxCanada(self.inflation_adjustments, province='BC')

        # Set up some people to test on:
        # Person1 makes $100,000/yr, has a taxable account with $500,000
        # taxable income, and an RRSP with $500,000 in taxable income.
        self.person1 = Person(
            self.initial_year, "Tester 1", self.initial_year - 20,
            retirement_date=self.initial_year + 45, gross_income=100000)
        self.taxable_account1 = TaxableAccount(
            owner=self.person1,
            acb=0, balance=Money(1000000), rate=Decimal('0.05'), nper=1)
        self.taxable_account1.add_transaction(-Money(1000000), when='start')
        # NOTE: by using an RRSP here, a pension income tax credit will
        # be applied by TaxCanadaJurisdiction. Be aware of this if you
        # want to test this output against a generic Tax object with
        # Canadian brackets.
        self.rrsp = RRSP(
            self.person1,
            inflation_adjust=self.inflation_adjustments,
            contribution_room=0,
            balance=Money(500000), rate=Decimal('0.05'), nper=1)
        self.rrsp.add_transaction(-Money(500000), when='start')

        # Person2 makes $50,000/yr and has a taxable account with
        # $5000 taxable income.
        self.person2 = Person(
            self.initial_year, "Tester 2", self.initial_year - 18,
            retirement_date=self.initial_year + 47, gross_income=50000)
        self.taxable_account2 = TaxableAccount(
            owner=self.person2,
            acb=0, balance=Money(10000), rate=Decimal('0.05'), nper=1)
        self.taxable_account2.add_transaction(-Money(10000), when='start')
Example #11
0
    def setUp_decimal(self):
        """ Sets up class to use Canadian default values. """
        # This handles almost everything:
        super().setUp_decimal()

        self.settings = SettingsCanada(high_precision=Decimal)
        self.constants = constants.ConstantsCanada(high_precision=Decimal)

        # Override tax_treatment to use TaxCanada object:
        self.tax_treatment = TaxCanada(
            inflation_adjust=self.scenario.inflation_adjust,
            province=self.settings.tax_province,
            high_precision=Decimal,
            constants=self.constants)
        # The AccountTransactionStrategy settings for ForecasterCanada
        # don't include an Account object; replace it with an
        # otherwise-identical TaxableAccount, which is represented in
        # the settings.
        self.account = TaxableAccount(
            owner=self.person,
            balance=self.account.balance,
            rate=self.account.rate,
            nper=self.account.nper,
            high_precision=Decimal)
Example #12
0
class TestTax(unittest.TestCase):
    """ Tests the `Tax` class. """

    # We save a number of attributes for convenience in testing later
    # on. We could refactor, but it would complicate the tests, which
    # would be worse.
    # pylint: disable=too-many-instance-attributes

    def setUp(self):
        self.initial_year = 2000
        # Build 100 years of inflation adjustments with steadily growing
        # adjustment factors. First, pick a nice number (ideally a power
        # of 2 to avoid float precision issues); inflation_adjustment
        # will grow by adding the inverse (1/n) of this number annually
        growth_factor = 32
        year_range = range(self.initial_year, self.initial_year + 100)
        self.inflation_adjustments = {
            year: 1 + (year - self.initial_year) / growth_factor
            for year in year_range
        }
        # For convenience, store the year where inflation has doubled
        # the nominal value of money
        self.double_year = self.initial_year + growth_factor
        # Build some brackets with nice round numbers:
        self.tax_brackets = {self.initial_year: {0: 0.1, 100: 0.2, 10000: 0.3}}
        # For convenience, build a sorted, type-converted array of
        # each of the tax bracket thresholds:
        self.brackets = sorted({
            key: self.tax_brackets[self.initial_year][key]
            for key in self.tax_brackets[self.initial_year].keys()
        })
        # For convenience in testing, build an accum dict that
        # corresponds to the tax brackets above.
        self.accum = {self.initial_year: {0: 0, 100: 10, 10000: 1990}}
        self.personal_deduction = {self.initial_year: 100}
        self.credit_rate = {self.initial_year: 0.1}

        self.tax = Tax(self.tax_brackets,
                       inflation_adjust=self.inflation_adjustments,
                       personal_deduction=self.personal_deduction,
                       credit_rate=self.credit_rate)

        # Set up a simple person with no account-derived taxable income
        self.person = Person(self.initial_year,
                             "Tester",
                             self.initial_year - 25,
                             retirement_date=self.initial_year + 40,
                             gross_income=0)

        # Set up two people, spouses, on which to do more complex tests
        self.person1 = Person(self.initial_year,
                              "Tester 1",
                              self.initial_year - 20,
                              retirement_date=self.initial_year + 45,
                              gross_income=100000)
        self.person2 = Person(self.initial_year,
                              "Tester 2",
                              self.initial_year - 22,
                              retirement_date=self.initial_year + 43,
                              gross_income=50000)

        # Give the first person two accounts, one taxable and one
        # tax-deferred. Withdraw the entirety from the taxable account,
        # so that we don't need to worry about tax on unrealized growth:
        self.taxable_account1 = TaxableAccount(owner=self.person1,
                                               acb=0,
                                               balance=50000,
                                               rate=0.05,
                                               nper=1)
        self.taxable_account1.add_transaction(-50000, when='start')
        self.rrsp = RRSP(owner=self.person1,
                         inflation_adjust=self.inflation_adjustments,
                         contribution_room=0,
                         balance=10000,
                         rate=0.05,
                         nper=1)
        # Employment income is fully taxable, and only half of capital
        # gains (the income from the taxable account) is taxable:
        self.person1_taxable_income = (self.person1.gross_income +
                                       self.taxable_account1.balance / 2)

        # Give the second person two accounts, one taxable and one
        # non-taxable. Withdraw the entirety from the taxable account,
        # so that we don't need to worry about tax on unrealized growth,
        # and withdraw a bit from the non-taxable account (which should
        # have no effect on taxable income):
        self.taxable_account2 = TaxableAccount(owner=self.person2,
                                               acb=0,
                                               balance=20000,
                                               rate=0.05,
                                               nper=1)
        self.taxable_account2.add_transaction(-20000, when='start')
        self.tfsa = TFSA(owner=self.person2, balance=50000, rate=0.05, nper=1)
        self.tfsa.add_transaction(-20000, when='start')
        # Employment income is fully taxable, and only half of capital
        # gains (the income from the taxable account) is taxable:
        self.person2_taxable_income = (self.person2.gross_income +
                                       self.taxable_account2.balance / 2)

    def setUp_decimal(self):
        self.initial_year = 2000
        # Build 100 years of inflation adjustments with steadily growing
        # adjustment factors. First, pick a nice number (ideally a power
        # of 2 to avoid float precision issues); inflation_adjustment
        # will grow by adding the inverse (1/n) of this number annually
        growth_factor = 32
        year_range = range(self.initial_year, self.initial_year + 100)
        self.inflation_adjustments = {
            year: 1 + (year - self.initial_year) / growth_factor
            for year in year_range
        }
        # For convenience, store the year where inflation has doubled
        # the nominal value of money
        self.double_year = self.initial_year + growth_factor
        # Build some brackets with nice round numbers:
        self.tax_brackets = {
            self.initial_year: {
                Decimal(0): Decimal('0.1'),
                Decimal('100'): Decimal('0.2'),
                Decimal('10000'): Decimal('0.3')
            }
        }
        # For convenience, build a sorted, type-converted array of
        # each of the tax bracket thresholds:
        self.brackets = sorted({
            Decimal(key): self.tax_brackets[self.initial_year][key]
            for key in self.tax_brackets[self.initial_year].keys()
        })
        # For convenience in testing, build an accum dict that
        # corresponds to the tax brackets above.
        self.accum = {
            self.initial_year: {
                Decimal(0): Decimal('0'),
                Decimal('100'): Decimal('10'),
                Decimal('10000'): Decimal('1990')
            }
        }
        self.personal_deduction = {self.initial_year: Decimal('100')}
        self.credit_rate = {self.initial_year: Decimal('0.1')}

        self.tax = Tax(self.tax_brackets,
                       inflation_adjust=self.inflation_adjustments,
                       personal_deduction=self.personal_deduction,
                       credit_rate=self.credit_rate)

        # Set up a simple person with no account-derived taxable income
        self.person = Person(self.initial_year,
                             "Tester",
                             self.initial_year - 25,
                             retirement_date=self.initial_year + 40,
                             gross_income=Decimal(0))

        # Set up two people, spouses, on which to do more complex tests
        self.person1 = Person(self.initial_year,
                              "Tester 1",
                              self.initial_year - 20,
                              retirement_date=self.initial_year + 45,
                              gross_income=Decimal(100000))
        self.person2 = Person(self.initial_year,
                              "Tester 2",
                              self.initial_year - 22,
                              retirement_date=self.initial_year + 43,
                              gross_income=Decimal(50000))

        # Give the first person two accounts, one taxable and one
        # tax-deferred. Withdraw the entirety from the taxable account,
        # so that we don't need to worry about tax on unrealized growth:
        self.taxable_account1 = TaxableAccount(owner=self.person1,
                                               acb=Decimal(0),
                                               balance=Decimal(50000),
                                               rate=Decimal('0.05'),
                                               nper=Decimal(1))
        self.taxable_account1.add_transaction(Decimal(-50000), when='start')
        self.rrsp = RRSP(owner=self.person1,
                         inflation_adjust=self.inflation_adjustments,
                         contribution_room=Decimal(0),
                         balance=Decimal(10000),
                         rate=Decimal('0.05'),
                         nper=Decimal(1))
        # Employment income is fully taxable, and only half of capital
        # gains (the income from the taxable account) is taxable:
        self.person1_taxable_income = Decimal(self.person1.gross_income +
                                              self.taxable_account1.balance /
                                              2)

        # Give the second person two accounts, one taxable and one
        # non-taxable. Withdraw the entirety from the taxable account,
        # so that we don't need to worry about tax on unrealized growth,
        # and withdraw a bit from the non-taxable account (which should
        # have no effect on taxable income):
        self.taxable_account2 = TaxableAccount(owner=self.person2,
                                               acb=Decimal(0),
                                               balance=Decimal(20000),
                                               rate=Decimal('0.05'),
                                               nper=Decimal(1))
        self.taxable_account2.add_transaction(Decimal(-20000), when='start')
        self.tfsa = TFSA(owner=self.person2,
                         balance=Decimal(50000),
                         rate=Decimal('0.05'),
                         nper=Decimal(1))
        self.tfsa.add_transaction(Decimal(-20000), when='start')
        # Employment income is fully taxable, and only half of capital
        # gains (the income from the taxable account) is taxable:
        self.person2_taxable_income = Decimal(self.person2.gross_income +
                                              self.taxable_account2.balance /
                                              2)

    def test_init_optional(self):
        """ Test Tax.__init__ with all arguments, including optional. """
        tax = Tax(self.tax_brackets,
                  inflation_adjust=self.inflation_adjustments,
                  personal_deduction=self.personal_deduction,
                  credit_rate=self.credit_rate)
        for year in self.tax_brackets:
            self.assertEqual(tax.tax_brackets(year), self.tax_brackets[year])
            self.assertEqual(tax.accum(year), self.accum[year])
            self.assertEqual(tax.personal_deduction(year),
                             self.personal_deduction[year])
            self.assertEqual(tax.credit_rate(year), self.credit_rate[year])
        self.assertTrue(callable(tax.inflation_adjust))

    def test_init_basic(self):
        """ Test Tax.__init__ with only mandatory arguments. """
        tax = Tax(self.tax_brackets,
                  inflation_adjust=self.inflation_adjustments)
        for year in self.tax_brackets:
            self.assertEqual(tax.tax_brackets(year), self.tax_brackets[year])
            self.assertEqual(tax.accum(year), self.accum[year])
        self.assertTrue(callable(tax.inflation_adjust))
        self.assertEqual(tax.personal_deduction(self.initial_year), 0)
        self.assertEqual(tax.credit_rate(self.initial_year), 1)

    def test_income_0_money(self):
        """ Call Test on $0 income. """
        # $0 should return $0 in tax owing. This is the easiest test.
        income = 0
        self.assertEqual(self.tax(income, self.initial_year), 0)

    def test_income_0_person(self):
        """ Test tax on a person with $0 income. """
        # $0 should return $0 in tax owing. This is the easiest test.
        self.person.gross_income = 0
        self.assertEqual(self.tax(self.person, self.initial_year), 0)

    def test_income_under_deduction(self):
        """ Test tax on person with income under personal deduction. """
        self.person.gross_income = (
            self.personal_deduction[self.initial_year] / 2)
        # Should return $0
        self.assertEqual(self.tax(self.person, self.initial_year), 0)

    def test_income_at_deduction(self):
        """ Call Test on income equal to the personal deduction. """
        self.person.gross_income = self.personal_deduction[self.initial_year]
        # Should return $0
        self.assertEqual(self.tax(self.person, self.initial_year), 0)

    def test_income_in_bracket_1_money(self):
        """ Call Test on income mid-way into the lowest tax bracket. """
        # NOTE: brackets[0] is $0; we need something between brackets[0]
        # and brackets[1])
        income = self.brackets[1] / 2
        self.assertEqual(
            self.tax(income, self.initial_year),
            income * self.tax_brackets[self.initial_year][self.brackets[0]])

    def test_income_in_bracket_1_person(self):
        """ Call Test on income mid-way into the lowest tax bracket. """
        # NOTE: brackets[0] is $0; we need something between brackets[0]
        # and brackets[1])
        self.person.gross_income = (self.brackets[1] / 2 +
                                    self.personal_deduction[self.initial_year])
        self.assertEqual(
            self.tax(self.person, self.initial_year),
            (self.person.gross_income -
             self.personal_deduction[self.initial_year]) *
            self.tax_brackets[self.initial_year][self.brackets[0]])

    def test_income_at_bracket_1_money(self):
        """ Call Test on income equal to the lowest tax bracket. """
        # Try a value that's at the limit of the lowest tax
        # bracket (NOTE: brackets are inclusive, so brackets[1] is
        # entirely taxed at the rate associated with brackets[0])
        income = self.brackets[1]
        self.assertEqual(self.tax(income, self.initial_year),
                         self.accum[self.initial_year][self.brackets[1]])

    def test_income_at_bracket_1_person(self):
        """ Call Test on income equal to the lowest tax bracket. """
        # Try a value that's at the limit of the lowest tax
        # bracket (NOTE: brackets are inclusive, so brackets[1] is
        # entirely taxed at the rate associated with brackets[0])
        self.person.gross_income = (self.brackets[1] +
                                    self.personal_deduction[self.initial_year])
        self.assertEqual(self.tax(self.person, self.initial_year),
                         self.accum[self.initial_year][self.brackets[1]])

    def test_income_in_bracket_2_money(self):
        """ Call Test on income mid-way into the second tax bracket. """
        # Find a value that's mid-way into the next (second) bracket.
        # Assuming a person deduction of $100 and tax rates bounded at
        # $0, $100 and $10000 with 10%, 20%, and 30% rates, this gives:
        #   Tax on first $100:  $0
        #   Tax on next $100:   $10
        #   Tax on remaining:   20% of remaining
        # For a $5150 amount, this works out to tax of $1000.
        income = (self.brackets[1] + self.brackets[2]) / 2
        self.assertEqual(
            self.tax(income, self.initial_year),
            self.accum[self.initial_year][self.brackets[1]] +
            ((self.brackets[1] + self.brackets[2]) / 2 - self.brackets[1]) *
            self.tax_brackets[self.initial_year][self.brackets[1]])

    def test_income_in_bracket_2_person(self):
        """ Call Test on income mid-way into the second tax bracket. """
        # Find a value that's mid-way into the next (second) bracket.
        # Assuming a person deduction of $100 and tax rates bounded at
        # $0, $100 and $10000 with 10%, 20%, and 30% rates, this gives:
        #   Tax on first $100:  $0
        #   Tax on next $100:   $10
        #   Tax on remaining:   20% of remaining
        # For a $5150 amount, this works out to tax of $1000.
        self.person.gross_income = ((self.brackets[1] + self.brackets[2]) / 2 +
                                    self.personal_deduction[self.initial_year])
        target = (self.accum[self.initial_year][self.brackets[1]] + (
            (self.brackets[1] + self.brackets[2]) / 2 - self.brackets[1]) *
                  self.tax_brackets[self.initial_year][self.brackets[1]])
        self.assertEqual(self.tax(self.person, self.initial_year), target)

    def test_income_at_bracket_2_money(self):
        """ Call Test on income equal to the second tax bracket. """
        # Try again for a value that's at the limit of the second tax
        # bracket (NOTE: brackets are inclusive, so brackets[2] is
        # entirely taxed at the rate associated with brackets[1])
        income = self.brackets[2]
        self.assertEqual(
            self.tax(income, self.initial_year),
            self.accum[self.initial_year][self.brackets[1]] +
            (self.brackets[2] - self.brackets[1]) *
            self.tax_brackets[self.initial_year][self.brackets[1]])

    def test_income_at_bracket_2_person(self):
        """ Call Test on income equal to the second tax bracket. """
        # Try again for a value that's at the limit of the second tax
        # bracket (NOTE: brackets are inclusive, so brackets[2] is
        # entirely taxed at the rate associated with brackets[1])
        self.person.gross_income = (self.brackets[2] +
                                    self.personal_deduction[self.initial_year])
        self.assertEqual(
            self.tax(self.person, self.initial_year),
            self.accum[self.initial_year][self.brackets[1]] +
            (self.brackets[2] - self.brackets[1]) *
            self.tax_brackets[self.initial_year][self.brackets[1]])

    def test_income_in_bracket_3_money(self):
        """ Call Test on income in the highest tax bracket. """
        # Find a value that's somewhere in the highest (unbounded) bracket.
        bracket = max(self.brackets)
        income = bracket * 2
        self.assertEqual(
            self.tax(income, self.initial_year),
            self.accum[self.initial_year][bracket] +
            bracket * self.tax_brackets[self.initial_year][bracket])

    def test_income_in_bracket_3_person(self):
        """ Call Test on income in the highest tax bracket. """
        # Find a value that's somewhere in the highest (unbounded) bracket.
        bracket = max(self.brackets)
        self.person.gross_income = (bracket * 2 +
                                    self.personal_deduction[self.initial_year])
        self.assertEqual(
            self.tax(self.person, self.initial_year),
            self.accum[self.initial_year][bracket] +
            bracket * self.tax_brackets[self.initial_year][bracket])

    def test_taxpayer_single(self):
        """ Call test on a single taxpayer. """
        # The tax paid on the person's income should be the same as if
        # we calculated the tax directly on the money itself (after
        # accounting for the personal deduction amount)
        self.assertEqual(
            self.tax(self.person1, self.initial_year),
            self.tax(
                self.person1_taxable_income -
                self.personal_deduction[self.initial_year], self.initial_year))
        # We should get a similar result on the other person:
        self.assertEqual(
            self.tax(self.person2, self.initial_year),
            self.tax(
                self.person2_taxable_income -
                self.personal_deduction[self.initial_year], self.initial_year))

    def test_taxpayer_single_set(self):
        """ Call test on a set with a single taxpayer member. """
        # Try with a single-member set; should return the same as
        # it would if calling on the person directly.
        self.assertEqual(self.tax({self.person1}, self.initial_year),
                         self.tax(self.person1, self.initial_year))

    def test_taxpayer_set(self):
        """ Call Test on a set of two non-spouse taxpayers. """
        # The two test people are set up as spouses; we need to split
        # them up.
        self.person1.spouse = None
        self.person2.spouse = None
        test_result = (self.tax({self.person1, self.person2},
                                self.initial_year))
        test_target = (self.tax(self.person1, self.initial_year) +
                       self.tax(self.person2, self.initial_year))
        self.assertEqual(test_result, test_target)

    def test_taxpayer_spouses(self):
        """ Call Test on a set of two spouse taxpayers. """
        # NOTE: This test is vulnerable to breakage if special tax
        # credits get implemented for spouses. Watch out for that.
        self.assertEqual(
            self.tax({self.person1, self.person2}, self.initial_year),
            self.tax(self.person1, self.initial_year) +
            self.tax(self.person2, self.initial_year))

    def test_inflation_adjust(self):
        """ Call Test on a future year with inflation effects. """
        # Start with a baseline result in initial_year. Then confirm
        # that the tax owing on twice that amount in double_year should
        # be exactly double the tax owing on the baseline result.
        # (Anything else suggests that something is not being inflation-
        # adjusted properly, e.g. a bracket or a deduction)
        double_tax = self.tax(self.person1_taxable_income * 2,
                              self.double_year)
        single_tax = self.tax(self.person1_taxable_income, self.initial_year)
        self.assertEqual(single_tax * 2, double_tax)

    def test_payment_timing(self):
        """ Tests `payment_timing` property. """
        # `payment_timing` should have exactly one timing: 0
        self.tax.payment_timing = 'start'
        self.assertEqual(set(self.tax.payment_timing), {0})

    def test_refund_timing(self):
        """ Tests `refund_timing` property. """
        # `refund_timing` should have exactly one timing: 0
        self.tax.refund_timing = 'start'
        self.assertEqual(set(self.tax.refund_timing), {0})

    def test_decimal(self):
        """ Call Test with Decimal values. """
        # Convert values to Decimal:
        self.setUp_decimal()
        # This test is based on test_income_in_bracket_2_person
        self.person.gross_income = ((self.brackets[1] + self.brackets[2]) / 2 +
                                    self.personal_deduction[self.initial_year])
        target = (self.accum[self.initial_year][self.brackets[1]] + (
            (self.brackets[1] + self.brackets[2]) / 2 - self.brackets[1]) *
                  self.tax_brackets[self.initial_year][self.brackets[1]])
        self.assertEqual(self.tax(self.person, self.initial_year), target)
Example #13
0
    def setUp(self):
        self.initial_year = 2000
        # Build 100 years of inflation adjustments with steadily growing
        # adjustment factors. First, pick a nice number (ideally a power
        # of 2 to avoid float precision issues); inflation_adjustment
        # will grow by adding the inverse (1/n) of this number annually
        growth_factor = 32
        year_range = range(self.initial_year, self.initial_year + 100)
        self.inflation_adjustments = {
            year: 1 + (year - self.initial_year) / growth_factor
            for year in year_range
        }
        # For convenience, store the year where inflation has doubled
        # the nominal value of money
        self.double_year = self.initial_year + growth_factor
        # Build some brackets with nice round numbers:
        self.tax_brackets = {self.initial_year: {0: 0.1, 100: 0.2, 10000: 0.3}}
        # For convenience, build a sorted, type-converted array of
        # each of the tax bracket thresholds:
        self.brackets = sorted({
            key: self.tax_brackets[self.initial_year][key]
            for key in self.tax_brackets[self.initial_year].keys()
        })
        # For convenience in testing, build an accum dict that
        # corresponds to the tax brackets above.
        self.accum = {self.initial_year: {0: 0, 100: 10, 10000: 1990}}
        self.personal_deduction = {self.initial_year: 100}
        self.credit_rate = {self.initial_year: 0.1}

        self.tax = Tax(self.tax_brackets,
                       inflation_adjust=self.inflation_adjustments,
                       personal_deduction=self.personal_deduction,
                       credit_rate=self.credit_rate)

        # Set up a simple person with no account-derived taxable income
        self.person = Person(self.initial_year,
                             "Tester",
                             self.initial_year - 25,
                             retirement_date=self.initial_year + 40,
                             gross_income=0)

        # Set up two people, spouses, on which to do more complex tests
        self.person1 = Person(self.initial_year,
                              "Tester 1",
                              self.initial_year - 20,
                              retirement_date=self.initial_year + 45,
                              gross_income=100000)
        self.person2 = Person(self.initial_year,
                              "Tester 2",
                              self.initial_year - 22,
                              retirement_date=self.initial_year + 43,
                              gross_income=50000)

        # Give the first person two accounts, one taxable and one
        # tax-deferred. Withdraw the entirety from the taxable account,
        # so that we don't need to worry about tax on unrealized growth:
        self.taxable_account1 = TaxableAccount(owner=self.person1,
                                               acb=0,
                                               balance=50000,
                                               rate=0.05,
                                               nper=1)
        self.taxable_account1.add_transaction(-50000, when='start')
        self.rrsp = RRSP(owner=self.person1,
                         inflation_adjust=self.inflation_adjustments,
                         contribution_room=0,
                         balance=10000,
                         rate=0.05,
                         nper=1)
        # Employment income is fully taxable, and only half of capital
        # gains (the income from the taxable account) is taxable:
        self.person1_taxable_income = (self.person1.gross_income +
                                       self.taxable_account1.balance / 2)

        # Give the second person two accounts, one taxable and one
        # non-taxable. Withdraw the entirety from the taxable account,
        # so that we don't need to worry about tax on unrealized growth,
        # and withdraw a bit from the non-taxable account (which should
        # have no effect on taxable income):
        self.taxable_account2 = TaxableAccount(owner=self.person2,
                                               acb=0,
                                               balance=20000,
                                               rate=0.05,
                                               nper=1)
        self.taxable_account2.add_transaction(-20000, when='start')
        self.tfsa = TFSA(owner=self.person2, balance=50000, rate=0.05, nper=1)
        self.tfsa.add_transaction(-20000, when='start')
        # Employment income is fully taxable, and only half of capital
        # gains (the income from the taxable account) is taxable:
        self.person2_taxable_income = (self.person2.gross_income +
                                       self.taxable_account2.balance / 2)
Example #14
0
class TestTransactionStrategyOrdered(TestCaseTransactions):
    """ A test case for TransactionStrategy.strategy_ordered """

    def setUp(self):
        """ Sets up variables for testing. """
        # Different groups of tests use different groups of variables.
        # We could split this class up into multiple classes (perhaps
        # one for ordered strategies and one for weighted strategies?),
        # but for now this project's practice is one test case for each
        # custom class.
        # pylint: disable=too-many-instance-attributes

        initial_year = 2000
        person = Person(
            initial_year, 'Testy McTesterson', 1980, retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(
            person,
            balance=Money(200), rate=0, contribution_room=Money(200))
        self.tfsa = TFSA(
            person,
            balance=Money(100), rate=0, contribution_room=Money(100))
        self.taxable_account = TaxableAccount(
            person, balance=Money(1000), rate=0)
        self.accounts = {self.rrsp, self.tfsa, self.taxable_account}

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): 1}

        # Build strategy for testing (in non-init tests):
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_ordered, {
                'RRSP': 1,
                'TFSA': 2,
                'TaxableAccount': 3
            })

    # Test with inflows:

    def test_in_basic(self):
        """ Test strategy_ordered with a small amount of inflows. """
        # The amount being contributed is less than the available
        # contribution room in the top-weighted account type.
        available = make_available(Money(100), self.timing)
        results = self.strategy(available, self.accounts)
        self.assertTransactions(results[self.rrsp], Money(100))
        # Accounts with no transactions aren't guaranteed to be
        # included in the results dict:
        if self.tfsa in results:
            self.assertTransactions(results[self.tfsa], Money(0))
        if self.taxable_account in results:
            self.assertTransactions(results[self.taxable_account], Money(0))

    def test_in_fill_one(self):
        """ Test strategy_ordered with inflows to fill 1 account. """
        # Contribute more than the rrsp will accomodate.
        # The extra $50 should go to the tfsa, which is next in line.
        available = make_available(Money(250), self.timing)
        results = self.strategy(available, self.accounts)
        self.assertTransactions(results[self.rrsp], Money(200))
        self.assertTransactions(results[self.tfsa], Money(50))
        if self.taxable_account in results:
            self.assertTransactions(results[self.taxable_account], Money(0))

    def test_in_fill_two(self):
        """ Test strategy_ordered with inflows to fill 2 accounts. """
        # The rrsp and tfsa will get filled and the remainder will go to
        # the taxable account.
        available = make_available(Money(1000), self.timing)
        results = self.strategy(available, self.accounts)
        self.assertTransactions(results[self.rrsp], Money(200))
        self.assertTransactions(results[self.tfsa], Money(100))
        self.assertTransactions(results[self.taxable_account], Money(700))

    # Test with outflows:

    def test_out_basic(self):
        """ Test strategy_ordered with a small amount of outflows. """
        # The amount being withdrawn is less than the max outflow in the
        # top-weighted account type.
        available = make_available(Money(-100), self.timing)
        results = self.strategy(available, self.accounts)
        self.assertTransactions(results[self.rrsp], Money(-100))
        if self.tfsa in results:
            self.assertTransactions(results[self.tfsa], Money(0))
        if self.taxable_account in results:
            self.assertTransactions(results[self.taxable_account], Money(0))

    def test_out_empty_one(self):
        """ Test strategy_ordered with outflows to empty 1 account. """
        # Now withdraw more than the rrsp will accomodate. The extra $50
        # should come from the tfsa, which is next in line.
        available = make_available(Money(-250), self.timing)
        results = self.strategy(available, self.accounts)
        self.assertTransactions(results[self.rrsp], Money(-200))
        self.assertTransactions(results[self.tfsa], Money(-50))
        if self.taxable_account in results:
            self.assertTransactions(results[self.taxable_account], Money(0))

    def test_out_empty_two(self):
        """ Test strategy_ordered with outflows to empty 2 accounts. """
        # The rrsp and tfsa will get emptied and the remainder will go
        # to the taxable account.
        available = make_available(Money(-1000), self.timing)
        results = self.strategy(available, self.accounts)
        self.assertTransactions(results[self.rrsp], self.rrsp.max_outflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        self.assertTransactions(results[self.taxable_account], Money(-700))

    def test_out_empty_all(self):
        """ Test strategy_ordered with outflows to empty all account. """
        # Try withdrawing more than all of the accounts have:
        val = sum(
            sum(account.max_outflows().values())
            for account in self.accounts
        ) * 2
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        self.assertTransactions(results[self.rrsp], self.rrsp.max_outflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        self.assertTransactions(
            results[self.taxable_account],
            self.taxable_account.max_outflows())

    def test_change_order(self):
        """ Test strategy_ordered works with changed order vars. """
        self.strategy.weights['RRSP'] = 2
        self.strategy.weights['TFSA'] = 1
        available = make_available(Money(100), self.timing)
        results = self.strategy(available, self.accounts)
        # Contribute the full $100 to TFSA:
        self.assertTransactions(results[self.tfsa], self.tfsa.max_inflows())
        # Remaining accounts shouldn't be contributed to:
        if self.rrsp in results:
            self.assertTransactions(results[self.rrsp], Money(0))
        if self.taxable_account in results:
            self.assertTransactions(results[self.taxable_account], Money(0))
Example #15
0
class TestTransactionStrategyWeightedLink(TestCaseTransactions):
    """ Tests TransactionStrategy.strategy_weighted with linked accounts
    
    This test case includes multiple linked accounts (e.g. two RRSPs)
    to ensure that accounts that share a weighting are handled properly.
    """

    def setUp(self):
        """ Sets up variables for testing. """
        # Vars for building accounts:
        initial_year = 2000
        person = Person(
            initial_year, 'Testy McTesterson', 1980, retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(
            person,
            balance=Money(200), rate=0, contribution_room=Money(200))
        self.rrsp2 = RRSP(person, balance=Money(100), rate=0)
        self.tfsa = TFSA(
            person,
            balance=Money(100), rate=0, contribution_room=Money(100))
        self.taxable_account = TaxableAccount(
            person, balance=Money(1000), rate=0)
        self.accounts = {
            self.rrsp, self.rrsp2, self.tfsa, self.taxable_account
        }

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): 1}

        # Build strategy for testing (in non-init tests):
        self.weights = {
            'RRSP': Decimal('0.4'),
            'TFSA': Decimal('0.3'),
            'TaxableAccount': Decimal('0.3')
        }
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_weighted, self.weights)

    def test_out_basic(self):
        """ Test strategy_weighted with multiple RRSPs, small outflows. """
        # Amount withdrawn is less than the balance of each account.
        val = Money(
            max(sum(account.max_outflows().values())
                for account in self.accounts))
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Sum up results for each account for convenience:
        results_totals = {
            account: sum(transactions.values())
            for account, transactions in results.items()}
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Confirm RRSPs' shared weight is respected:
        self.assertAlmostEqual(
            results_totals[self.rrsp] + results_totals[self.rrsp2],
            val * self.weights['RRSP'])
        # Confirm that money is withdrawn from each RRSP, but don't
        # put constraints on how much:
        self.assertLess(results_totals[self.rrsp], Money(0))
        self.assertLess(results_totals[self.rrsp2], Money(0))
        # Confirm that remaining accounts have expected amounts:
        self.assertTransactions(results[self.tfsa], val * self.weights['TFSA'])
        self.assertTransactions(
            results[self.taxable_account],
            val * self.weights['TaxableAccount'])

    def test_out_empty_one(self):
        """ Test strategy_weighted with multiple RRSPs, empty 1 account. """
        # Withdraw enough to exceed the balance of one account (the
        # TFSA, in this case, as it has the smallest balance):
        threshold = (
            sum(self.tfsa.max_outflows().values())
            / self.weights['TFSA'])
        val = Money(threshold - Money(50))
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Sum up results for each account for convenience:
        results_totals = {
            account: sum(transactions.values())
            for account, transactions in results.items()}
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Confirm each account has the expected set of transactions:
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        # The excess (i.e. the amount that would ordinarily be
        # contributed to the TFSA but can't due to contribution room
        # limits) should also be split between RRSPs and the TFSA
        # proportionately to their relative weights.
        self.assertAlmostEqual(
            results_totals[self.rrsp] + results_totals[self.rrsp2],
            results_totals[self.taxable_account]
            * self.weights['RRSP'] / self.weights['TaxableAccount'])

    def test_out_empty_three(self):
        """ Test strategy_weighted with mult. RRSPs, empty 3 accounts. """
        # Try withdrawing just a little less than the total available
        # balance. This will clear out the RRSPs and TFSA and leave the
        # remainder in the taxable account, since the taxable account
        # has a much larger balance and roughly similar weight:
        val = sum(
            sum(account.max_outflows().values())
            for account in self.accounts
        ) + Money(50)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Sum up results for each account for convenience:
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Also confirm that the smaller accounts get emptied:
        self.assertTransactions(results[self.rrsp], self.rrsp.max_outflows())
        self.assertTransactions(results[self.rrsp2], self.rrsp2.max_outflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        # And confirm that the largest account is not-quite-filled:
        self.assertTransactions(
            results[self.taxable_account],
            sum(self.taxable_account.max_outflows().values()) + Money(50))

    def test_out_empty_all(self):
        """ Test strategy_weighted with mult. RRSPs, empty all accounts. """
        # Try withdrawing more than the accounts have
        val = sum(
            sum(account.max_outflows().values())
            for account in self.accounts
        ) - Money(50)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm each account has the expected set of transactions:
        self.assertTransactions(results[self.rrsp], self.rrsp.max_outflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        self.assertTransactions(
            results[self.taxable_account],
            self.taxable_account.max_outflows())

    def test_in_basic(self):
        """ Test strategy_weighted with multiple RRSPs, small inflows. """
        # Amount contributed is more than the RRSPs can receive:
        val = self.rrsp.contribution_room / self.weights['RRSP'] + Money(50)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Sum up results for each account for convenience:
        results_totals = {
            account: sum(transactions.values())
            for account, transactions in results.items()}
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)

        # Confirm that the total amount contributed to the RRSPs is
        # equal to their (shared) contribution room.
        # If it exceeds that limit, then it's likely that their
        # contribution room sharing isn't being respected.
        self.assertAlmostEqual(
            results_totals[self.rrsp] + results_totals[self.rrsp2],
            self.rrsp.contribution_room)
Example #16
0
class TestTransactionStrategyWeighted(TestCaseTransactions):
    """ A test case for TransactionStrategy.strategy_weighted. """

    def setUp(self):
        """ Sets up variables for testing. """

        # Vars for building accounts:
        initial_year = 2000
        person = Person(
            initial_year, 'Testy McTesterson', 1980, retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(
            person,
            balance=Money(200), rate=0, contribution_room=Money(200))
        self.tfsa = TFSA(
            person,
            balance=Money(100), rate=0, contribution_room=Money(100))
        self.taxable_account = TaxableAccount(
            person, balance=Money(1000), rate=0)
        self.accounts = {self.rrsp, self.tfsa, self.taxable_account}

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): 1}

        self.max_outflow = sum(
            sum(account.max_outflows(timing=self.timing).values())
            for account in self.accounts)
        self.max_inflows = sum(
            sum(account.max_inflows(timing=self.timing).values())
            for account in self.accounts)

        # Build strategy for testing (in non-init tests):
        self.weights = {
            'RRSP': Decimal('0.4'),
            'TFSA': Decimal('0.3'),
            'TaxableAccount': Decimal('0.3')
        }
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_weighted, self.weights)

    # Test with outflows:

    def test_out_basic(self):
        """ Test strategy_weighted with small amount of outflows. """
        # Amount withdrawn is smaller than the balance of each account.
        val = max(
            sum(account.max_outflows().values())
            for account in self.accounts)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Confirm each account gets the expected total transactions:
        self.assertTransactions(results[self.rrsp], val * self.weights['RRSP'])
        self.assertTransactions(results[self.tfsa], val * self.weights['TFSA'])
        self.assertTransactions(
            results[self.taxable_account],
            val * self.weights['TaxableAccount'])

    def test_out_one_empty(self):
        """ Test strategy_weighted with outflows to empty 1 account. """
        # Now withdraw enough to exceed the TFSA's balance, plus a bit.
        threshold = (
            sum(self.tfsa.max_outflows().values())
            / self.weights['TFSA'])
        val = Money(threshold - Money(50))
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Sum up results for each account for convenience:
        results_totals = {
            account: sum(transactions.values())
            for account, transactions in results.items()}
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Confirm each account gets the expected total transactions:
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        self.assertAlmostEqual(
            results_totals[self.rrsp] / self.weights['RRSP'],
            results_totals[self.taxable_account]
            / self.weights['TaxableAccount'])

    def test_out_two_empty(self):
        """ Test strategy_weighted with outflows to empty 2 accounts. """
        # Withdraw just a little less than the total available balance.
        # This will clear out the RRSP and TFSA.
        val = sum(
            sum(account.max_outflows().values())
            for account in self.accounts
        ) + Money(50)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Confirm each account gets the expected total transactions:
        self.assertTransactions(results[self.rrsp], self.rrsp.max_outflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        self.assertTransactions(
            results[self.taxable_account],
            sum(self.taxable_account.max_outflows().values())
            + Money(50))

    def test_out_all_empty(self):
        """ Test strategy_weighted with outflows to empty all accounts. """
        # Withdraw more than the accounts have:
        val = sum(
            sum(account.max_outflows().values())
            for account in self.accounts
        ) - Money(50)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm each account gets the expected total transactions:
        self.assertTransactions(results[self.rrsp], self.rrsp.max_outflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        self.assertTransactions(
            results[self.taxable_account],
            self.taxable_account.max_outflows())

    def test_in_basic(self):
        """ Test strategy_weighted with a small amount of inflows. """
        # The amount being contributed is less than the available
        # contribution room for each account
        val = min(
            sum(account.max_inflows().values())
            for account in self.accounts)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Confirm accounts have separate total transaction values:
        self.assertTransactions(results[self.rrsp], val * self.weights['RRSP'])
        self.assertTransactions(results[self.tfsa], val * self.weights['TFSA'])
        self.assertTransactions(
            results[self.taxable_account],
            val * self.weights['TaxableAccount'])

    def test_in_fill_one(self):
        """ Test strategy_weighted with inflows to fill 1 account. """
        # Now contribute enough to exceed the TFSA's contribution room.
        # The excess (i.e. the amount that would be contributed to the
        # TFSA but can't because of its lower contribution room) should
        # be redistributed to the other accounts proportionately to
        # their relative weights:
        threshold = (
            sum(self.tfsa.max_inflows().values()) / self.weights['TFSA'])
        val = Money(threshold + Money(50))
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)

        self.assertTransactions(results[self.tfsa], self.tfsa.max_inflows())
        self.assertTransactions(
            results[self.rrsp],
            sum(results[self.taxable_account].values())
            * self.weights['RRSP'] / self.weights['TaxableAccount'])

    def test_in_fill_two(self):
        """ Test strategy_weighted with inflows to fill 2 accounts. """
        # Contribute a lot of money - the rrsp and tfsa will get
        # filled and the remainder will go to the taxable account.
        threshold = max(
            sum(self.rrsp.max_inflows().values()) / self.weights['RRSP'],
            sum(self.tfsa.max_inflows().values()) / self.weights['TFSA'])
        val = threshold + Money(50)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Confirm accounts have expected transactions:
        self.assertTransactions(results[self.rrsp], self.rrsp.max_inflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_inflows())
        self.assertTransactions(
            results[self.taxable_account],
            val - (
                sum(self.rrsp.max_inflows().values())
                + sum(self.tfsa.max_inflows().values())))
Example #17
0
class TestTransactionStrategyOrderedMult(TestCaseTransactions):
    """ Tests TransactionStrategy.strategy_ordered with account groups.
    In particular, this test case includes multiple accounts of the same
    type (e.g. two RRSPs) to ensure that accounts that share a weighting
    are handled properly.
    """

    def setUp(self):
        """ Sets up variables for testing. """
        initial_year = 2000
        person = Person(
            initial_year, 'Testy McTesterson', 1980, retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(
            person,
            balance=Money(200), rate=0, contribution_room=Money(200))
        self.rrsp2 = RRSP(person, balance=Money(100), rate=0)
        self.tfsa = TFSA(
            person,
            balance=Money(100), rate=0, contribution_room=Money(100))
        self.taxable_account = TaxableAccount(
            person, balance=Money(1000), rate=0)
        self.accounts = {
            self.rrsp, self.rrsp2, self.tfsa, self.taxable_account
        }

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): 1}

        self.max_outflow = sum(
            sum(account.max_outflows(timing=self.timing).values())
            for account in self.accounts)
        self.max_inflows = sum(
            sum(account.max_inflows(timing=self.timing).values())
            for account in self.accounts)

        # Build strategies for testing (in non-init tests):
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_ordered, {
                'RRSP': 1,
                'TFSA': 2,
                'TaxableAccount': 3
            })

    def test_out_basic(self):
        """ Test strategy_ordered with multiple RRSPs, small outflows. """
        available = make_available(Money(-150), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm that the total of all outflows sums up to `-$150`,
        # which should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, Money(-150))
        self.assertAlmostEqual(
            sum(results[self.rrsp].values())
            + sum(results[self.rrsp2].values()),
            Money(-150))

    def test_out_empty_three(self):
        """ Test strategy_ordered with multiple RRSPs, empty 3 accounts. """
        available = make_available(Money(-400), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm that the total of all outflows sums up to -$400, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, Money(-400))
        self.assertTransactions(results[self.rrsp], Money(-200))
        self.assertTransactions(results[self.rrsp2], Money(-100))
        self.assertTransactions(results[self.tfsa], Money(-100))

    def test_out_empty_all(self):
        """ Test strategy_ordered with multiple RRSPs, empty all accounts. """
        # Try to withdraw more than all accounts combined contain:
        val = self.max_outflow * 2
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Ensure that the correct amount is withdrawn in total; should
        # be the amount available in the accounts (i.e. their balances):
        self.assertAccountTransactionsTotal(
            results,
            -sum(account.balance for account in self.accounts))
        # Confirm balances for each account:
        self.assertTransactions(results[self.rrsp], self.rrsp.max_outflows())
        self.assertTransactions(results[self.rrsp2], self.rrsp2.max_outflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        self.assertTransactions(
            results[self.taxable_account], self.taxable_account.max_outflows())

    def test_in_basic(self):
        """ Test strategy_ordered with multiple RRSPs, small inflows. """
        # Amount contributed is more than the RRSPs can receive:
        val = self.rrsp.contribution_room + Money(50)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)

        # Confirm that the total amount contributed to the RRSPs is
        # equal to their (shared) contribution room.
        # If it exceeds that limit, then it's likely that their
        # contribution room sharing isn't being respected.
        self.assertAlmostEqual(
            sum(results[self.rrsp].values())
            + sum(results[self.rrsp2].values()),
            self.rrsp.contribution_room)
        # The remainder should be contributed to the TFSA:
        self.assertTransactions(results[self.tfsa], Money(50))
Example #18
0
    def setUp_decimal(self):  # pylint: disable=invalid-name
        """ Sets up mutable variables for each test call. """
        # Set up constants:
        self.initial_year = 2000
        self.constants = constants.ConstantsCanada(high_precision=Decimal)

        # Modify constants to make math easier:
        # Build some brackets with nice round numbers:
        self.constants.TAX_BRACKETS = {
            'Federal': {
                self.initial_year: {
                    Decimal(0): Decimal('0.1'),
                    Decimal(100): Decimal('0.2'),
                    Decimal(10000): Decimal('0.3')
                }
            },
            'BC': {
                self.initial_year: {
                    Decimal(0): Decimal('0.25'),
                    Decimal(1000): Decimal('0.5'),
                    Decimal(100000): Decimal('0.75')
                }
            }
        }
        self.constants.TAX_PERSONAL_DEDUCTION = {
            'Federal': {
                self.initial_year: Decimal('100')
            },
            'BC': {
                self.initial_year: Decimal('1000')
            }
        }
        self.constants.TAX_CREDIT_RATE = {
            'Federal': {
                self.initial_year: Decimal('0.1')
            },
            'BC': {
                self.initial_year: Decimal('0.25')
            }
        }
        self.constants.TAX_PENSION_CREDIT = {
            'Federal': {
                self.initial_year: Decimal('100')
            },
            'BC': {
                self.initial_year: Decimal('1000')
            }
        }
        # It's convenient (and accurate!) to use the same values
        # for the spousal amount and the personal deduction:
        self.constants.TAX_SPOUSAL_AMOUNT = (
            self.constants.TAX_PERSONAL_DEDUCTION)

        # Build 100 years of inflation adjustments.
        growth_factor = Decimal(32)
        year_range = range(self.initial_year, self.initial_year + 100)
        self.inflation_adjustments = {
            year: 1 + (year - self.initial_year) / growth_factor
            for year in year_range
        }

        # Set to default province:
        self.province = 'BC'
        self.tax = TaxCanada(self.inflation_adjustments,
                             province='BC',
                             constants=self.constants)

        # Set up some people to test on:
        # Person1 makes $100,000/yr, has a taxable account with $500,000
        # taxable income, and an RRSP with $500,000 in taxable income.
        self.person1 = Person(self.initial_year,
                              "Tester 1",
                              self.initial_year - 20,
                              retirement_date=self.initial_year + 45,
                              gross_income=100000)
        self.taxable_account1 = TaxableAccount(owner=self.person1,
                                               acb=0,
                                               balance=Decimal(1000000),
                                               rate=Decimal('0.05'),
                                               nper=1)
        self.taxable_account1.add_transaction(-Decimal(1000000), when='start')
        # NOTE: by using an RRSP here, a pension income tax credit will
        # be applied by TaxCanadaJurisdiction. Be aware of this if you
        # want to test this output against a generic Tax object with
        # Canadian brackets.
        self.rrsp = RRSP(self.person1,
                         inflation_adjust=self.inflation_adjustments,
                         contribution_room=0,
                         balance=Decimal(500000),
                         rate=Decimal('0.05'),
                         nper=1,
                         constants=self.constants)
        self.rrsp.add_transaction(-Decimal(500000), when='start')

        # Person2 makes $50,000/yr and has a taxable account with
        # $5000 taxable income.
        self.person2 = Person(self.initial_year,
                              "Tester 2",
                              self.initial_year - 18,
                              retirement_date=self.initial_year + 47,
                              gross_income=50000)
        self.taxable_account2 = TaxableAccount(owner=self.person2,
                                               acb=0,
                                               balance=Decimal(10000),
                                               rate=Decimal('0.05'),
                                               nper=1)
        self.taxable_account2.add_transaction(-Decimal(10000), when='start')
Example #19
0
class TestTaxCanada(unittest.TestCase):
    """ Tests TaxCanada. """
    def setUp(self):
        """ Sets up mutable variables for each test call. """
        # Set up constants:
        self.initial_year = 2000
        self.constants = constants.ConstantsCanada()

        # Modify constants to make math easier:
        # Build some brackets with nice round numbers:
        self.constants.TAX_BRACKETS = {
            'Federal': {
                self.initial_year: {
                    0: 0.1,
                    100: 0.2,
                    10000: 0.3
                }
            },
            'BC': {
                self.initial_year: {
                    0: 0.25,
                    1000: 0.5,
                    100000: 0.75
                }
            }
        }
        self.constants.TAX_PERSONAL_DEDUCTION = {
            'Federal': {
                self.initial_year: 100
            },
            'BC': {
                self.initial_year: 1000
            }
        }
        self.constants.TAX_CREDIT_RATE = {
            'Federal': {
                self.initial_year: 0.1
            },
            'BC': {
                self.initial_year: 0.25
            }
        }
        self.constants.TAX_PENSION_CREDIT = {
            'Federal': {
                self.initial_year: 100
            },
            'BC': {
                self.initial_year: 1000
            }
        }
        # It's convenient (and accurate!) to use the same values
        # for the spousal amount and the personal deduction:
        self.constants.TAX_SPOUSAL_AMOUNT = (
            self.constants.TAX_PERSONAL_DEDUCTION)

        # Build 100 years of inflation adjustments.
        growth_factor = 32
        year_range = range(self.initial_year, self.initial_year + 100)
        self.inflation_adjustments = {
            year: 1 + (year - self.initial_year) / growth_factor
            for year in year_range
        }

        # Set to default province:
        self.province = 'BC'
        self.tax = TaxCanada(self.inflation_adjustments,
                             province='BC',
                             constants=self.constants)

        # Set up some people to test on:
        # Person1 makes $100,000/yr, has a taxable account with $500,000
        # taxable income, and an RRSP with $500,000 in taxable income.
        self.person1 = Person(self.initial_year,
                              "Tester 1",
                              self.initial_year - 20,
                              retirement_date=self.initial_year + 45,
                              gross_income=100000)
        self.taxable_account1 = TaxableAccount(owner=self.person1,
                                               acb=0,
                                               balance=1000000,
                                               rate=0.05,
                                               nper=1)
        self.taxable_account1.add_transaction(-1000000, when='start')
        # NOTE: by using an RRSP here, a pension income tax credit will
        # be applied by TaxCanadaJurisdiction. Be aware of this if you
        # want to test this output against a generic Tax object with
        # Canadian brackets.
        self.rrsp = RRSP(self.person1,
                         inflation_adjust=self.inflation_adjustments,
                         contribution_room=0,
                         balance=500000,
                         rate=0.05,
                         nper=1,
                         constants=self.constants)
        self.rrsp.add_transaction(-500000, when='start')

        # Person2 makes $50,000/yr and has a taxable account with
        # $5000 taxable income.
        self.person2 = Person(self.initial_year,
                              "Tester 2",
                              self.initial_year - 18,
                              retirement_date=self.initial_year + 47,
                              gross_income=50000)
        self.taxable_account2 = TaxableAccount(owner=self.person2,
                                               acb=0,
                                               balance=10000,
                                               rate=0.05,
                                               nper=1)
        self.taxable_account2.add_transaction(-10000, when='start')

    def setUp_decimal(self):  # pylint: disable=invalid-name
        """ Sets up mutable variables for each test call. """
        # Set up constants:
        self.initial_year = 2000
        self.constants = constants.ConstantsCanada(high_precision=Decimal)

        # Modify constants to make math easier:
        # Build some brackets with nice round numbers:
        self.constants.TAX_BRACKETS = {
            'Federal': {
                self.initial_year: {
                    Decimal(0): Decimal('0.1'),
                    Decimal(100): Decimal('0.2'),
                    Decimal(10000): Decimal('0.3')
                }
            },
            'BC': {
                self.initial_year: {
                    Decimal(0): Decimal('0.25'),
                    Decimal(1000): Decimal('0.5'),
                    Decimal(100000): Decimal('0.75')
                }
            }
        }
        self.constants.TAX_PERSONAL_DEDUCTION = {
            'Federal': {
                self.initial_year: Decimal('100')
            },
            'BC': {
                self.initial_year: Decimal('1000')
            }
        }
        self.constants.TAX_CREDIT_RATE = {
            'Federal': {
                self.initial_year: Decimal('0.1')
            },
            'BC': {
                self.initial_year: Decimal('0.25')
            }
        }
        self.constants.TAX_PENSION_CREDIT = {
            'Federal': {
                self.initial_year: Decimal('100')
            },
            'BC': {
                self.initial_year: Decimal('1000')
            }
        }
        # It's convenient (and accurate!) to use the same values
        # for the spousal amount and the personal deduction:
        self.constants.TAX_SPOUSAL_AMOUNT = (
            self.constants.TAX_PERSONAL_DEDUCTION)

        # Build 100 years of inflation adjustments.
        growth_factor = Decimal(32)
        year_range = range(self.initial_year, self.initial_year + 100)
        self.inflation_adjustments = {
            year: 1 + (year - self.initial_year) / growth_factor
            for year in year_range
        }

        # Set to default province:
        self.province = 'BC'
        self.tax = TaxCanada(self.inflation_adjustments,
                             province='BC',
                             constants=self.constants)

        # Set up some people to test on:
        # Person1 makes $100,000/yr, has a taxable account with $500,000
        # taxable income, and an RRSP with $500,000 in taxable income.
        self.person1 = Person(self.initial_year,
                              "Tester 1",
                              self.initial_year - 20,
                              retirement_date=self.initial_year + 45,
                              gross_income=100000)
        self.taxable_account1 = TaxableAccount(owner=self.person1,
                                               acb=0,
                                               balance=Decimal(1000000),
                                               rate=Decimal('0.05'),
                                               nper=1)
        self.taxable_account1.add_transaction(-Decimal(1000000), when='start')
        # NOTE: by using an RRSP here, a pension income tax credit will
        # be applied by TaxCanadaJurisdiction. Be aware of this if you
        # want to test this output against a generic Tax object with
        # Canadian brackets.
        self.rrsp = RRSP(self.person1,
                         inflation_adjust=self.inflation_adjustments,
                         contribution_room=0,
                         balance=Decimal(500000),
                         rate=Decimal('0.05'),
                         nper=1,
                         constants=self.constants)
        self.rrsp.add_transaction(-Decimal(500000), when='start')

        # Person2 makes $50,000/yr and has a taxable account with
        # $5000 taxable income.
        self.person2 = Person(self.initial_year,
                              "Tester 2",
                              self.initial_year - 18,
                              retirement_date=self.initial_year + 47,
                              gross_income=50000)
        self.taxable_account2 = TaxableAccount(owner=self.person2,
                                               acb=0,
                                               balance=Decimal(10000),
                                               rate=Decimal('0.05'),
                                               nper=1)
        self.taxable_account2.add_transaction(-Decimal(10000), when='start')

    def test_init_federal(self):
        """ Test TaxCanada.__init__ for federal jurisdiction. """
        # There's some type-conversion going on, so test the Decimal-
        # valued `amount` of the Tax's tax bracket's keys against the
        # Decimal key object of the Constants tax brackets.
        tax = TaxCanada(self.inflation_adjustments,
                        self.province,
                        constants=self.constants)
        for year in self.constants.TAX_BRACKETS['Federal']:
            self.assertEqual(tax.federal_tax.tax_brackets(year),
                             self.constants.TAX_BRACKETS['Federal'][year])
            self.assertEqual(
                tax.federal_tax.personal_deduction(year),
                self.constants.TAX_PERSONAL_DEDUCTION['Federal'][year])
            self.assertEqual(tax.federal_tax.credit_rate(year),
                             self.constants.TAX_CREDIT_RATE['Federal'][year])
        self.assertTrue(callable(tax.federal_tax.inflation_adjust))
        # Test that the default timings for CRA refunds/payments have
        # been set:
        self.assertEqual(set(tax.payment_timing),
                         {self.constants.TAX_PAYMENT_TIMING})
        self.assertEqual(set(tax.refund_timing),
                         {self.constants.TAX_REFUND_TIMING})

    def test_init_provincial(self):
        """ Test TaxCanada.__init__ for provincial jurisdiction. """
        tax = TaxCanada(self.inflation_adjustments,
                        self.province,
                        constants=self.constants)
        for year in self.constants.TAX_BRACKETS[self.province]:
            self.assertEqual(
                tax.provincial_tax.tax_brackets(year), {
                    bracket: value
                    for bracket, value in self.constants.TAX_BRACKETS[
                        self.province][year].items()
                })
            self.assertEqual(
                tax.provincial_tax.personal_deduction(year),
                self.constants.TAX_PERSONAL_DEDUCTION[self.province][year])
            self.assertEqual(
                tax.provincial_tax.credit_rate(year),
                self.constants.TAX_CREDIT_RATE[self.province][year])
        self.assertTrue(callable(tax.provincial_tax.inflation_adjust))

    def test_init_min_args(self):
        """ Test init when Omitting optional arguments. """
        tax = TaxCanada(self.inflation_adjustments, constants=self.constants)
        for year in self.constants.TAX_BRACKETS[self.province]:
            self.assertEqual(
                tax.provincial_tax.tax_brackets(year), {
                    bracket: value
                    for bracket, value in self.constants.TAX_BRACKETS[
                        self.province][year].items()
                })
            self.assertEqual(
                tax.provincial_tax.personal_deduction(year),
                self.constants.TAX_PERSONAL_DEDUCTION[self.province][year])
            self.assertEqual(
                tax.provincial_tax.credit_rate(year),
                self.constants.TAX_CREDIT_RATE[self.province][year])
        self.assertTrue(callable(tax.provincial_tax.inflation_adjust))

    def test_call_money(self):
        """ Test TaxCanada.__call__ on Decimal input """
        taxable_income = 100000
        self.assertEqual(
            self.tax(taxable_income, self.initial_year),
            self.tax.federal_tax(taxable_income, self.initial_year) +
            self.tax.provincial_tax(taxable_income, self.initial_year))

    def test_call_person(self):
        """ Test TaxCanada.__call__ on one Person input """
        self.assertEqual(
            self.tax(self.person1, self.initial_year),
            self.tax.federal_tax(self.person1, self.initial_year) +
            self.tax.provincial_tax(self.person1, self.initial_year))

    def test_call_person_set(self):
        """ Test TaxCanada.__call__ on a one-Person set input """
        # Should get the same result as for a setless Person:
        self.assertEqual(self.tax({self.person1}, self.initial_year),
                         self.tax(self.person1, self.initial_year))

    def test_call_people(self):
        """ Test TaxCanada.__call__ on a set of multiple people. """
        # The people are unrelated, so should get a result which is
        # just the sum of their tax treatments.
        self.assertEqual(
            self.tax({self.person1, self.person2}, self.initial_year),
            self.tax.federal_tax({self.person1, self.person2},
                                 self.initial_year) +
            self.tax.provincial_tax({self.person1, self.person2},
                                    self.initial_year))

    def test_spousal_tax_credit(self):
        """ Test spousal tax credit behaviour. """
        # Ensure person 2's net income is less than the federal spousal
        # amount:
        spousal_amount = (
            self.constants.TAX_SPOUSAL_AMOUNT['Federal'][self.initial_year])
        shortfall = spousal_amount / 2
        deduction = self.tax.federal_tax.deduction(self.person2,
                                                   self.initial_year)
        self.person2.gross_income = deduction + spousal_amount - shortfall

        # Ensure that there is no taxable income for person2 beyond the
        # above (to stay under spousal amount):
        self.taxable_account2.owner = self.person1

        # Get a tax treatment baseline for unrelated people:
        baseline_tax = self.tax.federal_tax({self.person1, self.person2},
                                            self.initial_year)

        # Wed the two people in holy matrimony:
        self.person1.spouse = self.person2

        # Now determine total tax liability federally:
        spousal_tax = self.tax.federal_tax({self.person1, self.person2},
                                           self.initial_year)

        # Tax should be reduced (relative to baseline) by the shortfall
        # of person2's income (relative to the spousal amount, after
        # applying deductions), scaled down by the credit rate.
        # That is, for every dollar that person2 earns _under_ the
        # spousal amount, tax is reduced by (e.g.) 15 cents (assuming
        # a credit rate of 15%)
        target = baseline_tax - (
            shortfall * self.tax.federal_tax.credit_rate(self.initial_year))

        # The different between these scenarios should be equal to
        # the amount of the spousal tax credit:
        self.assertEqual(spousal_tax, target)

    def test_pension_tax_credit(self):
        """ Test pension tax credit behaviour. """
        # TODO Implement pension tax credit, then test it.
        pass