feat: composizione corporea, deficit dinamico, condizioni mediche (PRD §5.1)

Tre leve per migliorare l'accuratezza del piano oltre Mifflin-St Jeor.

1. Composizione corporea (US Navy + Katch-McArdle)
   - Profile: neckCm/waistCm/hipsCm opzionali in input
   - bodyFatPct calcolato lato server con la formula US Navy metrica
     (Hodgdon-Beckett 1984) e cifrato at-rest come dato sanitario
   - BMR usa Katch-McArdle (FFM-based) quando bodyFatPct è noto, Mifflin
     altrimenti — più accurato del 5-10% sui casi fuori-norma
   - Test: estimateBodyFatPercentageUSNavy + calculateBmrKatchMcArdle

2. Deficit dinamico
   - Profile.targetWeeklyLossKg (kg/settimana, opzionale)
   - computeDailyKcalTarget calcola il deficit da kg/sett ×7700/7 con due cap
     di sicurezza: 30% TDEE (Schoenfeld 2014) e 1% peso/sett (Helms 2014)
   - plan.routes lo usa al posto del -500/-200 hard-coded
   - Test: 6 casi (mantenimento, transizione, intensiva, cap peso, cap TDEE,
     default mancante)

3. Condizioni mediche strutturate
   - Profile.medicalConditions (array JSON cifrato)
   - 11 condizioni: 7 adattabili (tiroide → BMR -10%, diabete II, IBS,
     dislipidemia, ipertensione, reni → cap proteine 0.8 g/kg, fegato →
     1.0 g/kg) + 4 escludenti (gravidanza, allattamento, diabete I, disturbi
     alimentari)
   - PATCH /me/profile/conditions per salvataggio mirato dall'onboarding
   - hasExcludingCondition: plan.routes restituisce 409 medical_block se
     attiva una condizione incompatibile

Onboarding allargato a 6 step: aggiunto "Condizioni mediche" tra Profile
e Preferences. Step labels e numeri rinumerati.

Schema migration 20260429203451_add_profile_health_fields.

44/44 unit test verdi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Opus 4.7 committed 2026-04-29 22:44:16 +02:00
1 parent feb54c1f9a
commit 9401497f03
15 files changed
+744 -26

No files matched your search

+11
View File
@@ -1,5 +1,7 @@
import { z } from 'zod';
import { MEDICAL_CONDITIONS } from '../medical/conditions.js';
export const GENDERS = ['MALE', 'FEMALE', 'OTHER'] as const;
export const ACTIVITY_LEVELS = ['SEDENTARY', 'LIGHT', 'MODERATE', 'INTENSE'] as const;
@@ -12,6 +14,15 @@ export const profileInputSchema = z.object({
weightGoalKg: z.coerce.number().min(35).max(300),
activityLevel: z.enum(ACTIVITY_LEVELS),
targetDate: z.coerce.date().optional(),
// Velocità di calo desiderata (kg/sett.). Cap 0.25-1.5 per UI; il backend
// applica anche un cap di sicurezza dinamico in base al peso.
targetWeeklyLossKg: z.coerce.number().min(0.1).max(1.5).optional(),
// Condizioni mediche dichiarate. Le "escludenti" bloccano la generazione piano.
medicalConditions: z.array(z.enum(MEDICAL_CONDITIONS)).max(20).optional(),
// Circonferenza collo: serve alla formula US Navy per stimare la BF%.
neckCm: z.coerce.number().min(20).max(60).optional(),
waistCm: z.coerce.number().min(40).max(200).optional(),
hipsCm: z.coerce.number().min(40).max(200).optional(),
});
export type ProfileInput = z.input<typeof profileInputSchema>;