From 1889b0dea0edeb19319bca5baec05e8e45157813 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 23:08:20 +0200 Subject: [PATCH] feat(shopping): toggleCheck (idempotent) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Idempotentes Setzen/Loeschen von shopping_cart_check-Eintraegen ueber (name_key, unit_key). Check ueberlebt Recipe-Removals, solange ein anderes Rezept weiterhin zur Zeile beitraegt — verifiziert durch 3 neue Integrationstests (17 total). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/server/shopping/repository.ts | 20 ++++++-- tests/integration/shopping-repository.test.ts | 48 ++++++++++++++++++- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/lib/server/shopping/repository.ts b/src/lib/server/shopping/repository.ts index 5463f6c..2a40528 100644 --- a/src/lib/server/shopping/repository.ts +++ b/src/lib/server/shopping/repository.ts @@ -107,12 +107,22 @@ export function listShoppingList( } export function toggleCheck( - _db: Database.Database, - _nameKey: string, - _unitKey: string, - _checked: boolean + db: Database.Database, + nameKey: string, + unitKey: string, + checked: boolean ): void { - throw new Error('not implemented'); + if (checked) { + db.prepare( + `INSERT INTO shopping_cart_check (name_key, unit_key) + VALUES (?, ?) + ON CONFLICT(name_key, unit_key) DO NOTHING` + ).run(nameKey, unitKey); + } else { + db.prepare( + 'DELETE FROM shopping_cart_check WHERE name_key = ? AND unit_key = ?' + ).run(nameKey, unitKey); + } } export function clearCheckedItems(_db: Database.Database): void { diff --git a/tests/integration/shopping-repository.test.ts b/tests/integration/shopping-repository.test.ts index 437b087..8453e5f 100644 --- a/tests/integration/shopping-repository.test.ts +++ b/tests/integration/shopping-repository.test.ts @@ -5,7 +5,8 @@ import { addRecipeToCart, removeRecipeFromCart, listShoppingList, - setCartServings + setCartServings, + toggleCheck } from '../../src/lib/server/shopping/repository'; import type { Recipe } from '../../src/lib/types'; @@ -185,3 +186,48 @@ describe('listShoppingList aggregation', () => { expect(rows[0].total_quantity).toBeNull(); }); }); + +describe('toggleCheck', () => { + function setupOneRowCart() { + const db = openInMemoryForTest(); + const id = insertRecipe(db, recipe({ + ingredients: [{ position: 1, quantity: 200, unit: 'g', name: 'Mehl', note: null, raw_text: '', section_heading: null }] + })); + addRecipeToCart(db, id, null); + return { db, id }; + } + + it('marks a row as checked', () => { + const { db } = setupOneRowCart(); + toggleCheck(db, 'mehl', 'g', true); + const rows = listShoppingList(db).rows; + expect(rows[0].checked).toBe(1); + }); + + it('unchecks a row when passed false', () => { + const { db } = setupOneRowCart(); + toggleCheck(db, 'mehl', 'g', true); + toggleCheck(db, 'mehl', 'g', false); + expect(listShoppingList(db).rows[0].checked).toBe(0); + }); + + it('check survives removal of one recipe when another still contributes', () => { + const db = openInMemoryForTest(); + const a = insertRecipe(db, recipe({ + title: 'A', + ingredients: [{ position: 1, quantity: 100, unit: 'g', name: 'Mehl', note: null, raw_text: '', section_heading: null }] + })); + const b = insertRecipe(db, recipe({ + title: 'B', + ingredients: [{ position: 1, quantity: 200, unit: 'g', name: 'Mehl', note: null, raw_text: '', section_heading: null }] + })); + addRecipeToCart(db, a, null); + addRecipeToCart(db, b, null); + toggleCheck(db, 'mehl', 'g', true); + // Rezept A weg, Mehl kommt noch aus B — check bleibt, mit neuer Menge + removeRecipeFromCart(db, a); + const rows = listShoppingList(db).rows; + expect(rows[0].checked).toBe(1); + expect(rows[0].total_quantity).toBe(200); + }); +});