pub enum AppError {
Show 47 variants
Validation(String),
Unauthorized,
InvalidCredentials,
TokenError(String),
Forbidden(String),
AccountDeactivated,
NotFound(String),
Conflict(String),
AcpNotInScope {
acp_id: Uuid,
},
ModuleDisabled {
module: String,
},
UnknownModule {
module: String,
},
ModuleAlwaysOn {
module: String,
},
RateLimited,
Database(String),
Crypto(String),
Internal(String),
MagicLinkInvalid,
MagicLinkExpired,
MagicLinkAlreadyConsumed,
MandateExpired,
MandateRevoked,
MandateInvalidScope,
MandateNotFound,
RoleAlreadyAssigned {
user_id: Uuid,
role: String,
},
DelegationChainNotAllowed,
TicketImmutable,
ResponseImmutable,
TechnicalSpecAlreadyApproved,
TechnicalSpecResignatureRequired,
SignatoryNotAuthorized,
SignatureAlreadyExists,
TechnicalSpecRequired,
EvaluatorIsContractor,
BuildingNotConformant {
building_id: Uuid,
units_delta: i32,
quota_delta: Decimal,
quota_basis: i32,
},
MeetingNotCompletable {
meeting_id: Uuid,
missing: Vec<MissingInvariant>,
},
AcpNotConformant {
acp_id: Uuid,
units_delta: i32,
quota_delta: Decimal,
quota_basis: i32,
},
ReserveFundInsufficient {
acp_id: Uuid,
required: Decimal,
actual: Decimal,
ordinary_charges_n1: Decimal,
},
VotingRightSuspended {
unit_id: Uuid,
},
MeetingModeRequiresVideoconf {
mode: String,
},
NotaryLinkInvalid,
NotaryLinkExpired,
NotaryLinkRevoked,
VoteAuthMethodRequired,
VoteAuthInsufficient {
mode: String,
auth_method: String,
},
ResolutionAutoNotRemovable,
CdcElectionQuorumNotReached {
meeting_id: Uuid,
},
ReservationMotifRequired,
}Expand description
Application-level error.
Each variant maps to a specific HTTP status code via ResponseError.
See module-level docs for usage guidelines.
Variants§
Validation(String)
Input validation failed (bad request payload, missing fields, format errors).
Authentication required but not provided / token missing.
InvalidCredentials
Provided credentials are invalid. Used uniformly for “email not found” AND “wrong password” to prevent username enumeration attacks.
TokenError(String)
Token expired, malformed, or revoked.
Forbidden(String)
User is authenticated but lacks the required role/permission.
AccountDeactivated
User account exists but is deactivated.
NOTE: returning a distinct error from InvalidCredentials may leak
account existence — security review needed for auth/login flow.
NotFound(String)
Resource not found (e.g., user by id, building by id).
Conflict(String)
Conflict (e.g., email already in use, ownership total > 100%).
AcpNotInScope
ACP accessed by a user out of scope (different cabinet, no role assignment). 403 typé — Story 1.1 / ADR-0010 architecture §6.3.
ModuleDisabled
Le module demandé est éteint pour cette ACP. 403 typé, produit par
ModuleGuard, jamais par l’interface seule — Story 5.1 @security
(ADR-0015). Le nom du module voyage dans l’erreur pour que le
frontend puisse dire lequel sans le deviner depuis l’URL.
UnknownModule
Nom de module inconnu (foobar). 422 — Story 5.1 @negative.
Distinct de ModuleDisabled : « ce module n’existe pas » n’est pas
« ce module est éteint », et les confondre apprendrait au client à
réessayer un nom qui ne marchera jamais.
ModuleAlwaysOn
Tentative d’éteindre un module toujours actif (identity). 403 —
Story 5.1 @negative. Ce n’est pas un défaut de droits de l’appelant,
c’est une propriété de la capacité : aucun rôle ne peut le faire.
RateLimited
Rate limit exceeded.
Database(String)
Database error (sqlx, connection, query). Internal — not surfaced verbatim to clients.
Crypto(String)
Cryptographic error (bcrypt, JWT signing).
Internal(String)
Catch-all for legacy Result<_, String> propagation.
Should be reduced over time as repositories migrate.
MagicLinkInvalid
MagicLink token does not match any record (forged / unknown / malformed). Returns 403 Forbidden. Story 3.2 (FR6).
MagicLinkExpired
MagicLink TTL elapsed. FR message guides the user to request a new link. Returns 403 Forbidden. Story 3.2 (FR6).
MagicLinkAlreadyConsumed
MagicLink already used (single-use enforcement / replay protection). Returns 403 Forbidden. Story 3.2 (FR6).
MandateExpired
Mandate is past its valid_until boundary. Returns 403 Forbidden.
Story 3.4 (FR7 INV-14).
MandateRevoked
Mandate has been revoked before its natural expiry. Returns 403 Forbidden. Story 3.4 (FR7 INV-14).
MandateInvalidScope
Mandate exists but does not authorise the requested scope (e.g. notary mandated on Building X tries to act on Building Y). Returns 403 Forbidden. Story 3.4 (FR7 INV-14).
MandateNotFound
No mandate matches the (subject, kind, scope) tuple. Returns 404. Story 3.4 (FR7 INV-14).
RoleAlreadyAssigned
The target user already holds the requested role actively. Returns 409. Story 3.5 (FR8 INV-8) — anti-double-grant.
DelegationChainNotAllowed
The delegator tries to re-delegate a role that was itself delegated to them. Returns 403 — Story 3.5 (FR8 INV-8) anti-bypass.
TicketImmutable
Ticket is locked from further edits — INV-24 enforces a 5-minute editability window after creation. Subsequent edits MUST go through dedicated workflow endpoints (assign / resolve / cancel …). Returns 403 Forbidden. Story 3.6 (FR31).
ResponseImmutable
SyndicResponse is append-only (INV-23). Any attempt to mutate an existing response (edit / delete) MUST surface here, never as a generic Conflict / Internal. Returns 403 Forbidden. Story 3.7 (FR32).
TechnicalSpecAlreadyApproved
TechnicalSpec already approved — cannot be edited in place. Returns
409 Conflict. The caller must bump_version instead. Story 3.8 (FR33).
TechnicalSpecResignatureRequired
A new major version of a TechnicalSpec was issued and requires fresh signatures from every required signatory. Returns 422 Unprocessable Entity. Story 3.8 (FR33).
SignatoryNotAuthorized
The user’s role does not match any required_signatures slot on the
TechnicalSpec, or no active Mandate authorises them. Returns 403.
Story 3.8 (FR33).
SignatureAlreadyExists
The (signatory, role) pair has already signed this TechnicalSpec. Returns 409 Conflict. Story 3.8 (FR33).
TechnicalSpecRequired
A ContractorEvaluation requires the referenced TechnicalSpec to be in
status Approved (Story 3.9 — FR34). A spec in Draft /
PendingSignatures / Superseded does not legitimise an evaluation:
the prestation either has not been signed off yet, or the spec it
signed off has been replaced. Returns 422 Unprocessable Entity.
EvaluatorIsContractor
A user attempts to evaluate themselves as a contractor (i.e.
evaluator_user_id == contractor_user_id). Returns 422 Unprocessable
Entity. Story 3.9 (FR34 INV-21).
BuildingNotConformant
Track H Story H1 — Building::assert_conformant() a échoué.
L’immeuble n’est pas conforme à son acte de base : le pre-check
validate-before-compute bloque toute mutation/calcul (charges,
appels de fonds, états datés…). Code 422 + payload
BUILDING_NOT_CONFORMANT exploitable côté frontend (toast +
banner narratif). Mémoire validate-before-compute.
N’expose pas d’info sensible (pas d’user_id, pas d’org_id) :
uniquement building_id + deltas + quota_basis — payload requis
par l’admin pour corriger.
MeetingNotCompletable
Track H Story H3 — Meeting::assert_can_complete() invariants Art. 3.87 §3-5 CC.
Erreur 422 typée avec liste des invariants manquants pour bloquer la
transition Scheduled → Completed. Permet au FE d’afficher
<MissingInvariantsList> avec narratif par invariant.
AcpNotConformant
Track H Story H5 (CL1) — Acp::assert_conformant() (Art. 3.84 CC, ADR-0010).
La copropriété (ACP) n’est pas conforme à son acte de base (Σ quotités
de tous les blocs ≠ acps.total_tantiemes). 422 + payload
ACP_NOT_CONFORMANT (acp_id + deltas + quota_basis), même format que
BuildingNotConformant. N’expose pas d’info sensible.
ReserveFundInsufficient
Track H Story H13 (CL4) — Acp::assert_reserve_fund_compliant()
(Art. 3.86 §3 CC, loi 2019). Le fonds de réserve est sous le seuil légal
des 5 % des charges ordinaires N-1 et non renoncé (vote 4/5). 422 +
payload RESERVE_FUND_INSUFFICIENT (acp_id + required/actual + base).
VotingRightSuspended
Track H Story H17 (CL3) — assert_voting_right_active() (Art. 3.87 §1
CC). Le lot est démembré (usufruit/nue-propriété, emphytéose, superficie)
ou en indivision sans représentant unique désigné : son droit de vote est
suspendu. 422 + payload VOTING_RIGHT_SUSPENDED (unit_id).
MeetingModeRequiresVideoconf
Story 4.1 — Meeting::set_mode() a échoué : mode distanciel/hybride
annoncé sans URL de visioconférence configurée. 422 + payload
MEETING_MODE_REQUIRES_VIDEOCONF (FE guide la saisie du champ
manquant plutôt que de laisser échouer la convocation plus tard).
NotaryLinkInvalid
#845 / ADR 0051 — lien notaire inconnu, forgé, ou scopé sur un autre
état daté. Uniforme avec “jeton inconnu” (anti-énumération, même
rationale que MagicLinkInvalid) : un jeton qui ouvre l’état daté A
ne doit pas distinguablement échouer sur l’état daté B. Returns 403.
NotaryLinkExpired
#845 / ADR 0051 — le lien notaire a dépassé ses sept jours de validité. Le syndic peut le renouveler. Returns 403.
NotaryLinkRevoked
#845 / ADR 0051 — le syndic a révoqué le lien avant terme. Un lien révoqué ne se renouvelle pas : il faut en émettre un nouveau. Returns 403.
VoteAuthMethodRequired
Story 4.2 (#48) — auth_method absent du bulletin de vote. Un vote ne
peut pas être enregistré sans savoir comment le votant a été
authentifié : ni preuve ni contestation ne sont alors possibles.
Retourne 422 + payload VOTE_AUTH_METHOD_REQUIRED.
VoteAuthInsufficient
Story 4.2 (#48) — Art. 3.87 §1er, §4 CC : le mode de l’AG (remote ou
hybrid) exige une méthode qui engage réellement le votant (itsme/eID),
ou une procuration en bonne et due forme. presence ne fait
qu’affirmer une présence que la modalité distancielle ne permet
justement pas de vérifier — c’est exactement la fraude que
l’authentification forte doit rendre impossible. Retourne 403 +
payload VOTE_AUTH_INSUFFICIENT.
ResolutionAutoNotRemovable
Story 4.6 (#581) — Resolution::is_auto_generated() est vraie : la
résolution d’évaluation des prestataires générée d’office à toute AGO
(Art. 3.89 § 5, 12° Code Civil belge) ne peut être ni supprimée ni
modifiée par le syndic qu’elle évalue. Returns 403 Forbidden.
CdcElectionQuorumNotReached
Story 4.7 — élection du conseil de copropriété tentée sur une AG dont
le statut n’est pas Completed : la clôture d’une AG suppose déjà le
quorum double atteint (Meeting::assert_can_complete, Art. 3.87 §5
CC) — une AG non clôturée n’a donc jamais prouvé son quorum. 422 +
payload CDC_ELECTION_QUORUM_NOT_REACHED.
ReservationMotifRequired
Story 5.4 (#588, INV-5/FR27) — on_behalf_of_acp = true sans motif.
L’exception à l’interdiction de participation personnelle du syndic
ne se justifie pas d’elle-même : sans motif, elle ne serait pas
traçable. 422 Unprocessable Entity — la requête est syntaxiquement
valide, la règle métier la refuse.
Implementations§
Trait Implementations§
Source§impl Error for AppError
impl Error for AppError
1.30.0 · Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()
Source§impl From<AcpNotConformantError> for AppError
impl From<AcpNotConformantError> for AppError
Source§fn from(err: AcpNotConformantError) -> Self
fn from(err: AcpNotConformantError) -> Self
Track H Story H5 — la copropriété (ACP) n’est pas conforme à son acte
de base → 422 + payload ACP_NOT_CONFORMANT. Utilisé par les gates
validate-before-compute ACP-level (Story H7).
Source§impl From<AlerteRefusee> for AppError
impl From<AlerteRefusee> for AppError
Source§fn from(e: AlerteRefusee) -> Self
fn from(e: AlerteRefusee) -> Self
Une alerte CdC malformée (texte vide) est une erreur d’entrée client → 400 validation, jamais 500 Internal (Story 4.7 / #582).
Source§impl From<AppError> for ScopeGuardError
impl From<AppError> for ScopeGuardError
Source§impl From<BuildingNotConformantError> for AppError
impl From<BuildingNotConformantError> for AppError
Source§fn from(err: BuildingNotConformantError) -> Self
fn from(err: BuildingNotConformantError) -> Self
Track H Story H1 — pre-check validate-before-compute. L’immeuble
n’est pas conforme à son acte de base → 422 + payload narratif
BUILDING_NOT_CONFORMANT (FE rend banner + toast).
Source§impl From<CallForFundsError> for AppError
impl From<CallForFundsError> for AppError
Source§fn from(e: CallForFundsError) -> Self
fn from(e: CallForFundsError) -> Self
Un appel de fonds malformé (montant ≤ 0, titre/description vide, échéance ≤ appel) est une erreur d’entrée client → 400 validation, jamais 500 Internal (#433 / WP-A6 EXP-008).
Source§impl From<ChargeDistributionError> for AppError
impl From<ChargeDistributionError> for AppError
Source§fn from(e: ChargeDistributionError) -> Self
fn from(e: ChargeDistributionError) -> Self
Une répartition de charges malformée est une erreur d’entrée client
(quote-part hors [0, 1], total négatif, somme des quotités > 100%) →
400 validation, jamais 500 Internal (le From<String> générique
mappait à tort vers Internal) — #433 / WP-A4 EXP-005.
Source§impl From<Error> for AppError
impl From<Error> for AppError
Source§fn from(e: Error) -> Self
fn from(e: Error) -> Self
Convertit une erreur sqlx, en la traçant.
Le silence d’avant n’était pas anodin. Pendant l’incident du
2026-08-24 (09:16Z–09:48Z), les 502 sur /units et /acps n’ont
laissé que sept lignes de log au total, aucune liée aux endpoints en
échec : ni panic, ni erreur sqlx. L’hypothèse « pool épuisé » n’était
pas réfutée, elle était structurellement inobservable — ce qui est
pire, parce qu’on ne peut pas non plus l’écarter.
PoolTimedOut est distingué des autres variantes et journalisé en
error! avec un marqueur explicite : c’est la seule signature qui
permette, après coup, de trancher entre un pool saturé et une requête
lente. Les autres erreurs de base restent en error! aussi, mais sans
ce marqueur.
RowNotFound ne loggue rien : ce n’est pas une panne, c’est une
réponse. La journaliser noierait les vraies erreurs sous le bruit des
404 légitimes.
Voir #719 et #718.
Source§impl From<EtatDateError> for AppError
impl From<EtatDateError> for AppError
Source§fn from(e: EtatDateError) -> Self
fn from(e: EtatDateError) -> Self
Un état daté malformé (quote-part hors bornes, montant négatif
interdit, transition workflow invalide, champ obligatoire vide) est
une erreur d’entrée client → 400 validation, jamais 500 Internal
(le From<String> générique mappait à tort vers Internal) —
#433 / WP-A5 EXP-007.
Source§impl From<JournalEntryError> for AppError
impl From<JournalEntryError> for AppError
Source§fn from(e: JournalEntryError) -> Self
fn from(e: JournalEntryError) -> Self
Une écriture comptable malformée est une erreur d’entrée client
(débit≠crédit, ligne invalide, type journal inconnu…) → 400
validation, jamais 500 Internal (le From<String> générique
mappait à tort vers Internal).
Source§impl From<LienNotaireError> for AppError
impl From<LienNotaireError> for AppError
Source§fn from(err: LienNotaireError) -> Self
fn from(err: LienNotaireError) -> Self
Un lien notaire malformé à l’émission (etat_date_id/emis_par nil)
est une erreur d’entrée serveur — ces UUID viennent du chemin de la
requête et de AuthenticatedUser, jamais du client → 400 validation.
DejaRevoque est un refus métier (renouveler un lien mort) → 409
Conflict, distinct des 403 NotaryLink* qui sanctionnent la LECTURE.
Source§impl From<MeetingModeError> for AppError
impl From<MeetingModeError> for AppError
Source§fn from(err: MeetingModeError) -> Self
fn from(err: MeetingModeError) -> Self
Story 4.1 — Meeting::set_mode() refusé (mode remote/hybrid sans
URL de visioconférence) → 422 + payload MEETING_MODE_REQUIRES_VIDEOCONF.
Source§impl From<MeetingNotCompletableError> for AppError
impl From<MeetingNotCompletableError> for AppError
Source§fn from(err: MeetingNotCompletableError) -> Self
fn from(err: MeetingNotCompletableError) -> Self
Track H Story H3 — convertit l’erreur domain typée vers AppError 422
avec liste structurée des invariants manquants. Le FE consomme
details.missing[] pour rendre <MissingInvariantsList> + toast i18n.
Source§impl From<OwnerContributionError> for AppError
impl From<OwnerContributionError> for AppError
Source§fn from(e: OwnerContributionError) -> Self
fn from(e: OwnerContributionError) -> Self
Une contribution malformée (montant négatif, description vide) est une erreur d’entrée client → 400 validation, jamais 500 Internal (#433 / WP-A6 EXP-008).
Source§impl From<PortfolioError> for AppError
impl From<PortfolioError> for AppError
Source§fn from(e: PortfolioError) -> Self
fn from(e: PortfolioError) -> Self
Un portefeuille malformé (nom vide, trop court, trop long, description trop longue) est une erreur d’entrée client → 400 validation, jamais 500 Internal (Story 2.1 — ADR-0011).
Source§impl From<ReservationOnBehalfError> for AppError
impl From<ReservationOnBehalfError> for AppError
Source§fn from(err: ReservationOnBehalfError) -> Self
fn from(err: ReservationOnBehalfError) -> Self
on_behalf_of_acp = true sans motif → 422 ReservationMotifRequired.
Source§impl From<ReserveFundInsufficientError> for AppError
impl From<ReserveFundInsufficientError> for AppError
Source§fn from(err: ReserveFundInsufficientError) -> Self
fn from(err: ReserveFundInsufficientError) -> Self
Track H Story H13 — fonds de réserve sous le seuil légal des 5 %
(Art. 3.86 §3, loi 2019) → 422 + payload RESERVE_FUND_INSUFFICIENT.
Source§impl From<String> for AppError
Transition convenience: convert legacy String errors from repositories
into AppError::Internal. Should be used sparingly via .map_err(AppError::from)
at the boundary; prefer dedicated variants when the error semantic is known.
impl From<String> for AppError
Transition convenience: convert legacy String errors from repositories
into AppError::Internal. Should be used sparingly via .map_err(AppError::from)
at the boundary; prefer dedicated variants when the error semantic is known.
Source§impl From<TechnicalInspectionError> for AppError
impl From<TechnicalInspectionError> for AppError
Source§fn from(e: TechnicalInspectionError) -> Self
fn from(e: TechnicalInspectionError) -> Self
Un coût d’inspection négatif est une erreur d’entrée client → 400 validation, jamais 500 Internal (ADR-0008, suite #661).
Source§impl From<VoteAuthError> for AppError
impl From<VoteAuthError> for AppError
Source§fn from(err: VoteAuthError) -> Self
fn from(err: VoteAuthError) -> Self
Story 4.2 — assert_vote_auth_sufficient refusé : Missing → 422
(VOTE_AUTH_METHOD_REQUIRED), Insufficient → 403
(VOTE_AUTH_INSUFFICIENT).
Source§impl From<VotingRightError> for AppError
impl From<VotingRightError> for AppError
Source§fn from(err: VotingRightError) -> Self
fn from(err: VotingRightError) -> Self
#848 (Art. 3.87 §1 CC) — refus de désignation. Le contrôle dormant
assert_single_voting_representative (jusqu’ici démontré par
tests/bdd_voting_right.rs mais appelé par aucun code de production)
est désormais câblé dans UnitOwnerUseCases::designate_voting_representative.
MultipleRepresentatives → 409 : un second représentant pour le même
lot est un CONFLIT avec l’état existant, pas une entrée invalide.
UnknownOwnershipType ne devrait pas survenir sur ce chemin (la valeur
vient d’une colonne déjà contrainte par le CHECK SQL) — narré en
interne plutôt que masqué.
Source§impl From<VotingRightSuspendedError> for AppError
impl From<VotingRightSuspendedError> for AppError
Source§fn from(err: VotingRightSuspendedError) -> Self
fn from(err: VotingRightSuspendedError) -> Self
Track H Story H17 — lot démembré/indivis sans représentant unique
(Art. 3.87 §1) → 422 + payload VOTING_RIGHT_SUSPENDED.
Source§impl From<WorkReportError> for AppError
impl From<WorkReportError> for AppError
Source§fn from(e: WorkReportError) -> Self
fn from(e: WorkReportError) -> Self
Un coût de travaux négatif est une erreur d’entrée client → 400
validation, jamais 500 Internal. Reprend l’invariant que portait
#[validate(range(min = 0.0))] sur le DTO avant la conversion Decimal
(ADR-0008, suite #661).
Source§impl ResponseError for AppError
impl ResponseError for AppError
Source§fn status_code(&self) -> StatusCode
fn status_code(&self) -> StatusCode
Source§fn error_response(&self) -> HttpResponse
fn error_response(&self) -> HttpResponse
Auto Trait Implementations§
impl Freeze for AppError
impl RefUnwindSafe for AppError
impl Send for AppError
impl Sync for AppError
impl Unpin for AppError
impl UnsafeUnpin for AppError
impl UnwindSafe for AppError
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
§impl<T> Fake for T
impl<T> Fake for T
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more