def __init__(self, tax_brackets_configuration):
        self.tax_brackets = []
        for bracket in tax_brackets_configuration['brackets']:
            new_bracket = TaxBracket(bracket['income'], bracket['rate'])
            self.tax_brackets.append(new_bracket)

        self.top_marginal_bracket = TaxBracket(
            Decimal('Infinity'),
            tax_brackets_configuration['top_marginal_rate'])

        logging.info(
            f"Initialized with {len(tax_brackets_configuration['brackets'])} " \
            f"brackets, top marginal rate of "\
            f"{tax_brackets_configuration['top_marginal_rate']}"
        )
class TaxBracketCollection:
    def __init__(self, tax_brackets_configuration):
        self.tax_brackets = []
        for bracket in tax_brackets_configuration['brackets']:
            new_bracket = TaxBracket(bracket['income'], bracket['rate'])
            self.tax_brackets.append(new_bracket)

        self.top_marginal_bracket = TaxBracket(
            Decimal('Infinity'),
            tax_brackets_configuration['top_marginal_rate'])

        logging.info(
            f"Initialized with {len(tax_brackets_configuration['brackets'])} " \
            f"brackets, top marginal rate of "\
            f"{tax_brackets_configuration['top_marginal_rate']}"
        )

    def calculate_tax(self, income):
        if income < 0:
            raise Exception("Negative incomes are not supported")

        total_tax = 0
        for bracket in self.tax_brackets:
            if income == 0:
                break
            taxable_in_bracket = min(income, bracket.income)
            total_tax += bracket.calculate_tax(taxable_in_bracket)
            income -= taxable_in_bracket

        if income > 0:
            total_tax += self.top_marginal_bracket.calculate_tax(income)

        return total_tax

    def effective_tax_rate(self, income):
        # We could improve performance here by saving / memoizing the value
        # of "calculate_tax" if we called it earlier, at the expense of making
        # TaxBracketCollection stateful.
        total_tax = self.calculate_tax(income)
        return format(total_tax / income, '.2f')
 def test_calculate_tax_below_limit(self):
     bracket = TaxBracket(100000, 0.10)
     self.assertEqual(bracket.calculate_tax(3000), 300)
 def test_init(self):
     bracket = TaxBracket(100000, 0.10)
     self.assertTrue(isinstance(bracket, TaxBracket))
 def test_error_negative_rate(self):
     with self.assertRaises(Exception):
         bracket = TaxBracket(1, -0.10)
 def test_error_negative_income(self):
     with self.assertRaises(Exception):
         bracket = TaxBracket(-1, 0.10)
 def test_error_calculate_tax_beyond_limit(self):
     bracket = TaxBracket(100000, 0.10)
     with self.assertRaises(Exception):
         bracket.calculate_tax(200000)
 def test_does_not_round_to_two_decimal_places(self):
     # Intentional decision not to round to two decimal places (ie. cents)
     # until the end. More description in utils.py.
     bracket = TaxBracket(100000, 0.19)
     self.assertEqual(bracket.calculate_tax(7.10), 1.349)