API: séparer allergies et intolérances (kind sur Category) (step 7/8)
Retour fonctionnel : les allergies et intolérances doivent être
distinguées, pas listées ensemble.
- schema.prisma: enum AllergenKind (ALLERGY|INTOLERANCE) + Category.kind
(@default(ALLERGY), migration écrite à la main comme précédemment —
`migrate dev` refuse en environnement non-interactif ici — SQL généré
via `prisma migrate diff`).
- reference-seed-data.ts: classification par substance (Gluten et
Sulfites = INTOLERANCE, les 12 autres = ALLERGY — réaction
non-immunitaire documentée vs réaction immunitaire classique).
Corrige au passage l'upsert : `update: { kind }` au lieu de `update:
{}` — un reseed doit pouvoir corriger `kind` sur une Category déjà
existante, pas juste no-op.
- reference.service.ts / packages/shared: AllergyView gagne `kind`.
PATCH /profile/allergies ne change pas (une seule liste d'IDs, kind
ne sert qu'au groupement d'affichage côté client).
- Tests Mocha (29 passing) + Cucumber (15 scenarios, inchangés).
Classifié par substance (pas par utilisateur) — documenté comme
limitation connue dans le README. Web (split UI + hot saving sur
/foyer) dans le commit suivant.
This commit is contained in:
parent
9722f4a27b
commit
6cfa71730c
7 changed files with 86 additions and 28 deletions
10
README.md
10
README.md
|
|
@ -211,6 +211,16 @@ spec d'origine) précisément pour permettre cet upsert idempotent par nom.
|
|||
Liste des 14 allergènes : ceux du règlement UE 1169/2011 (annexe II) — liste
|
||||
standard, pas inventée.
|
||||
|
||||
**Allergies vs intolérances** (retour fonctionnel, pas dans le doc spec d'origine) :
|
||||
`Category.kind` (`AllergenKind` — `ALLERGY` | `INTOLERANCE`) classe chaque allergène.
|
||||
Seuls `Gluten` et `Sulfites` sont en `INTOLERANCE` (réaction non-immunitaire
|
||||
documentée) ; les 12 autres en `ALLERGY` (réaction immunitaire classique). Classifié
|
||||
par substance, pas par utilisateur — un même foyer ne peut pas déclarer "allergie au
|
||||
lait" pour un membre et "intolérance au lait" pour un autre ; a suffi pour le besoin
|
||||
exprimé, à revoir si ça devient un problème réel. `GET /reference/allergies` renvoie
|
||||
`kind` dans chaque `AllergyView` ; `PATCH /profile/allergies` ne change pas (une
|
||||
seule liste d'IDs, `kind` ne sert qu'à grouper l'affichage côté client).
|
||||
|
||||
## Foyer & profil — nom, régime, allergènes (apps/api)
|
||||
|
||||
Nécessitent tous une session (`requireAuth`) — contrairement aux endpoints de
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "AllergenKind" AS ENUM ('ALLERGY', 'INTOLERANCE');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "category" ADD COLUMN "kind" "AllergenKind" NOT NULL DEFAULT 'ALLERGY';
|
||||
|
|
@ -35,11 +35,22 @@ model Diet {
|
|||
@@map("diet")
|
||||
}
|
||||
|
||||
/// Not in the original spec doc — a category is either a true (IgE-mediated)
|
||||
/// allergy or a non-immune intolerance; the UI groups selectable allergens
|
||||
/// into two separate lists (`AllergySelect`, apps/web) instead of one flat
|
||||
/// "allergies & intolérances" list.
|
||||
enum AllergenKind {
|
||||
ALLERGY
|
||||
INTOLERANCE
|
||||
}
|
||||
|
||||
/// Enumeration-style table, meant to grow over time (e.g. allergy nuances).
|
||||
/// `name` is `@unique` for the same reason as `Diet.name` above.
|
||||
/// `name` is `@unique` for the same reason as `Diet.name` above. `kind` is
|
||||
/// also not in the original spec doc — see {@link AllergenKind}.
|
||||
model Category {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
kind AllergenKind @default(ALLERGY)
|
||||
|
||||
allergies Allergy[]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { PrismaClient } from "@prisma/client";
|
||||
import type { AllergenKind, PrismaClient } from "@prisma/client";
|
||||
|
||||
// Short, optional-to-pick regime list — `UserProfile.dietId` stays
|
||||
// nullable, this is not meant to be exhaustive.
|
||||
|
|
@ -6,22 +6,26 @@ const DIETS = ["Omnivore", "Végétarien", "Végan", "Pescétarien", "Sans glute
|
|||
|
||||
// The 14 allergens EU Regulation 1169/2011 (Annex II) requires food
|
||||
// businesses to declare — a standard, defensible reference list rather than
|
||||
// an invented one.
|
||||
const ALLERGENS = [
|
||||
"Gluten",
|
||||
"Crustacés",
|
||||
"Œufs",
|
||||
"Poissons",
|
||||
"Arachides",
|
||||
"Soja",
|
||||
"Lait",
|
||||
"Fruits à coque",
|
||||
"Céleri",
|
||||
"Moutarde",
|
||||
"Graines de sésame",
|
||||
"Sulfites",
|
||||
"Lupin",
|
||||
"Mollusques",
|
||||
// an invented one. Split into ALLERGY (classic IgE-mediated immune
|
||||
// reaction) vs INTOLERANCE (non-immune — gluten sensitivity, sulfite
|
||||
// sensitivity) per the product decision discussed in chat: only Gluten and
|
||||
// Sulfites are commonly-recognized intolerances among the 14; the rest are
|
||||
// true allergens.
|
||||
const ALLERGENS: Array<{ name: string; kind: AllergenKind }> = [
|
||||
{ name: "Gluten", kind: "INTOLERANCE" },
|
||||
{ name: "Crustacés", kind: "ALLERGY" },
|
||||
{ name: "Œufs", kind: "ALLERGY" },
|
||||
{ name: "Poissons", kind: "ALLERGY" },
|
||||
{ name: "Arachides", kind: "ALLERGY" },
|
||||
{ name: "Soja", kind: "ALLERGY" },
|
||||
{ name: "Lait", kind: "ALLERGY" },
|
||||
{ name: "Fruits à coque", kind: "ALLERGY" },
|
||||
{ name: "Céleri", kind: "ALLERGY" },
|
||||
{ name: "Moutarde", kind: "ALLERGY" },
|
||||
{ name: "Graines de sésame", kind: "ALLERGY" },
|
||||
{ name: "Sulfites", kind: "INTOLERANCE" },
|
||||
{ name: "Lupin", kind: "ALLERGY" },
|
||||
{ name: "Mollusques", kind: "ALLERGY" },
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -40,12 +44,14 @@ export async function seedReferenceData(prisma: PrismaClient): Promise<void> {
|
|||
// `Allergy` itself carries no `name` — it's the selectable instance of a
|
||||
// named `Category` (see schema.prisma) — so seeding an allergen means one
|
||||
// Category (upserted by name) plus exactly one Allergy row under it,
|
||||
// created only the first time.
|
||||
for (const name of ALLERGENS) {
|
||||
// created only the first time. `update: { kind }` (not `{}`) — a reseed
|
||||
// must correct `kind` on an already-existing category if the
|
||||
// classification above ever changes, not just skip it.
|
||||
for (const { name, kind } of ALLERGENS) {
|
||||
const category = await prisma.category.upsert({
|
||||
where: { name },
|
||||
update: {},
|
||||
create: { name },
|
||||
update: { kind },
|
||||
create: { name, kind },
|
||||
});
|
||||
const existing = await prisma.allergy.findFirst({ where: { categoryId: category.id } });
|
||||
if (!existing) {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,12 @@ export async function getDiets(): Promise<DietView[]> {
|
|||
*/
|
||||
export async function getAllergies(): Promise<AllergyView[]> {
|
||||
const allergies = await prisma.allergy.findMany({
|
||||
include: { category: { select: { name: true } } },
|
||||
include: { category: { select: { name: true, kind: true } } },
|
||||
orderBy: { category: { name: "asc" } },
|
||||
});
|
||||
return allergies.map((allergy) => ({ id: allergy.id, name: allergy.category.name }));
|
||||
return allergies.map((allergy) => ({
|
||||
id: allergy.id,
|
||||
name: allergy.category.name,
|
||||
kind: allergy.category.kind,
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,17 @@ describe("Reference data", () => {
|
|||
expect(res.status).to.equal(200);
|
||||
expect(res.body).to.have.length(14);
|
||||
expect(res.body.map((a: { name: string }) => a.name)).to.include("Arachides");
|
||||
expect(res.body[0]).to.have.keys(["id", "name"]);
|
||||
expect(res.body[0]).to.have.keys(["id", "name", "kind"]);
|
||||
});
|
||||
|
||||
it("classifies Gluten and Sulfites as intolerances, the rest as allergies", async () => {
|
||||
const res = await request(app).get("/reference/allergies");
|
||||
|
||||
const byName = (name: string) => res.body.find((a: { name: string }) => a.name === name);
|
||||
expect(byName("Gluten").kind).to.equal("INTOLERANCE");
|
||||
expect(byName("Sulfites").kind).to.equal("INTOLERANCE");
|
||||
expect(byName("Arachides").kind).to.equal("ALLERGY");
|
||||
expect(res.body.filter((a: { kind: string }) => a.kind === "INTOLERANCE")).to.have.length(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,15 +7,27 @@ export interface DietView {
|
|||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an allergen is a true (IgE-mediated) allergy or a non-immune
|
||||
* intolerance — mirrors `AllergenKind` in `schema.prisma`. Declared by hand
|
||||
* here rather than derived from the Prisma enum, same reasoning as
|
||||
* `SafeUserProfile`: `apps/web` must not depend on `@prisma/client`.
|
||||
*/
|
||||
export type AllergenKind = "ALLERGY" | "INTOLERANCE";
|
||||
|
||||
/**
|
||||
* A selectable allergen, as returned by `GET /reference/allergies`.
|
||||
*
|
||||
* `name` is resolved server-side from the parent `Category` — the `Allergy`
|
||||
* table itself carries no name of its own (see `schema.prisma`), so this
|
||||
* flattens that split away: callers just get `{id, name}` and never need to
|
||||
* know a `Category` exists underneath.
|
||||
* know a `Category` exists underneath. `kind` groups allergens into two
|
||||
* separate lists client-side (`AllergySelect`, `apps/web`) rather than one
|
||||
* flat "allergies & intolérances" list — a single `PATCH /profile/allergies`
|
||||
* call still covers both, this is a display grouping only.
|
||||
*/
|
||||
export interface AllergyView {
|
||||
id: number;
|
||||
name: string;
|
||||
kind: AllergenKind;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue