This commit is contained in:
Aerilyn Weber 2026-05-14 18:38:50 +09:00
parent 029940b079
commit e396f5088c
36 changed files with 4199 additions and 22 deletions

View file

@ -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';

View 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');
});
});

View 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',
}

View file

@ -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';

View 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;
};
}

View 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;
}

View file

@ -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';

View 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);
});
});

View 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>;

View file

@ -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);
});
});

View 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>;