+class PayeeState(enum.Enum):
+ NotYetSubmitted = "not yet submitted"
+ Submitted = "submitted"
+ Paid = "paid"
+
+
+_Date = Union[date, datetime]
+
+
+def _parse_money_from_toml(value: Any) -> Money:
+ if not isinstance(value, (int, str)):
+ msg = f"monetary amount is not a string or integer " \
+ f"(to use fractional amounts such as 123.45, write " \
+ f"\"123.45\"): {value!r}"
+ raise ValueError(msg)
+ return Money(value)
+
+
+def _parse_date_time_or_none_from_toml(value: Any) -> Optional[_Date]:
+ if value is None or isinstance(value, (date, datetime)):
+ return value
+ elif isinstance(value, time):
+ msg = f"just a time of day by itself is not enough," \
+ f" a date must be included: {str(value)}"
+ raise ValueError(msg)
+ elif isinstance(value, bool):
+ msg = f"invalid date: {str(value).lower()}"
+ raise ValueError(msg)
+ elif isinstance(value, (str, int, float)):
+ msg = f"invalid date: {value!r}"
+ raise ValueError(msg)
+ else:
+ msg = f"invalid date"
+ raise ValueError(msg)
+
+
+class Payment:
+ def __init__(self,
+ node: "Node",
+ payee_key: str,
+ amount: Money,
+ paid: Optional[_Date],
+ submitted: Optional[_Date]):
+ self.node = node
+ self.payee_key = payee_key
+ self.amount = amount
+ self.paid = paid
+ self.submitted = submitted
+
+ @property
+ def state(self):
+ if self.paid is not None:
+ return PayeeState.Paid
+ if self.submitted is not None:
+ return PayeeState.Submitted
+ return PayeeState.NotYetSubmitted
+
+ @staticmethod
+ def from_toml(node: "Node", payee_key: str, toml_value: Any):
+ paid = None
+ submitted = None
+ known_keys = ("paid", "submitted", "amount")
+ if isinstance(toml_value, dict):
+ try:
+ amount = toml_value['amount']
+ except KeyError:
+ msg = f"value for key {payee_key!r} is missing the " \
+ f"`amount` field which is required"
+ raise BudgetGraphPayeesParseError(node.bug.id, msg) \
+ .with_traceback(sys.exc_info()[2])
+ for k, v in toml_value.items():
+ if k in ("paid", "submitted"):
+ try:
+ parsed_value = _parse_date_time_or_none_from_toml(v)
+ except ValueError as e:
+ msg = f"failed to parse `{k}` field for" \
+ f" key {payee_key!r}: {e}"
+ raise BudgetGraphPayeesParseError(
+ node.bug.id, msg) \
+ .with_traceback(sys.exc_info()[2])
+ if k == "paid":
+ paid = parsed_value
+ else:
+ assert k == "submitted"
+ submitted = parsed_value
+ if k not in known_keys:
+ msg = f"value for key {payee_key!r} has an unknown" \
+ f" field: `{k}`"
+ raise BudgetGraphPayeesParseError(node.bug.id, msg) \
+ .with_traceback(sys.exc_info()[2])
+ try:
+ paid = _parse_date_time_or_none_from_toml(
+ toml_value.get('paid'))
+ except ValueError as e:
+ msg = f"failed to parse `paid` field for" \
+ f" key {payee_key!r}: {e}"
+ raise BudgetGraphPayeesParseError(
+ node.bug.id, msg) \
+ .with_traceback(sys.exc_info()[2])
+ try:
+ submitted = _parse_date_time_or_none_from_toml(
+ toml_value.get('submitted'))
+ except ValueError as e:
+ msg = f"failed to parse `submitted` field for" \
+ f" key {payee_key!r}: {e}"
+ raise BudgetGraphPayeesParseError(
+ node.bug.id, msg) \
+ .with_traceback(sys.exc_info()[2])
+ elif isinstance(toml_value, (int, str, float)):
+ # float included for better error messages
+ amount = toml_value
+ else:
+ msg = f"value for key {payee_key!r} is invalid -- it should " \
+ f"either be a monetary value or a table"
+ raise BudgetGraphPayeesParseError(node.bug.id, msg)
+ try:
+ amount = _parse_money_from_toml(amount)
+ except ValueError as e:
+ msg = f"failed to parse monetary amount for key {payee_key!r}: {e}"
+ raise BudgetGraphPayeesParseError(
+ node.bug.id, msg) \
+ .with_traceback(sys.exc_info()[2])
+ return Payment(node=node, payee_key=payee_key, amount=amount,
+ paid=paid, submitted=submitted)
+
+ def __repr__(self):
+ return (f"Payment(node={_NodeSimpleReprWrapper(self.node)}, "
+ f"payee_key={self.payee_key!r}, "
+ f"amount={self.amount}, "
+ f"state={self.state.name}, "
+ f"paid={str(self.paid)}, "
+ f"submitted={str(self.submitted)})")
+
+