"""accounts — domain exceptions. Raised by the service layer; the BFF maps each to the §6.4 error shape. Keeping them here (not in main) keeps the rules in the domain (INV-6). """ from __future__ import annotations class AccountsError(Exception): """Base for accounts-domain errors.""" class InvalidEmail(AccountsError): """The supplied email is not a plausible address (§6.4 400 invalid_email).""" class ResendCooldown(AccountsError): """A code was issued for this email under 60s ago (INV-3 → §6.4 429 resend_cooldown).""" def __init__(self, retry_after_s: int) -> None: super().__init__(f"resend available in {retry_after_s}s") self.retry_after_s = retry_after_s class CodeMismatch(AccountsError): """The submitted code is wrong (PUC-2a → §6.4 400 code_mismatch).""" def __init__(self, attempts_remaining: int) -> None: super().__init__("code did not match") self.attempts_remaining = attempts_remaining class CodeExpired(AccountsError): """No live code for this email — expired, consumed, or never issued (§6.4 400 code_expired).""" class CodeExhausted(AccountsError): """The code's attempt budget is spent; it is invalidated (INV-3 → §6.4 400 code_exhausted).""" class DeliveryFailed(AccountsError): """The relay refused the code email; nothing was committed (INV-9 → §6.4 502 delivery_failed)."""