dodil/erp-brick-gl
DODIL ERP brick: general ledger
src/brick_gl/domain/readiness.py
"""Readiness (shared.yaml conventions.readiness): every gate of a transition as {gate, met, reason, fix}.
`met` is computed at read time; `reason` says why not in the customer's words; `fix` names the action (and who can
take it). A refusal carries the gate in its words as `[gate] reason (fix: ...)`, because the doors answer
{"detail": text} and a calling brick reads the gate from the text.
"""
from dataclasses import asdict, dataclass
from brick_gl.domain.errors import RuleBroken
@dataclass(frozen=True)
class Gate:
gate: str
met: bool
reason: str
fix: str
def as_dict(self) -> dict:
return asdict(self)
def met(gate: str) -> Gate:
return Gate(gate, True, "", "")
def unmet(gate: str, reason: str, fix: str) -> Gate:
return Gate(gate, False, reason, fix)
def refusal(gate: str, reason: str, fix: str, status: int = 409) -> RuleBroken:
"""One unmet gate as the rule that refuses: `[period_open] the period 2026-09 is closed (fix: ...)`."""
return RuleBroken(f"[{gate}] {reason} (fix: {fix})", status=status, gate=gate)
class GatesUnmet(RuleBroken):
"""A transition refused because gates are unmet: the words are the gates' own, the list is kept for the screen."""
def __init__(self, what: str, gates, status: int = 409):
self.gates = [g.as_dict() for g in gates if not g.met]
first = self.gates[0]["gate"] if self.gates else ""
super().__init__(f"cannot {what}: " + "; ".join(f"[{g['gate']}] {g['reason']} (fix: {g['fix']})" for g in self.gates),
status=status, gate=first)
def require(what: str, gates, status: int = 409) -> None:
if any(not g.met for g in gates):
raise GatesUnmet(what, gates, status)