feat: KitchenStock light UI, Gemma 31B AI, supermarket store filters & 130+ vegan products

This commit is contained in:
KitchenStock
2026-07-26 20:08:16 +02:00
commit 83fcf6909c
81 changed files with 17001 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
// Dummy mock for Next.js 'server-only' package in Vitest environment
export {};
+54
View File
@@ -0,0 +1,54 @@
import { describe, it, expect } from 'vitest';
import { MockKitchenAiProvider } from '@/lib/ai/client';
describe('MockKitchenAiProvider (No Billing Test)', () => {
const provider = new MockKitchenAiProvider();
it('suggests recipes with expected structured outputs', async () => {
const recipes = await provider.suggestRecipes({
householdId: 'test-household',
inventory: [
{
id: 'prod-1',
householdId: 'test-household',
categoryId: null,
defaultLocationId: null,
defaultUnitId: null,
name: 'Reis',
brand: null,
description: null,
barcode: null,
imagePath: null,
packageSize: 1000,
packageUnit: 'g',
minimumQuantity: 100,
favorite: true,
preferredStore: null,
estimatedPriceCents: 249,
priceUpdatedAt: null,
archived: false,
totalQuantity: 1000,
stockValueCents: 249,
isLowStock: false,
isExpiringSoon: false,
earliestExpirationDate: null,
},
],
});
expect(recipes).toHaveLength(1);
expect(recipes[0].title).toBeDefined();
expect(recipes[0].inventoryCoveragePercent).toBeGreaterThan(0);
});
it('returns verified price searches via Mock Provider', async () => {
const price = await provider.searchPrices({
householdId: 'test-household',
query: 'Spaghetti',
retailer: 'REWE',
});
expect(price.priceCents).toBe(189);
expect(price.confidence).toBe('HIGH');
});
});
+27
View File
@@ -0,0 +1,27 @@
import { describe, it, expect } from 'vitest';
import { formatCurrency, calculateBasePrice, calculateStockValueCents } from '@/lib/formatting/currency';
describe('Currency & Stock Value Formatting', () => {
it('formats integer cents into German Euro currency string', () => {
expect(formatCurrency(249)).toContain('2,49');
expect(formatCurrency(0)).toContain('0,00');
expect(formatCurrency(1299)).toContain('12,99');
});
it('calculates full package stock value correctly', () => {
// 2 packages @ 1,99 € (199 Cent) = 398 Cent
expect(calculateStockValueCents(2, 1, 199)).toBe(398);
});
it('calculates partial package stock value correctly (750g of 1000g @ 1,29 € = 0,97 €)', () => {
// 750g / 1000g * 129 Cent = 96.75 Cent -> rounded to 97 Cent
expect(calculateStockValueCents(750, 1000, 129)).toBe(97);
});
it('calculates base price (Grundpreis) per 1kg for grams', () => {
// 500g Barilla Spaghetti @ 1,89 € (189 Cent) -> 3.78 € / kg (378 Cent)
const base = calculateBasePrice(189, 500, 'g');
expect(base.basePriceCents).toBe(378);
expect(base.basePriceUnit).toBe('1 kg');
});
});
+20
View File
@@ -0,0 +1,20 @@
import { describe, it, expect } from 'vitest';
import { formatDateGerman, getExpirationStatus } from '@/lib/formatting/date';
describe('Date & Expiration Status Helpers', () => {
it('formats ISO dates into German TT.MM.JJJJ format', () => {
expect(formatDateGerman('2026-07-26')).toBe('26.07.2026');
});
it('returns OK for null expiration date', () => {
const status = getExpirationStatus(null);
expect(status.status).toBe('OK');
expect(status.label).toBe('Kein Ablaufdatum');
});
it('correctly flags expired dates', () => {
const status = getExpirationStatus('2020-01-01');
expect(status.status).toBe('EXPIRED');
expect(status.label).toContain('Abgelaufen');
});
});