# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/how-must-an-instance-scope-partial-payment-allocation-order-across-principal-interest-and-fees.md
# Remnant Warrant:    https://corpusfetch.com/c/how-must-an-instance-scope-partial-payment-allocation-order-across-principal-interest-and-fees.warrant.json
# Markdown SHA-256:   1172baefe694ae96ffca9ac3bf90da929c4e9be06e66dc94d87043725c4a551a
# Code AST SHA-256:   bc669a569c8ef20cb90cef08b03a53ee561230995263e98474d4dee599fe510d
# Verified Classes:   AllocationAmbiguityError, AllocationGate
# Defensive Guards:   3 fail-closed invariants (zero external deps)

from dataclasses import dataclass


class AllocationAmbiguityError(RuntimeError):
    """Raised when partial-payment allocation order is ambiguous between parties."""


VALID_ORDERS = ("fees_first", "interest_first", "principal_first", "pro_rata")


@dataclass(frozen=True)
class AllocationGate:
    contract_order: str  # "" if unspecified
    instance_order: str
    counterparty_order: str

    def verify(self) -> bool:
        if not self.contract_order:
            raise AllocationAmbiguityError(
                "no allocation order in contract: each party defaults independently"
            )
        if self.contract_order not in VALID_ORDERS:
            raise AllocationAmbiguityError(
                f"unknown allocation order {self.contract_order!r}"
            )
        if self.instance_order != self.counterparty_order:
            raise AllocationAmbiguityError(
                f"order mismatch instance={self.instance_order} "
                f"counterparty={self.counterparty_order}"
            )
        return True


def dispatch_allocation_mode(gate: AllocationGate) -> str:
    try:
        gate.verify()
    except AllocationAmbiguityError:
        return "reconcile-components-escalate"
    return "apply-allocation"
