Phase 8
This commit is contained in:
parent
029940b079
commit
e396f5088c
36 changed files with 4199 additions and 22 deletions
|
|
@ -8,3 +8,4 @@ export * from './purchase.enums.js';
|
|||
export * from './product.enums.js';
|
||||
export * from './recipe.enums.js';
|
||||
export * from './pantry.enums.js';
|
||||
export * from './meal-plan.enums.js';
|
||||
|
|
|
|||
17
packages/shared/src/enums/meal-plan.enums.test.ts
Normal file
17
packages/shared/src/enums/meal-plan.enums.test.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { MealType, MealPlanStatus } from './meal-plan.enums.js';
|
||||
|
||||
describe('MealPlan Enums', () => {
|
||||
it('should have correct MealType values', () => {
|
||||
expect(MealType.BREAKFAST).toBe('breakfast');
|
||||
expect(MealType.LUNCH).toBe('lunch');
|
||||
expect(MealType.DINNER).toBe('dinner');
|
||||
expect(MealType.SNACK).toBe('snack');
|
||||
});
|
||||
|
||||
it('should have correct MealPlanStatus values', () => {
|
||||
expect(MealPlanStatus.DRAFT).toBe('draft');
|
||||
expect(MealPlanStatus.ACTIVE).toBe('active');
|
||||
expect(MealPlanStatus.COMPLETED).toBe('completed');
|
||||
});
|
||||
});
|
||||
18
packages/shared/src/enums/meal-plan.enums.ts
Normal file
18
packages/shared/src/enums/meal-plan.enums.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Type of meal in a meal plan
|
||||
*/
|
||||
export enum MealType {
|
||||
BREAKFAST = 'breakfast',
|
||||
LUNCH = 'lunch',
|
||||
DINNER = 'dinner',
|
||||
SNACK = 'snack',
|
||||
}
|
||||
|
||||
/**
|
||||
* Status of a weekly meal plan
|
||||
*/
|
||||
export enum MealPlanStatus {
|
||||
DRAFT = 'draft',
|
||||
ACTIVE = 'active',
|
||||
COMPLETED = 'completed',
|
||||
}
|
||||
|
|
@ -14,4 +14,6 @@ export * from './purchase.js';
|
|||
export * from './product.js';
|
||||
export * from './recipe.js';
|
||||
export * from './pantry.js';
|
||||
export * from './meal-plan.js';
|
||||
export * from './nutrition-target.js';
|
||||
export * from './freshness.js';
|
||||
|
|
|
|||
41
packages/shared/src/types/meal-plan.ts
Normal file
41
packages/shared/src/types/meal-plan.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import type { NutritionInfo } from './product.js';
|
||||
import type { MealType, MealPlanStatus } from '../enums/meal-plan.enums.js';
|
||||
|
||||
export interface PlannedMeal {
|
||||
id: string; // UUID for UI reference
|
||||
type: MealType;
|
||||
recipeId?: string;
|
||||
recipeName: string; // Denormalized for display without full recipe fetch
|
||||
servings: number;
|
||||
customName?: string;
|
||||
customNutrition?: NutritionInfo;
|
||||
perServingNutrition: NutritionInfo;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface MealPlanDay {
|
||||
date: string; // ISO date string (YYYY-MM-DD)
|
||||
meals: PlannedMeal[];
|
||||
dailyNutritionTotal: NutritionInfo; // Computed field
|
||||
}
|
||||
|
||||
export interface MealPlan {
|
||||
_id: string;
|
||||
householdId: string;
|
||||
weekStartDate: string; // Monday of the week (ISO date string YYYY-MM-DD)
|
||||
days: MealPlanDay[];
|
||||
status: MealPlanStatus;
|
||||
shoppingListId?: string;
|
||||
createdBy: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface MealPlanResponse {
|
||||
data: MealPlan[];
|
||||
pagination: {
|
||||
cursor: string | null;
|
||||
hasMore: boolean;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
15
packages/shared/src/types/nutrition-target.ts
Normal file
15
packages/shared/src/types/nutrition-target.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export interface NutritionTarget {
|
||||
_id: string;
|
||||
userId: string;
|
||||
householdId: string;
|
||||
dailyCalories: number;
|
||||
proteinG: number;
|
||||
carbsG: number;
|
||||
fatG: number;
|
||||
fiberG?: number;
|
||||
sodiumMg?: number;
|
||||
sugarG?: number;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
|
@ -10,6 +10,8 @@ export * from './medicine-price.schemas.js';
|
|||
export * from './refill.schemas.js';
|
||||
export * from './purchase.schemas.js';
|
||||
export * from './product.schemas.js';
|
||||
export * from './meal-plan.schemas.js';
|
||||
export * from './nutrition-target.schemas.js';
|
||||
export * from './recipe.schemas.js';
|
||||
export * from './pantry.schemas.js';
|
||||
export * from './freshness-rule.schemas.js';
|
||||
|
|
|
|||
42
packages/shared/src/validation/meal-plan.schemas.test.ts
Normal file
42
packages/shared/src/validation/meal-plan.schemas.test.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { CreateMealPlanSchema } from './meal-plan.schemas.js';
|
||||
import { MealType, MealPlanStatus } from '../enums/meal-plan.enums.js';
|
||||
|
||||
describe('MealPlan Schemas', () => {
|
||||
const validMeal = {
|
||||
id: '550e8400-e29b-41d4-a716-446655440000',
|
||||
type: MealType.DINNER,
|
||||
recipeName: 'Chicken Salad',
|
||||
servings: 2,
|
||||
perServingNutrition: { calories: 300, protein: 30, carbs: 10, fat: 15 }
|
||||
};
|
||||
|
||||
const validDay = {
|
||||
date: '2026-05-18',
|
||||
meals: [validMeal],
|
||||
dailyNutritionTotal: { calories: 600, protein: 60, carbs: 20, fat: 30 }
|
||||
};
|
||||
|
||||
const validPlan = {
|
||||
weekStartDate: '2026-05-18',
|
||||
days: Array(7).fill(validDay),
|
||||
status: MealPlanStatus.DRAFT
|
||||
};
|
||||
|
||||
it('should validate a valid meal plan', () => {
|
||||
const result = CreateMealPlanSchema.safeParse(validPlan);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject a plan with missing days', () => {
|
||||
const invalidPlan = { ...validPlan, days: validPlan.days.slice(0, 6) };
|
||||
const result = CreateMealPlanSchema.safeParse(invalidPlan);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject invalid date format', () => {
|
||||
const invalidPlan = { ...validPlan, weekStartDate: '18-05-2026' };
|
||||
const result = CreateMealPlanSchema.safeParse(invalidPlan);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
63
packages/shared/src/validation/meal-plan.schemas.ts
Normal file
63
packages/shared/src/validation/meal-plan.schemas.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { z } from 'zod/v4';
|
||||
import { MealType, MealPlanStatus } from '../enums/meal-plan.enums.js';
|
||||
import { NutritionInfoSchema } from './product.schemas.js';
|
||||
|
||||
export const PlannedMealSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
type: z.nativeEnum(MealType),
|
||||
recipeId: z.string().optional(),
|
||||
recipeName: z.string().min(1),
|
||||
servings: z.number().positive(),
|
||||
customName: z.string().optional(),
|
||||
customNutrition: NutritionInfoSchema.optional(),
|
||||
perServingNutrition: NutritionInfoSchema,
|
||||
notes: z.string().max(500).optional(),
|
||||
});
|
||||
|
||||
export const MealPlanDaySchema = z.object({
|
||||
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be YYYY-MM-DD'),
|
||||
meals: z.array(PlannedMealSchema),
|
||||
dailyNutritionTotal: NutritionInfoSchema,
|
||||
});
|
||||
|
||||
export const CreateMealPlanSchema = z.object({
|
||||
weekStartDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be YYYY-MM-DD'),
|
||||
days: z.array(MealPlanDaySchema).length(7),
|
||||
status: z.nativeEnum(MealPlanStatus).default(MealPlanStatus.DRAFT),
|
||||
});
|
||||
|
||||
export const UpdateMealPlanSchema = z.object({
|
||||
days: z.array(MealPlanDaySchema).length(7).optional(),
|
||||
status: z.nativeEnum(MealPlanStatus).optional(),
|
||||
shoppingListId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const MealPlanQuerySchema = z.object({
|
||||
cursor: z.string().optional(),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(20),
|
||||
});
|
||||
|
||||
export const MealPlanResponseSchema = z.object({
|
||||
_id: z.string(),
|
||||
householdId: z.string(),
|
||||
weekStartDate: z.string(),
|
||||
days: z.array(MealPlanDaySchema),
|
||||
status: z.nativeEnum(MealPlanStatus),
|
||||
shoppingListId: z.string().optional(),
|
||||
createdBy: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
|
||||
export const MealPlanListResponseSchema = z.object({
|
||||
data: z.array(MealPlanResponseSchema),
|
||||
pagination: z.object({
|
||||
cursor: z.string().nullable(),
|
||||
hasMore: z.boolean(),
|
||||
total: z.number().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type CreateMealPlanInput = z.infer<typeof CreateMealPlanSchema>;
|
||||
export type UpdateMealPlanInput = z.infer<typeof UpdateMealPlanSchema>;
|
||||
export type MealPlanQueryInput = z.infer<typeof MealPlanQuerySchema>;
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { NutritionTargetSchema } from './nutrition-target.schemas.js';
|
||||
|
||||
describe('NutritionTarget Schemas', () => {
|
||||
const validTarget = {
|
||||
dailyCalories: 2000,
|
||||
proteinG: 150,
|
||||
carbsG: 200,
|
||||
fatG: 70,
|
||||
isActive: true
|
||||
};
|
||||
|
||||
it('should validate a valid target', () => {
|
||||
const result = NutritionTargetSchema.safeParse(validTarget);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject negative macros', () => {
|
||||
const invalidTarget = { ...validTarget, proteinG: -10 };
|
||||
const result = NutritionTargetSchema.safeParse(invalidTarget);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject zero calories', () => {
|
||||
const invalidTarget = { ...validTarget, dailyCalories: 0 };
|
||||
const result = NutritionTargetSchema.safeParse(invalidTarget);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
23
packages/shared/src/validation/nutrition-target.schemas.ts
Normal file
23
packages/shared/src/validation/nutrition-target.schemas.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { z } from 'zod/v4';
|
||||
|
||||
export const NutritionTargetSchema = z.object({
|
||||
dailyCalories: z.number().positive(),
|
||||
proteinG: z.number().min(0),
|
||||
carbsG: z.number().min(0),
|
||||
fatG: z.number().min(0),
|
||||
fiberG: z.number().min(0).optional(),
|
||||
sodiumMg: z.number().min(0).optional(),
|
||||
sugarG: z.number().min(0).optional(),
|
||||
isActive: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const NutritionTargetResponseSchema = NutritionTargetSchema.extend({
|
||||
_id: z.string(),
|
||||
userId: z.string(),
|
||||
householdId: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
|
||||
export type SetNutritionTargetInput = z.infer<typeof NutritionTargetSchema>;
|
||||
export type NutritionTargetResponse = z.infer<typeof NutritionTargetResponseSchema>;
|
||||
Loading…
Add table
Add a link
Reference in a new issue