remove unnecessary __ne__ implementation
[utils.git] / src / budget_sync / test / test_budget_graph.py
index 932d48efd0cc5a93a138f4d6a93e3caef0bf47ef..885a20ed299418add263c3e21b83c034f0ceb2dd 100644 (file)
@@ -2,7 +2,8 @@ from budget_sync.test.mock_bug import MockBug
 from budget_sync.budget_graph import (BudgetGraphLoopError, BudgetGraph,
                                       Node, BudgetGraphMoneyWithNoMilestone,
                                       BudgetGraphBaseError,
-                                      BudgetGraphMoneyMismatch,
+                                      BudgetGraphMoneyMismatchForBudgetExcludingSubtasks,
+                                      BudgetGraphMoneyMismatchForBudgetIncludingSubtasks,
                                       BudgetGraphNegativeMoney,
                                       BudgetGraphMilestoneMismatch,
                                       BudgetGraphNegativePayeeMoney,
@@ -33,10 +34,18 @@ class TestErrorFormatting(unittest.TestCase):
                          "bug #1, root bug #5")
 
     def test_budget_graph_money_mismatch(self):
-        self.assertEqual(str(BudgetGraphMoneyMismatch(1, 5, "123.4")),
-                         "Budget assigned to task excluding subtasks "
-                         "(cf_budget field) doesn't match calculated value:"
-                         " bug #1, calculated value 123.4")
+        self.assertEqual(str(
+            BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
+                1, 5, "123.4")),
+            "Budget assigned to task excluding subtasks "
+            "(cf_budget field) doesn't match calculated value:"
+            " bug #1, calculated value 123.4")
+        self.assertEqual(str(
+            BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                1, 5, "123.4")),
+            "Budget assigned to task including subtasks "
+            "(cf_total_budget field) doesn't match calculated value:"
+            " bug #1, calculated value 123.4")
 
     def test_budget_graph_negative_money(self):
         self.assertEqual(str(BudgetGraphNegativeMoney(1, 5)),
@@ -55,10 +64,9 @@ class TestErrorFormatting(unittest.TestCase):
 
     def test_budget_graph_payees_money_mismatch(self):
         self.assertEqual(str(
-            BudgetGraphPayeesMoneyMismatch(1, 5, Money(123))),
-            "Budget assigned to task excluding subtasks (cf_budget field) "
-            "doesn't match total value assigned to payees (cf_payees_list):"
-            " bug #1, calculated total 123")
+            BudgetGraphPayeesMoneyMismatch(1, 5, Money(123), Money(456))),
+            "Total budget assigned to payees (cf_payees_list) doesn't match "
+            "expected value: bug #1, calculated total 123, expected value 456")
 
 
 EXAMPLE_BUG1 = MockBug(bug_id=1,
@@ -125,7 +133,7 @@ class TestBudgetGraph(unittest.TestCase):
         self.assertEqual(node.budget_excluding_subtasks, Money(cents=0))
         self.assertEqual(node.budget_including_subtasks, Money(cents=0))
         self.assertIsNone(node.nlnet_milestone)
-        self.assertEqual(node.payees, {})
+        self.assertEqual(node.payments, {})
 
     def test_loop1(self):
         with self.assertRaises(BudgetGraphLoopError) as cm:
@@ -157,7 +165,8 @@ class TestBudgetGraph(unittest.TestCase):
         self.assertEqual(node1.budget_including_subtasks, Money(cents=2000))
         self.assertEqual(node1.nlnet_milestone, "abc")
         self.assertEqual(list(node1.children()), [node2])
-        self.assertEqual(node1.payees, {})
+        self.assertEqual(list(node1.children_breadth_first()), [node2])
+        self.assertEqual(node1.payments, {})
         self.assertIsInstance(node2, Node)
         self.assertIs(node2.graph, bg)
         self.assertIs(node2.bug, EXAMPLE_CHILD_BUG2)
@@ -167,119 +176,431 @@ class TestBudgetGraph(unittest.TestCase):
         self.assertEqual(node2.budget_excluding_subtasks, Money(cents=1000))
         self.assertEqual(node2.budget_including_subtasks, Money(cents=1000))
         self.assertEqual(node2.nlnet_milestone, "abc")
-        self.assertEqual(node2.payees, {})
+        self.assertEqual(node2.payments, {})
 
-    def test_money_with_no_milestone(self):
+    def test_children(self):
         bg = BudgetGraph([
             MockBug(bug_id=1,
                     cf_budget_parent=None,
                     cf_budget="0",
-                    cf_total_budget="10",
+                    cf_total_budget="0",
                     cf_nlnet_milestone=None,
                     cf_payees_list=""),
-        ])
-        errors = bg.get_errors()
-        self.assertErrorTypesMatches(errors,
-                                     [BudgetGraphMoneyWithNoMilestone,
-                                      BudgetGraphMoneyMismatch])
-        self.assertEqual(errors[0].bug_id, 1)
-        self.assertEqual(errors[0].root_bug_id, 1)
-        bg = BudgetGraph([
-            MockBug(bug_id=1,
-                    cf_budget_parent=None,
-                    cf_budget="10",
+            MockBug(bug_id=2,
+                    cf_budget_parent=1,
+                    cf_budget="0",
                     cf_total_budget="0",
                     cf_nlnet_milestone=None,
                     cf_payees_list=""),
-        ])
-        errors = bg.get_errors()
-        self.assertErrorTypesMatches(errors,
-                                     [BudgetGraphMoneyWithNoMilestone,
-                                      BudgetGraphMoneyMismatch])
-        self.assertEqual(errors[0].bug_id, 1)
-        self.assertEqual(errors[0].root_bug_id, 1)
-        bg = BudgetGraph([
-            MockBug(bug_id=1,
-                    cf_budget_parent=None,
-                    cf_budget="10",
-                    cf_total_budget="10",
+            MockBug(bug_id=3,
+                    cf_budget_parent=1,
+                    cf_budget="0",
+                    cf_total_budget="0",
+                    cf_nlnet_milestone=None,
+                    cf_payees_list=""),
+            MockBug(bug_id=4,
+                    cf_budget_parent=1,
+                    cf_budget="0",
+                    cf_total_budget="0",
+                    cf_nlnet_milestone=None,
+                    cf_payees_list=""),
+            MockBug(bug_id=5,
+                    cf_budget_parent=3,
+                    cf_budget="0",
+                    cf_total_budget="0",
+                    cf_nlnet_milestone=None,
+                    cf_payees_list=""),
+            MockBug(bug_id=6,
+                    cf_budget_parent=3,
+                    cf_budget="0",
+                    cf_total_budget="0",
+                    cf_nlnet_milestone=None,
+                    cf_payees_list=""),
+            MockBug(bug_id=7,
+                    cf_budget_parent=5,
+                    cf_budget="0",
+                    cf_total_budget="0",
                     cf_nlnet_milestone=None,
                     cf_payees_list=""),
         ])
-        errors = bg.get_errors()
-        self.assertErrorTypesMatches(errors, [BudgetGraphMoneyWithNoMilestone])
-        self.assertEqual(errors[0].bug_id, 1)
-        self.assertEqual(errors[0].root_bug_id, 1)
+        self.assertEqual(len(bg.nodes), 7)
+        node1: Node = bg.nodes[1]
+        node2: Node = bg.nodes[2]
+        node3: Node = bg.nodes[3]
+        node4: Node = bg.nodes[4]
+        node5: Node = bg.nodes[5]
+        node6: Node = bg.nodes[6]
+        node7: Node = bg.nodes[7]
+        self.assertEqual(bg.roots, {node1})
+        self.assertEqual(list(node1.children()),
+                         [node2, node3, node5, node7, node6, node4])
+        self.assertEqual(list(node1.children_breadth_first()),
+                         [node2, node3, node4, node5, node6, node7])
 
-    def test_money_mismatch(self):
+    def test_money_with_no_milestone(self):
         bg = BudgetGraph([
             MockBug(bug_id=1,
                     cf_budget_parent=None,
                     cf_budget="0",
                     cf_total_budget="10",
-                    cf_nlnet_milestone="abc",
+                    cf_nlnet_milestone=None,
                     cf_payees_list=""),
         ])
         errors = bg.get_errors()
-        self.assertErrorTypesMatches(errors,
-                                     [BudgetGraphMoneyMismatch])
+        self.assertErrorTypesMatches(errors, [
+            BudgetGraphMoneyWithNoMilestone,
+            BudgetGraphMoneyMismatchForBudgetExcludingSubtasks])
         self.assertEqual(errors[0].bug_id, 1)
         self.assertEqual(errors[0].root_bug_id, 1)
-        self.assertEqual(errors[0].expected_budget_excluding_subtasks, 10)
         bg = BudgetGraph([
             MockBug(bug_id=1,
                     cf_budget_parent=None,
                     cf_budget="10",
                     cf_total_budget="0",
-                    cf_nlnet_milestone="abc",
+                    cf_nlnet_milestone=None,
                     cf_payees_list=""),
         ])
         errors = bg.get_errors()
-        self.assertErrorTypesMatches(errors,
-                                     [BudgetGraphMoneyMismatch])
+        self.assertErrorTypesMatches(errors, [
+            BudgetGraphMoneyWithNoMilestone,
+            BudgetGraphMoneyMismatchForBudgetIncludingSubtasks])
         self.assertEqual(errors[0].bug_id, 1)
         self.assertEqual(errors[0].root_bug_id, 1)
-        self.assertEqual(errors[0].expected_budget_excluding_subtasks, 0)
         bg = BudgetGraph([
             MockBug(bug_id=1,
                     cf_budget_parent=None,
                     cf_budget="10",
                     cf_total_budget="10",
-                    cf_nlnet_milestone="abc",
-                    cf_payees_list=""),
-        ])
-        errors = bg.get_errors()
-        self.assertEqual(errors, [])
-        bg = BudgetGraph([
-            MockBug(bug_id=1,
-                    cf_budget_parent=None,
-                    cf_budget="10",
-                    cf_total_budget="10",
-                    cf_nlnet_milestone="abc",
-                    cf_payees_list=""),
-            MockBug(bug_id=2,
-                    cf_budget_parent=1,
-                    cf_budget="10",
-                    cf_total_budget="10",
-                    cf_nlnet_milestone="abc",
-                    cf_payees_list=""),
-            MockBug(bug_id=3,
-                    cf_budget_parent=1,
-                    cf_budget="1",
-                    cf_total_budget="10",
-                    cf_nlnet_milestone="abc",
+                    cf_nlnet_milestone=None,
                     cf_payees_list=""),
         ])
         errors = bg.get_errors()
-        self.assertErrorTypesMatches(errors,
-                                     [BudgetGraphMoneyMismatch,
-                                      BudgetGraphMoneyMismatch])
+        self.assertErrorTypesMatches(errors, [BudgetGraphMoneyWithNoMilestone])
         self.assertEqual(errors[0].bug_id, 1)
         self.assertEqual(errors[0].root_bug_id, 1)
-        self.assertEqual(errors[0].expected_budget_excluding_subtasks, -10)
-        self.assertEqual(errors[1].bug_id, 3)
-        self.assertEqual(errors[1].root_bug_id, 1)
-        self.assertEqual(errors[1].expected_budget_excluding_subtasks, 10)
+
+    def test_money_mismatch(self):
+        def helper(budget, total_budget, payees_list, child_budget,
+                   expected_errors, expected_fixed_error_types=None):
+            if expected_fixed_error_types is None:
+                expected_fixed_error_types = []
+            bg = BudgetGraph([
+                MockBug(bug_id=1,
+                        cf_budget_parent=None,
+                        cf_budget=budget,
+                        cf_total_budget=total_budget,
+                        cf_nlnet_milestone="abc",
+                        cf_payees_list=payees_list),
+                MockBug(bug_id=2,
+                        cf_budget_parent=1,
+                        cf_budget=child_budget,
+                        cf_total_budget=child_budget,
+                        cf_nlnet_milestone="abc",
+                        cf_payees_list=""),
+            ])
+            node1: Node = bg.nodes[1]
+            errors = bg.get_errors()
+            self.assertErrorTypesMatches(errors,
+                                         [type(i) for i in expected_errors])
+            self.assertEqual([str(i) for i in errors],
+                             [str(i) for i in expected_errors])
+            bg = BudgetGraph([
+                MockBug(bug_id=1,
+                        cf_budget_parent=None,
+                        cf_budget=str(node1.fixed_budget_excluding_subtasks),
+                        cf_total_budget=str(
+                            node1.fixed_budget_including_subtasks),
+                        cf_nlnet_milestone="abc",
+                        cf_payees_list=payees_list),
+                MockBug(bug_id=2,
+                        cf_budget_parent=1,
+                        cf_budget=child_budget,
+                        cf_total_budget=child_budget,
+                        cf_nlnet_milestone="abc",
+                        cf_payees_list=""),
+            ])
+            errors = bg.get_errors()
+            self.assertErrorTypesMatches(errors,
+                                         expected_fixed_error_types)
+        helper(budget="0",
+               total_budget="0",
+               payees_list="",
+               child_budget="0",
+               expected_errors=[])
+        helper(budget="0",
+               total_budget="0",
+               payees_list="",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                       1, 1, Money(5)),
+               ])
+        helper(budget="0",
+               total_budget="0",
+               payees_list="a=1",
+               child_budget="0",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
+                       1, 1, Money(1)),
+                   BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                       1, 1, Money(1)),
+               ])
+        helper(budget="0",
+               total_budget="0",
+               payees_list="a=1",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
+                       1, 1, Money(1)),
+                   BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                       1, 1, Money(6)),
+               ])
+        helper(budget="0",
+               total_budget="0",
+               payees_list="a=10",
+               child_budget="0",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
+                       1, 1, Money(10)),
+                   BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                       1, 1, Money(10)),
+               ])
+        helper(budget="0",
+               total_budget="0",
+               payees_list="a=10",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
+                       1, 1, Money(10)),
+                   BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                       1, 1, Money(15)),
+               ])
+        helper(budget="0",
+               total_budget="100",
+               payees_list="",
+               child_budget="0",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
+                       1, 1, Money(100)),
+               ])
+        helper(budget="0",
+               total_budget="100",
+               payees_list="",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
+                       1, 1, Money(95)),
+               ])
+        helper(budget="0",
+               total_budget="100",
+               payees_list="a=1",
+               child_budget="0",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
+                       1, 1, Money(100)),
+                   BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(100)),
+               ],
+               expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
+        helper(budget="0",
+               total_budget="100",
+               payees_list="a=1",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
+                       1, 1, Money(95)),
+                   BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(95)),
+               ],
+               expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
+        helper(budget="0",
+               total_budget="100",
+               payees_list="a=10",
+               child_budget="0",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
+                       1, 1, Money(100)),
+                   BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(100)),
+               ],
+               expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
+        helper(budget="0",
+               total_budget="100",
+               payees_list="a=10",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
+                       1, 1, Money(95)),
+                   BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(95)),
+               ],
+               expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
+        helper(budget="0",
+               total_budget="5",
+               payees_list="",
+               child_budget="5",
+               expected_errors=[])
+        helper(budget="0",
+               total_budget="5",
+               payees_list="a=1",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(0)),
+               ],
+               expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
+        helper(budget="0",
+               total_budget="5",
+               payees_list="a=10",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphPayeesMoneyMismatch(1, 1, Money(10), Money(0)),
+               ],
+               expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
+        helper(budget="10",
+               total_budget="0",
+               payees_list="",
+               child_budget="0",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                       1, 1, Money(10)),
+               ])
+        helper(budget="10",
+               total_budget="0",
+               payees_list="",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                       1, 1, Money(15)),
+               ])
+        helper(budget="10",
+               total_budget="0",
+               payees_list="a=1",
+               child_budget="0",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                       1, 1, Money(10)),
+                   BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
+               ],
+               expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
+        helper(budget="10",
+               total_budget="0",
+               payees_list="a=1",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                       1, 1, Money(15)),
+                   BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
+               ],
+               expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
+        helper(budget="10",
+               total_budget="0",
+               payees_list="a=10",
+               child_budget="0",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                       1, 1, Money(10)),
+               ])
+        helper(budget="10",
+               total_budget="0",
+               payees_list="a=10",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                       1, 1, Money(15)),
+               ])
+        helper(budget="10",
+               total_budget="10",
+               payees_list="",
+               child_budget="0",
+               expected_errors=[])
+        helper(budget="10",
+               total_budget="10",
+               payees_list="a=1",
+               child_budget="0",
+               expected_errors=[
+                   BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
+               ],
+               expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
+        helper(budget="10",
+               total_budget="10",
+               payees_list="a=10",
+               child_budget="0",
+               expected_errors=[])
+        helper(budget="10",
+               total_budget="100",
+               payees_list="",
+               child_budget="0",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
+                       1, 1, Money(100)),
+               ])
+        helper(budget="10",
+               total_budget="100",
+               payees_list="",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
+                       1, 1, Money(95)),
+               ])
+        helper(budget="10",
+               total_budget="100",
+               payees_list="a=1",
+               child_budget="0",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                       1, 1, Money(10)),
+                   BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
+               ],
+               expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
+        helper(budget="10",
+               total_budget="100",
+               payees_list="a=1",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                       1, 1, Money(15)),
+                   BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
+               ],
+               expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
+        helper(budget="10",
+               total_budget="100",
+               payees_list="a=10",
+               child_budget="0",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                       1, 1, Money(10)),
+               ])
+        helper(budget="10",
+               total_budget="100",
+               payees_list="a=10",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetIncludingSubtasks(
+                       1, 1, Money(15)),
+               ])
+        helper(budget="10",
+               total_budget="15",
+               payees_list="",
+               child_budget="5",
+               expected_errors=[])
+        helper(budget="10",
+               total_budget="15",
+               payees_list="a=1",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphPayeesMoneyMismatch(1, 1, Money(1), Money(10)),
+               ],
+               expected_fixed_error_types=[BudgetGraphPayeesMoneyMismatch])
+        helper(budget="10",
+               total_budget="15",
+               payees_list="a=10",
+               child_budget="5",
+               expected_errors=[])
+
+        helper(budget="1",
+               total_budget="15",
+               payees_list="a=10",
+               child_budget="5",
+               expected_errors=[
+                   BudgetGraphMoneyMismatchForBudgetExcludingSubtasks(
+                       1, 1, Money("10"))
+               ])
 
     def test_negative_money(self):
         bg = BudgetGraph([
@@ -291,9 +612,9 @@ class TestBudgetGraph(unittest.TestCase):
                     cf_payees_list=""),
         ])
         errors = bg.get_errors()
