Phase 8
This commit is contained in:
parent
029940b079
commit
e396f5088c
36 changed files with 4199 additions and 22 deletions
110
packages/api/src/modules/meal-plans/shopping-gap.service.test.ts
Normal file
110
packages/api/src/modules/meal-plans/shopping-gap.service.test.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ShoppingGapService } from './shopping-gap.service.js';
|
||||
import type { MealPlanRepository } from './meal-plans.repository.js';
|
||||
import type { RecipesRepository } from '../recipes/recipes.repository.js';
|
||||
import type { PantryRepository } from '../pantry/pantry.repository.js';
|
||||
import type { ProductsRepository } from '../products/products.repository.js';
|
||||
import { NotFoundError } from '../../common/errors.js';
|
||||
|
||||
describe(ShoppingGapService.name, () => {
|
||||
let service: ShoppingGapService;
|
||||
let mockMealRepo: { [K in keyof MealPlanRepository]: ReturnType<typeof vi.fn> };
|
||||
let mockRecipesRepo: { [K in keyof RecipesRepository]: ReturnType<typeof vi.fn> };
|
||||
let mockPantryRepo: { [K in keyof PantryRepository]: ReturnType<typeof vi.fn> };
|
||||
let mockProductsRepo: { [K in keyof ProductsRepository]: ReturnType<typeof vi.fn> };
|
||||
|
||||
beforeEach(() => {
|
||||
mockMealRepo = { findById: vi.fn() } as never;
|
||||
mockRecipesRepo = { findById: vi.fn() } as never;
|
||||
mockPantryRepo = { findActiveByHousehold: vi.fn() } as never;
|
||||
mockProductsRepo = { findByIds: vi.fn() } as never;
|
||||
|
||||
service = new ShoppingGapService({
|
||||
mealPlanRepository: mockMealRepo as unknown as MealPlanRepository,
|
||||
recipesRepository: mockRecipesRepo as unknown as RecipesRepository,
|
||||
pantryRepository: mockPantryRepo as unknown as PantryRepository,
|
||||
productsRepository: mockProductsRepo as unknown as ProductsRepository,
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateGap', () => {
|
||||
it('throws NotFoundError if plan is missing', async () => {
|
||||
mockMealRepo.findById.mockResolvedValue(null);
|
||||
await expect(service.calculateGap('hh1', 'p1')).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it('correctly scales recipe ingredients and contrasts against pantry', async () => {
|
||||
// 1. Setup Meal Plan with 1 meal
|
||||
// Recipe A planned for 4 servings.
|
||||
mockMealRepo.findById.mockResolvedValue({
|
||||
_id: 'plan1',
|
||||
days: [
|
||||
{
|
||||
meals: [
|
||||
{ recipeId: 'recipe1', servings: 4 }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 2. Recipe 1: serves 2, needs 100g of ProdA (total needed = 200g for 4 servings)
|
||||
mockRecipesRepo.findById.mockResolvedValue({
|
||||
_id: 'recipe1',
|
||||
servings: 2,
|
||||
ingredients: [
|
||||
{ productId: 'prodA', quantity: 100, unit: 'g', isOptional: false }
|
||||
]
|
||||
});
|
||||
|
||||
// 3. Products Info
|
||||
mockProductsRepo.findByIds.mockResolvedValue([
|
||||
{ _id: 'prodA', name: 'Flour', category: 'baking' }
|
||||
]);
|
||||
|
||||
// 4. Pantry only has 50g of ProdA. Missing amount should be 150g!
|
||||
mockPantryRepo.findActiveByHousehold.mockResolvedValue([
|
||||
{ productId: 'prodA', quantity: 50 }
|
||||
]);
|
||||
|
||||
const result = await service.calculateGap('hh1', 'plan1');
|
||||
|
||||
expect(result.mealPlanId).toBe('plan1');
|
||||
expect(result.missingItems.length).toBe(1);
|
||||
|
||||
const gap = result.missingItems[0]!;
|
||||
expect(gap.productId).toBe('prodA');
|
||||
expect(gap.productName).toBe('Flour');
|
||||
expect(gap.requiredQuantity).toBe(200); // 100g * (4 planned / 2 base)
|
||||
expect(gap.pantryQuantity).toBe(50);
|
||||
expect(gap.missingQuantity).toBe(150);
|
||||
expect(gap.unit).toBe('g');
|
||||
});
|
||||
|
||||
it('does not include products that are fully stocked', async () => {
|
||||
mockMealRepo.findById.mockResolvedValue({
|
||||
_id: 'plan2',
|
||||
days: [
|
||||
{
|
||||
meals: [{ recipeId: 'recipe1', servings: 2 }]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
mockRecipesRepo.findById.mockResolvedValue({
|
||||
_id: 'recipe1',
|
||||
servings: 2,
|
||||
ingredients: [
|
||||
{ productId: 'prodB', quantity: 50, unit: 'g', isOptional: false }
|
||||
]
|
||||
});
|
||||
|
||||
mockProductsRepo.findByIds.mockResolvedValue([{ _id: 'prodB', name: 'Salt' }]);
|
||||
|
||||
// Pantry has 100g (more than enough)
|
||||
mockPantryRepo.findActiveByHousehold.mockResolvedValue([{ productId: 'prodB', quantity: 100 }]);
|
||||
|
||||
const result = await service.calculateGap('hh1', 'plan2');
|
||||
expect(result.missingItems.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue