# Module: Azaion.Common.BusinessException ## Purpose Custom exception type for domain-level errors, paired with an `ExceptionEnum` catalog of all business error codes. ## Public Interface ### BusinessException | Member | Signature | Description | |--------|-----------|-------------| | Constructor | `BusinessException(ExceptionEnum exEnum)` | Creates exception with message from `ExceptionEnum`'s `[Description]` attribute | | `ExceptionEnum` | `ExceptionEnum ExceptionEnum { get; set; }` | The specific error code | | `GetMessage` | `static string GetMessage(ExceptionEnum exEnum)` | Looks up human-readable message for an error code | ### ExceptionEnum | Value | Code | Description | HTTP Status | |-------|------|-------------|-------------| | `NoEmailFound` | 10 | No such email found | 409 | | `EmailExists` | 20 | Email already exists | 409 | | `WrongPassword` | 30 | Passwords do not match | 409 | | `PasswordLengthIncorrect` | 32 | Password should be at least 12 characters | 409 | | `EmailLengthIncorrect` | 35 | Email is empty or invalid | 409 | | `WrongEmail` | 37 | (no description attribute) | 409 | | `UserDisabled` | 38 | User account is disabled | 409 | | `AccountLocked` | 50 | AZ-537 — account temporarily locked due to too many failed login attempts (carries `RetryAfterSeconds`) | **423 Locked** | | `LoginRateLimited` | 51 | AZ-537 — too many login attempts per account; try again later (carries `RetryAfterSeconds`) | **429 Too Many Requests** | | `InvalidRefreshToken` | 52 | AZ-531 — refresh token invalid / expired / revoked / reuse-detected | **401 Unauthorized** | | `SessionNotFound` | 53 | AZ-535 — admin tried to revoke a non-existent session | **404 Not Found** | | `InvalidMissionRequest` | 54 | AZ-533 — mission_id pattern fail or planned_duration_h out of bounds | **400 Bad Request** | | `AircraftNotFound` | 55 | AZ-533 — aircraft id missing or not a `CompanionPC` user | **400 Bad Request** | | `MfaAlreadyEnabled` | 56 | AZ-534 — `/users/me/mfa/enroll` called for a user that already has MFA on | **409 Conflict** | | `MfaNotEnrolling` | 57 | AZ-534 — confirm called without a prior enroll | **409 Conflict** | | `MfaNotEnabled` | 58 | AZ-534 — disable / verify-for-login called for a user without MFA | **409 Conflict** | | `InvalidMfaCode` | 59 | AZ-534 — TOTP code (and recovery code) failed to verify | **401 Unauthorized** | | `NoFileProvided` | 60 | No file provided | 409 | | `InvalidMfaToken` | 61 | AZ-534 — step-1 MFA token failed to validate (signature / audience / expiry) | **401 Unauthorized** | ### RetryAfterSeconds | Member | Type | Description | |--------|------|-------------| | Constructor | `BusinessException(ExceptionEnum exEnum, int retryAfterSeconds)` | Cycle 2 (AZ-537) — sets `RetryAfterSeconds`, surfaced by `BusinessExceptionHandler` as a `Retry-After` response header. Used by `AccountLocked` (returns remaining lockout seconds) and `LoginRateLimited` (returns the window seconds). | | `RetryAfterSeconds` | `int?` | Optional cooldown hint; null when the exception was constructed without a window. | > **Cycle 1 (2026-05-13) note** — `HardwareIdMismatch = 40` and `BadHardware = 45` were removed by AZ-197. Codes 40 and 45 should NOT be reused. > > **Cycle 2 (2026-05-14) note** — `WrongResourceName = 50` was removed early in the cycle along with the `GetResourceRequest` validator. The integer 50 has since been **reused for `AccountLocked`** as part of AZ-537 (since the previous user-facing string "Wrong resource name" is no longer surfaced anywhere). This is the one deliberate exception to the "gap kept" lesson — the old code had no remaining client surface and the auth modernization wanted a tightly-clustered range of new codes. ## Internal Logic Static constructor eagerly loads all `ExceptionEnum` descriptions into a dictionary via `EnumExtensions.GetDescriptions()`. Messages are retrieved by dictionary lookup with fallback to `ToString()`. The two-arg constructor sets `RetryAfterSeconds` for the lockout / rate-limit paths. ## Dependencies - `EnumExtensions` — for `GetDescriptions()` ## Consumers - `BusinessExceptionHandler` — catches and maps via `MapStatusCode`. The default mapping is 409; cycle 2 codes use a per-enum status map (`AccountLocked` → 423, `LoginRateLimited` → 429, refresh/MFA validation failures → 401, `SessionNotFound` → 404, mission validation failures → 400, MFA conflict states → 409). When `RetryAfterSeconds > 0` the handler also stamps a `Retry-After` response header. - `UserService` — throws for the auth path (`NoEmailFound`, `WrongPassword`, `EmailExists`, `UserDisabled`, `AccountLocked`, `LoginRateLimited`) - `RefreshTokenService` — throws `InvalidRefreshToken` on bad/expired/reuse-detected - `SessionService` — throws `SessionNotFound` for admin-revoke of missing sids - `MissionTokenService` — throws `InvalidMissionRequest`, `AircraftNotFound` - `MfaService` — throws `MfaAlreadyEnabled`, `MfaNotEnrolling`, `MfaNotEnabled`, `InvalidMfaCode`, `InvalidMfaToken`, `NoEmailFound`, `WrongPassword` - `ResourcesService` — throws `NoFileProvided` for missing file uploads - `Program.cs` `ParseSidClaim` / `ParseUserIdClaim` helpers — throw `InvalidRefreshToken` (401) on missing or malformed claims - FluentValidation validators — reference `ExceptionEnum` codes in `.WithErrorCode()` ## Data Models None. ## Configuration None. ## External Integrations None. ## Security Error codes are returned to the client via `BusinessExceptionHandler` along with the per-enum HTTP status. The `Retry-After` header on lockout / rate-limit responses lets well-behaved clients back off without blind retries. ## Tests None.