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)