-        self.assertErrorTypesMatches(errors,
-                                     [BudgetGraphNegativeMoney,
-                                      BudgetGraphMoneyMismatch])
+        self.assertErrorTypesMatches(errors, [
+            BudgetGraphNegativeMoney,
+            BudgetGraphMoneyMismatchForBudgetExcludingSubtasks])
         self.assertEqual(errors[0].bug_id, 1)
         self.assertEqual(errors[0].root_bug_id, 1)
         self.assertEqual(errors[1].bug_id, 1)
@@ -308,14 +629,14 @@ class TestBudgetGraph(unittest.TestCase):
                     cf_payees_list=""),
         ])
         errors = bg.get_errors()
-        self.assertErrorTypesMatches(errors,
-                                     [BudgetGraphNegativeMoney,
-                                      BudgetGraphMoneyMismatch])
+        self.assertErrorTypesMatches(errors, [
+            BudgetGraphNegativeMoney,
+            BudgetGraphMoneyMismatchForBudgetIncludingSubtasks])
         self.assertEqual(errors[0].bug_id, 1)
         self.assertEqual(errors[0].root_bug_id, 1)
         self.assertEqual(errors[1].bug_id, 1)
         self.assertEqual(errors[1].root_bug_id, 1)
-        self.assertEqual(errors[1].expected_budget_excluding_subtasks, 0)
+        self.assertEqual(errors[1].expected_budget_including_subtasks, -10)
         bg = BudgetGraph([
             MockBug(bug_id=1,
                     cf_budget_parent=None,
@@ -331,7 +652,7 @@ class TestBudgetGraph(unittest.TestCase):
         self.assertEqual(errors[0].root_bug_id, 1)
 
     def test_payees_parse(self):
-        def check(cf_payees_list, expected_payees):
+        def check(cf_payees_list, expected_payments):
             bg = BudgetGraph([MockBug(bug_id=1,
                                       cf_budget_parent=None,
                                       cf_budget="0",
@@ -341,43 +662,137 @@ class TestBudgetGraph(unittest.TestCase):
                               ])
             self.assertEqual(len(bg.nodes), 1)
             node: Node = bg.nodes[1]
-            self.assertEqual(node.payees, expected_payees)
+            self.assertEqual([str(i) for i in node.payments.values()],
+                             expected_payments)
 
         check("""
               abc = 123
               """,
-              {"abc": Money(123)})
+              ["Payment(node=#1, payee_key='abc', amount=123, "
+               + "state=NotYetSubmitted, paid=None, submitted=None)"])
         check("""
               abc = "123"
               """,
-              {"abc": Money(123)})
+              ["Payment(node=#1, payee_key='abc', amount=123, "
+               + "state=NotYetSubmitted, paid=None, submitted=None)"])
         check("""
               abc = "123.45"
               """,
-              {"abc": Money("123.45")})
+              ["Payment(node=#1, payee_key='abc', amount=123.45, "
+               + "state=NotYetSubmitted, paid=None, submitted=None)"])
         check("""
               abc = "123.45"
               "d e f" = "21.35"
               """,
-              {
-                  "abc": Money("123.45"),
-                  "d e f": Money("21.35"),
-              })
+              ["Payment(node=#1, payee_key='abc', amount=123.45, "
+               + "state=NotYetSubmitted, paid=None, submitted=None)",
+               "Payment(node=#1, payee_key='d e f', amount=21.35, "
+               + "state=NotYetSubmitted, paid=None, submitted=None)"])
         check("""
               abc = "123.45"
               # my comments
               "AAA" = "-21.35"
               """,
-              {
-                  "abc": Money("123.45"),
-                  "AAA": Money("-21.35"),
-              })
+              ["Payment(node=#1, payee_key='abc', amount=123.45, "
+               + "state=NotYetSubmitted, paid=None, submitted=None)",
+               "Payment(node=#1, payee_key='AAA', amount=-21.35, "
+               + "state=NotYetSubmitted, paid=None, submitted=None)"])
         check("""
               "not-an-email@example.com" = "-2345"
               """,
-              {
-                  "not-an-email@example.com": Money(-2345),
-              })
+              ["Payment(node=#1, payee_key='not-an-email@example.com', "
+               + "amount=-2345, state=NotYetSubmitted, paid=None, "
+               + "submitted=None)"])
+        check("""
+              payee = { amount = 123 }
+              """,
+              ["Payment(node=#1, payee_key='payee', "
+               + "amount=123, state=NotYetSubmitted, paid=None, "
+               + "submitted=None)"])
+        check("""
+              payee = { amount = 123, submitted = 2020-05-01 }
+              """,
+              ["Payment(node=#1, payee_key='payee', "
+               + "amount=123, state=Submitted, paid=None, "
+               + "submitted=2020-05-01)"])
+        check("""
+              payee = { amount = 123, submitted = 2020-05-01T00:00:00 }
+              """,
+              ["Payment(node=#1, payee_key='payee', "
+               + "amount=123, state=Submitted, paid=None, "
+               + "submitted=2020-05-01 00:00:00)"])
+        check("""
+              payee = { amount = 123, submitted = 2020-05-01T00:00:00Z }
+              """,
+              ["Payment(node=#1, payee_key='payee', "
+               + "amount=123, state=Submitted, paid=None, "
+               + "submitted=2020-05-01 00:00:00+00:00)"])
+        check("""
+              payee = { amount = 123, submitted = 2020-05-01T00:00:00-07:23 }
+              """,
+              ["Payment(node=#1, payee_key='payee', "
+               + "amount=123, state=Submitted, paid=None, "
+               + "submitted=2020-05-01 00:00:00-07:23)"])
+        check("""
+              payee = { amount = 123, paid = 2020-05-01 }
+              """,
+              ["Payment(node=#1, payee_key='payee', "
+               + "amount=123, state=Paid, paid=2020-05-01, "
+               + "submitted=None)"])
+        check("""
+              payee = { amount = 123, paid = 2020-05-01T00:00:00 }
+              """,
+              ["Payment(node=#1, payee_key='payee', "
+               + "amount=123, state=Paid, paid=2020-05-01 00:00:00, "
+               + "submitted=None)"])
+        check("""
+              payee = { amount = 123, paid = 2020-05-01T00:00:00Z }
+              """,
+              ["Payment(node=#1, payee_key='payee', "
+               + "amount=123, state=Paid, paid=2020-05-01 00:00:00+00:00, "
+               + "submitted=None)"])
+        check("""
+              payee = { amount = 123, paid = 2020-05-01T00:00:00-07:23 }
+              """,
+              ["Payment(node=#1, payee_key='payee', "
+               + "amount=123, state=Paid, paid=2020-05-01 00:00:00-07:23, "
+               + "submitted=None)"])
+        check("""
+              [payee]
+              amount = 123
+              submitted = 2020-05-23
+              paid = 2020-05-01
+              """,
+              ["Payment(node=#1, payee_key='payee', "
+               + "amount=123, state=Paid, paid=2020-05-01, "
+               + "submitted=2020-05-23)"])
+        check("""
+              [payee]
+              amount = 123
+              submitted = 2020-05-23
+              paid = 2020-05-01T00:00:00
+              """,
+              ["Payment(node=#1, payee_key='payee', "
+               + "amount=123, state=Paid, paid=2020-05-01 00:00:00, "
+               + "submitted=2020-05-23)"])
+        check("""
+              [payee]
+              amount = 123
+              submitted = 2020-05-23
+              paid = 2020-05-01T00:00:00Z
+              """,
+              ["Payment(node=#1, payee_key='payee', "
+               + "amount=123, state=Paid, paid=2020-05-01 00:00:00+00:00, "
+               + "submitted=2020-05-23)"])
+        check("""
+              [payee]
+              amount = 123
+              submitted = 2020-05-23
+              paid = 2020-05-01T00:00:00-07:23
+              """,
+              ["Payment(node=#1, payee_key='payee', "
+               + "amount=123, state=Paid, paid=2020-05-01 00:00:00-07:23, "
+               + "submitted=2020-05-23)"])
 
     def test_payees_money_mismatch(self):
         bg = BudgetGraph([
@@ -394,20 +809,6 @@ class TestBudgetGraph(unittest.TestCase):
         self.assertEqual(errors[0].bug_id, 1)
         self.assertEqual(errors[0].root_bug_id, 1)
         self.assertEqual(errors[0].payees_total, 15)
-        bg = BudgetGraph([
-            MockBug(bug_id=1,
-                    cf_budget_parent=None,
-                    cf_budget="0",
-                    cf_total_budget="0",
-                    cf_nlnet_milestone=None,
-                    cf_payees_list="payee = 5\npayee2 = 10"),
-        ])
-        errors = bg.get_errors()
-        self.assertErrorTypesMatches(errors,
-                                     [BudgetGraphPayeesMoneyMismatch])
-        self.assertEqual(errors[0].bug_id, 1)
-        self.assertEqual(errors[0].root_bug_id, 1)
-        self.assertEqual(errors[0].payees_total, 15)
 
     def test_payees_parse_error(self):
         def check_parse_error(cf_payees_list, expected_msg):
@@ -425,16 +826,15 @@ class TestBudgetGraph(unittest.TestCase):
             self.assertEqual(errors[0].msg, expected_msg)
 
         check_parse_error("""
-                          "payee 1" = {}
+                          "payee 1" = []
                           """,
-                          "value for key 'payee 1' is not a string or integer "
-                          "(to use fractional values such as 123.45, write "
-                          "\"123.45\"): {}")
+                          "value for key 'payee 1' is invalid -- it should "
+                          "either be a monetary value or a table")
 
         check_parse_error("""
                           payee = "ashjkf"
                           """,
-                          "failed to parse Money value for key 'payee': "
+                          "failed to parse monetary amount for key 'payee': "
                           "invalid Money string: characters after sign and "
                           "before first `.` must be ascii digits")
 
@@ -448,9 +848,63 @@ class TestBudgetGraph(unittest.TestCase):
         check_parse_error("""
                           payee = 123.45
                           """,
-                          "value for key 'payee' is not a string or "
-                          "integer (to use fractional values such as "
-                          "123.45, write \"123.45\"): 123.45")
+                          "failed to parse monetary amount for key 'payee': "
+                          "monetary amount is not a string or integer (to "
+                          "use fractional amounts such as 123.45, write "
+                          "\"123.45\"): 123.45")
+
+        check_parse_error("""
+                          payee = {}
+                          """,
+                          "value for key 'payee' is missing the `amount` "
+                          "field which is required")
+
+        check_parse_error("""
+                          payee = { amount = 123.45 }
+                          """,
+                          "failed to parse monetary amount for key 'payee': "
+                          "monetary amount is not a string or integer (to "
+                          "use fractional amounts such as 123.45, write "
+                          "\"123.45\"): 123.45")
+
+        check_parse_error("""
+                          payee = { amount = 123, blah = false }
+                          """,
+                          "value for key 'payee' has an unknown field: `blah`")
+
+        check_parse_error("""
+                          payee = { amount = 123, submitted = false }
+                          """,
+                          "failed to parse `submitted` field for key "
+                          "'payee': invalid date: false")
+
+        check_parse_error("""
+                          payee = { amount = 123, submitted = 123 }
+                          """,
+                          "failed to parse `submitted` field for key 'payee':"
+                          " invalid date: 123")
+
+        check_parse_error(
+            """
+            payee = { amount = 123, paid = 2020-01-01, submitted = "abc" }
+            """,
+            "failed to parse `submitted` field for key 'payee': "
+            "invalid date: 'abc'")
+
+        check_parse_error(
+            """
+            payee = { amount = 123, paid = 12:34:56 }
+            """,
+            "failed to parse `paid` field for key 'payee': just a time of "
+            "day by itself is not enough, a date must be included: 12:34:56")
+
+        check_parse_error(
+            """
+            payee = { amount = 123, submitted = 12:34:56.123456 }
+            """,
+            "failed to parse `submitted` field for key 'payee': just a time "
+            "of day by itself is not enough, a date must be included: "
+            "12:34:56.123456")
 
     def test_negative_payee_money(self):
         bg = BudgetGraph([