batchCooking/packages/shared/src/errors/error-codes.ts
Nicolas 2beff139a3
All checks were successful
CI / lint (push) Successful in 2m57s
CI / build (push) Successful in 2m59s
CI / intent-service-test (push) Successful in 4m22s
CI / e2e (push) Successful in 9m4s
CI / test (push) Successful in 17m32s
feat(api): CRUD admin des scénarios de didacticiel (#44)
Deuxième volet backend du tutoriel guidé, sur le socle de #42/#44 : permet
à un admin de créer/éditer/supprimer les scénarios que
`GET /reference/tours` sert au moteur front.

`/admin/api/tours` (agrégateur `admin.routes.ts`, tout derrière
`requireAdmin`) :
- `GET /` — tous les scénarios (activés **et** désactivés) + étapes
  ordonnées.
- `GET /targets` — le catalogue `TourTarget` pour le sélecteur de cible de
  l'éditeur.
- `POST /` — crée un scénario vide (`key` + `name`) ; 409
  `TOUR_KEY_ALREADY_IN_USE` (nouveau code 4025) sur une clé prise.
- `PATCH /:id` — édite `name`/`enabled` **et remplace toute la liste
  d'étapes en bloc** (delete-all puis recreate avec `order` = index), même
  convention que `updateRecipe` pour les étapes d'une recette. `key`
  immuable. 404 `TOUR_NOT_FOUND` (id inconnu), 404 `TOUR_TARGET_NOT_FOUND`
  (nouveau code 4054, `targetKey` d'une étape hors catalogue — vérifié
  avant d'écrire, les étapes réelles restent intactes si la requête est
  rejetée).
- `DELETE /:id` — 204 ; les étapes et les `TourProgress` des utilisateurs
  cascadent.

Tests Mocha (`admin-tours.test.ts`) : 401 sans session admin sur chaque
route ; list (activés+désactivés) ; targets ; create→patch pour remplir
les étapes ; remplacement en bloc ; 409 clé dupliquée ; 404 cible
inconnue (sans toucher aux étapes existantes) ; 404 id inconnu ; delete +
cascade vérifiée ; un scénario désactivé disparaît de `/reference/tours`
mais reste dans la liste admin.

Refs #44
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 05:39:37 +02:00

104 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Enumeration of every business/domain error code the API can return.
*
* This is the single source of truth for error identification across the
* whole monorepo: `apps/api` throws errors carrying one of these codes,
* and `apps/web` maps each code to a localized, user-facing label (see
* `apps/web/src/services/error-message.service.ts`, backed by i18next
* locale files under `apps/web/src/locales/`). Neither side should ever
* hardcode a raw error value that the other side has to guess at — always
* reference `ErrorCode.XXX`, never a bare number/string.
*
* Numeric values (not string codes): grouped by category so the number
* itself hints at the kind of failure, similar in spirit to HTTP status
* code families —
* - `4000`–`4099`: request validation
* - `4010`–`4019`: authentication
* - `4020`–`4029`: conflicting/invalid state transition
* - `4030`–`4039`: authorization (caller authenticated, but not allowed to)
* - `4040`–`4049`: not found
* - `5000`–`5099`: internal/unexpected
*
* When adding a new failure case in the API:
* 1. Add a new member here, in the right range, with the next free number.
* 2. Throw it via `HttpError` (`@batch-cooking/express-tools`).
* 3. Add its translation key to every locale file under
* `apps/web/src/locales` (one `translation.json` per language).
*/
export enum ErrorCode {
/** Request body/query failed zod schema validation. */
VALIDATION_ERROR = 4000,
/** Signup attempted with an email that already has a profile. */
EMAIL_ALREADY_IN_USE = 4001,
/** Login failed — wrong email or wrong password (never say which). */
INVALID_CREDENTIALS = 4010,
/** Request required a session cookie/JWT that is missing, invalid, or stale. */
NOT_AUTHENTICATED = 4011,
/** `POST /house` or `POST /house/join` attempted while the profile already belongs to a household. */
ALREADY_HAS_HOUSE = 4020,
/** `DELETE /recipes/:id` attempted on a recipe still referenced by at least one `PlanningItem`. */
RECIPE_IN_USE = 4021,
/** `POST /sources/:sourceKey/import/:externalId` attempted on an item already imported (a `Recipe` already exists for that `sourceId`/`externalId` pair). */
RECIPE_ALREADY_IMPORTED = 4022,
/** `POST /admin/api/tech-steps/retrain` attempted while a previous retrain (F1 gate + backfill) is still running — it holds a process-wide lock so two can't overlap. */
RETRAIN_ALREADY_RUNNING = 4023,
/** `POST /planning/items/leftover` asked to assign more portions than the source item's leftover pool still has available (`computeAvailableLeftover`, revalidated server-side). */
LEFTOVER_QUANTITY_EXCEEDED = 4024,
/** A household action reserved to its admin (delete the household, remove a member) attempted by a non-admin member. */
NOT_HOUSE_ADMIN = 4030,
/** `PATCH /recipes/:id` or `DELETE /recipes/:id` attempted by someone other than the recipe's author — visibility controls reading, not writing. */
NOT_RECIPE_AUTHOR = 4031,
/** No route/resource matches the request. */
NOT_FOUND = 4040,
/** The profile making the request has no household yet (`houseId` is `null`). */
HOUSE_NOT_FOUND = 4041,
/** A `dietId` was given that doesn't match any reference `Diet` row. */
DIET_NOT_FOUND = 4042,
/** One or more `allergyIds` don't match any reference `Allergy` row. */
ALLERGY_NOT_FOUND = 4043,
/** `POST /house/join`'s `inviteCode` doesn't match any household. */
INVITE_CODE_NOT_FOUND = 4044,
/** `GET /recipes/:id`, `PATCH /recipes/:id` or `DELETE /recipes/:id` given an id that doesn't match any recipe. */
RECIPE_NOT_FOUND = 4045,
/** A recipe payload's `ingredientId` doesn't match any reference `Ingredient` row. */
INGREDIENT_NOT_FOUND = 4046,
/** `DELETE /planning/items/:id` given an id that doesn't match any planning item visible to the caller's household. */
PLANNING_ITEM_NOT_FOUND = 4047,
/** A recipe payload's `unitId` doesn't match any reference `Unit` row. */
UNIT_NOT_FOUND = 4048,
/** `PATCH /house/current/sources`'s `sourceIds` contains one that doesn't match any reference `Source` row. */
SOURCE_NOT_FOUND = 4049,
/** `POST /recipes/:id/steps/:stepId/corrections` given a `stepId` that doesn't belong to a recipe visible to the caller. */
STEP_NOT_FOUND = 4050,
/** A tech-step correction's `previousTechStepId`/`correctedTechStepId` doesn't match any reference `TechStep` row. */
TECH_STEP_NOT_FOUND = 4051,
/** A tech-step correction's manually-attached `utensils[].utensilId` doesn't match any reference `Utensil` row. */
UTENSIL_NOT_FOUND = 4052,
/** `POST /profile/tours/:tourKey/complete` (or an admin tour route) named a tour scenario key/id that doesn't match a `TourScenario` (and, for the public route, an *enabled* one). */
TOUR_NOT_FOUND = 4053,
/** An admin tour scenario step referenced a `targetKey` that doesn't match any seeded `TourTarget`. */
TOUR_TARGET_NOT_FOUND = 4054,
/** `POST /admin/api/tours` was given a `key` already used by another scenario. */
TOUR_KEY_ALREADY_IN_USE = 4025,
/** A tech-step correction's `start`/`end` span falls outside the target step's `description`, or `start >= end`. */
INVALID_CORRECTION_SPAN = 4002,
/** Unexpected/unhandled failure — the catch-all, always logged server-side. */
INTERNAL_ERROR = 5000,
}
/**
* Shape of every JSON error body the API returns, whatever the failure.
* Kept intentionally small and stable: `code` is what clients should
* branch on, `message` is a human-readable (English, developer-facing)
* description useful for logs/debugging — never shown to end users as-is,
* since end-user-facing text is localized client-side from `code`.
*/
export interface ApiErrorResponse {
/** Machine-readable error identifier — see {@link ErrorCode}. */
code: ErrorCode;
/** Developer-facing description (English). Not localized, not for UI display. */
message: string;
/** Present only for VALIDATION_ERROR: per-field error messages from zod. */
details?: Record<string, string[] | undefined>;
}