PR 2/4 du chantier « gestion des restes » (issue #34). Deux nouvelles routes sur `/planning` : - `POST /planning/items/leftover` → `assignLeftoverPlanningItem`, 201. Crée un item « restes » (`sourcePlanningItemId` non nul) sur un créneau, même recette que la source (`recipeId` copié, jamais envoyé par le client). `portions` revérifié serveur contre le pool réellement disponible via `computeAvailableLeftover` → 409 `LEFTOVER_QUANTITY_EXCEEDED` (nouveau code, 4024) si dépassé ; 404 `PLANNING_ITEM_NOT_FOUND` si source inconnue / d'un autre foyer / elle- même un item « restes » (pas de restes-sur-restes). - `PATCH /planning/items/:id/discard-leftover` → `discardLeftover`, 200. Jette tout le pool restant (`discardedPortions += disponible`) ; idempotent (rien à jeter → renvoyé inchangé, aucun évènement). Exclusion : `shopping-list.service.ts` et `cooking-session.service.ts` filtrent `items: { where: { sourcePlanningItemId: null } }` dans leur `include` — un créneau qui ne fait que resservir un plat déjà cuisiné n'a rien à acheter ni à cuisiner. `getPlanningForDate` ne filtre pas (la grille affiche ces items). Analytics : `planning.leftover_assigned` / `planning.leftover_discarded`. Le mapping `PlanningItem` → `PlanningItemView` est factorisé (`toPlanningItemView`) — 4 chemins le partagent désormais. Tests (Mocha, DB) : `assignLeftoverPlanningItem` (succès + `recipeId` copié, 409 sur dépassement, 404 source étrangère/inconnue/« restes », cascade à la suppression de la source) ; `discardLeftover` (jette le disponible, idempotent, ne jette pas ce qui est déjà servi, 404) ; non-régression liste de courses et session de cuisine ignorant les items « restes ». Réf #34 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
64 lines
3.1 KiB
TypeScript
64 lines
3.1 KiB
TypeScript
import { z } from "zod";
|
|
import { MEALS, WEEK_DAYS } from "../types/planning.js";
|
|
|
|
// See schemas/auth.ts for the shared client/server validation rationale.
|
|
|
|
/**
|
|
* Payload accepted by `GET /planning`'s `?date=` query param. Only checks
|
|
* the `YYYY-MM-DD` *shape* — whether it's a real calendar date (e.g.
|
|
* rejecting `2026-02-30`) is checked service-side via
|
|
* `@batch-cooking/date-tools`'s `parseDateOnly`, not here: `packages/shared`
|
|
* has no runtime dependencies of its own, and pulling in a date library just
|
|
* for this one check isn't worth losing that.
|
|
*/
|
|
export const getPlanningByDateSchema = z.object({
|
|
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date invalide"),
|
|
});
|
|
/** Inferred TS type for {@link getPlanningByDateSchema}'s validated output. */
|
|
export type GetPlanningByDateInput = z.infer<typeof getPlanningByDateSchema>;
|
|
|
|
/**
|
|
* Payload accepted by `POST /planning/items` — adds a recipe to one
|
|
* (day, meal) slot of the household's planning for the week containing
|
|
* `date`, creating that week's `Planning` row on the fly if it doesn't
|
|
* exist yet (see `planning.service.ts`'s `addPlanningItem`). `weekDay`/
|
|
* `meal` are finally validated against a closed set here — until now
|
|
* `WEEK_DAYS`/`MEALS` were only a documented convention (see their doc
|
|
* comments in `types/planning.ts`), never enforced, since no write endpoint
|
|
* existed yet.
|
|
*/
|
|
export const addPlanningItemSchema = z.object({
|
|
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date invalide"),
|
|
weekDay: z.enum(WEEK_DAYS),
|
|
meal: z.enum(MEALS),
|
|
recipeId: z.number().int().positive(),
|
|
/**
|
|
* How many portions this slot must actually feed. The server derives the
|
|
* batched `portions` to prepare from this plus the recipe's own yield
|
|
* (`computeBatchedPortions`, `@batch-cooking/shared`) — the client never
|
|
* sends `portions` directly. See `PlanningItemView.portionsRequested`.
|
|
*/
|
|
portionsRequested: z.number().int().positive("Le nombre de portions doit être positif"),
|
|
});
|
|
/** Inferred TS type for {@link addPlanningItemSchema}'s validated output. */
|
|
export type AddPlanningItemInput = z.infer<typeof addPlanningItemSchema>;
|
|
|
|
/**
|
|
* Payload accepted by `POST /planning/items/leftover` — serves `portions`
|
|
* of another item's leftover pool on the `(date, weekDay, meal)` slot,
|
|
* creating a "leftovers" `PlanningItem` (see `planning.service.ts`'s
|
|
* `assignLeftoverPlanningItem`). `recipeId` is deliberately **not** here:
|
|
* a leftovers item can only be the same recipe as its source, so the
|
|
* server copies it from `sourceItemId` rather than trusting the client.
|
|
* `portions` is re-checked against the pool actually still available
|
|
* server-side — a client value over that is a `409 LEFTOVER_QUANTITY_EXCEEDED`.
|
|
*/
|
|
export const assignLeftoverSchema = z.object({
|
|
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date invalide"),
|
|
weekDay: z.enum(WEEK_DAYS),
|
|
meal: z.enum(MEALS),
|
|
sourceItemId: z.number().int().positive(),
|
|
portions: z.number().int().positive("Le nombre de portions doit être positif"),
|
|
});
|
|
/** Inferred TS type for {@link assignLeftoverSchema}'s validated output. */
|
|
export type AssignLeftoverInput = z.infer<typeof assignLeftoverSchema>;
|