batchCooking/packages/shared/src/errors/error-codes.ts
Nicolas e4c8d910d4 feat(recipes): écran de revue et finalisation de l'import (étape 3/4)
Backend :
- `createRecipe` refactorisé en fine enveloppe autour d'un nouvel
  helper interne `createRecipeInternal`, paramétré par une source
  d'import optionnelle ; nouvelle fonction exportée
  `createImportedRecipe` qui réutilise toute la validation
  ingrédients/unités/diets et le matching des tech steps, sans
  dupliquer cette logique.
- La locale de l'adaptateur source est propagée jusqu'au chargement
  des `TechStepMapping`, pour que le texte anglais (TheMealDB, etc.)
  soit matché contre le bon jeu de règles au lieu du défaut français.
- Nouvel endpoint `POST /sources/:sourceKey/import/:externalId` —
  valide le payload via `createRecipeSchema` (même schéma qu'une
  création manuelle) et persiste une vraie `Recipe` liée à la source
  (`sourceId`/`externalId`).
- Nouveau code d'erreur `RECIPE_ALREADY_IMPORTED` (4022) quand
  l'item a déjà été importé pour ce foyer.

Frontend :
- `ImportRecipePage` (nouvelle page, `/recettes/importer/:sourceKey/:externalId`) —
  pré-remplit le formulaire depuis `previewSourceItem`, en miroir de
  `RecipeFormPage` (mêmes sous-composants : `IngredientRow`,
  `IngredientPicker`, `StepListEditor`, `DietTagSelect`). Ajoute une
  section dédiée aux lignes d'ingrédients non résolues automatiquement :
  l'utilisateur choisit un ingrédient réel via l'`IngredientPicker`
  existant ou retire la ligne — aucune recette invalide n'est jamais
  soumise, le bouton d'import reste désactivé tant qu'il en reste.
- `SourceItemPreviewPanel` gagne un lien « Importer cette recette »
  vers cet écran.

Tests :
- Mocha (`apps/api/test/sources.test.ts`) : 6 nouveaux tests sur
  `POST /sources/:sourceKey/import/:externalId` (payload valide,
  ingrédient/unité inconnus, déjà importé, deux foyers distincts,
  locale de la source respectée pour les tech steps). 282 tests
  passent au total, aucune régression.
- Cypress : nouveau scénario Gherkin bout-en-bout dans
  `recipe-sources.feature` (parcourir → prévisualiser → importer →
  résoudre un ingrédient non reconnu → confirmer → atterrir sur la
  recette sauvegardée). Steps d'édition d'ingrédients/étapes
  génériques déplacés de `recipe-form.ts` vers
  `cypress/support/step_definitions/common.steps.ts`, réutilisables
  par ce nouveau scénario.

Suite : étape 4 (ajouter au planning déclenche l'import si nécessaire).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 16:56:06 +02:00

85 lines
4.5 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,
/** 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,
/** 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>;
}