feat(shopping): addRecipeToCart (idempotent via ON CONFLICT)
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m20s
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m20s
This commit is contained in:
@@ -25,12 +25,20 @@ export type ShoppingListSnapshot = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function addRecipeToCart(
|
export function addRecipeToCart(
|
||||||
_db: Database.Database,
|
db: Database.Database,
|
||||||
_recipeId: number,
|
recipeId: number,
|
||||||
_profileId: number | null,
|
profileId: number | null,
|
||||||
_servings?: number
|
servings?: number
|
||||||
): void {
|
): void {
|
||||||
throw new Error('not implemented');
|
const row = db
|
||||||
|
.prepare('SELECT servings_default FROM recipe WHERE id = ?')
|
||||||
|
.get(recipeId) as { servings_default: number | null } | undefined;
|
||||||
|
const resolved = servings ?? row?.servings_default ?? 4;
|
||||||
|
db.prepare(
|
||||||
|
`INSERT INTO shopping_cart_recipe (recipe_id, servings, added_by_profile_id)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(recipe_id) DO UPDATE SET servings = excluded.servings`
|
||||||
|
).run(recipeId, resolved, profileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeRecipeFromCart(_db: Database.Database, _recipeId: number): void {
|
export function removeRecipeFromCart(_db: Database.Database, _recipeId: number): void {
|
||||||
@@ -45,8 +53,17 @@ export function setCartServings(
|
|||||||
throw new Error('not implemented');
|
throw new Error('not implemented');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listShoppingList(_db: Database.Database): ShoppingListSnapshot {
|
export function listShoppingList(db: Database.Database): ShoppingListSnapshot {
|
||||||
throw new Error('not implemented');
|
const recipes = db
|
||||||
|
.prepare(
|
||||||
|
`SELECT cr.recipe_id, r.title, r.image_path, cr.servings,
|
||||||
|
COALESCE(r.servings_default, cr.servings) AS servings_default
|
||||||
|
FROM shopping_cart_recipe cr
|
||||||
|
JOIN recipe r ON r.id = cr.recipe_id
|
||||||
|
ORDER BY cr.added_at ASC`
|
||||||
|
)
|
||||||
|
.all() as ShoppingCartRecipe[];
|
||||||
|
return { recipes, rows: [], uncheckedCount: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleCheck(
|
export function toggleCheck(
|
||||||
|
|||||||
65
tests/integration/shopping-repository.test.ts
Normal file
65
tests/integration/shopping-repository.test.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { openInMemoryForTest } from '../../src/lib/server/db';
|
||||||
|
import { insertRecipe } from '../../src/lib/server/recipes/repository';
|
||||||
|
import {
|
||||||
|
addRecipeToCart,
|
||||||
|
listShoppingList
|
||||||
|
} from '../../src/lib/server/shopping/repository';
|
||||||
|
import type { Recipe } from '../../src/lib/types';
|
||||||
|
|
||||||
|
function recipe(overrides: Partial<Recipe> = {}): Recipe {
|
||||||
|
return {
|
||||||
|
id: null,
|
||||||
|
title: 'Test',
|
||||||
|
description: null,
|
||||||
|
source_url: null,
|
||||||
|
source_domain: null,
|
||||||
|
image_path: null,
|
||||||
|
servings_default: 4,
|
||||||
|
servings_unit: null,
|
||||||
|
prep_time_min: null,
|
||||||
|
cook_time_min: null,
|
||||||
|
total_time_min: null,
|
||||||
|
cuisine: null,
|
||||||
|
category: null,
|
||||||
|
ingredients: [],
|
||||||
|
steps: [],
|
||||||
|
tags: [],
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('addRecipeToCart', () => {
|
||||||
|
it('inserts recipe with default servings from recipe.servings_default', () => {
|
||||||
|
const db = openInMemoryForTest();
|
||||||
|
const id = insertRecipe(db, recipe({ title: 'Pasta', servings_default: 4 }));
|
||||||
|
addRecipeToCart(db, id, null);
|
||||||
|
const snap = listShoppingList(db);
|
||||||
|
expect(snap.recipes).toHaveLength(1);
|
||||||
|
expect(snap.recipes[0].servings).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects explicit servings override', () => {
|
||||||
|
const db = openInMemoryForTest();
|
||||||
|
const id = insertRecipe(db, recipe({ servings_default: 4 }));
|
||||||
|
addRecipeToCart(db, id, null, 2);
|
||||||
|
expect(listShoppingList(db).recipes[0].servings).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is idempotent: second insert updates servings, not fails', () => {
|
||||||
|
const db = openInMemoryForTest();
|
||||||
|
const id = insertRecipe(db, recipe({ servings_default: 4 }));
|
||||||
|
addRecipeToCart(db, id, null, 2);
|
||||||
|
addRecipeToCart(db, id, null, 6);
|
||||||
|
const snap = listShoppingList(db);
|
||||||
|
expect(snap.recipes).toHaveLength(1);
|
||||||
|
expect(snap.recipes[0].servings).toBe(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to servings=4 when recipe has no default', () => {
|
||||||
|
const db = openInMemoryForTest();
|
||||||
|
const id = insertRecipe(db, recipe({ servings_default: null }));
|
||||||
|
addRecipeToCart(db, id, null);
|
||||||
|
expect(listShoppingList(db).recipes[0].servings).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user