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
@@ -0,0 +1,412 @@
-- Migration 00001: Initial Schema for KitchenStock
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Enums
CREATE TYPE household_role AS ENUM ('OWNER', 'ADMIN', 'MEMBER', 'VIEWER');
CREATE TYPE aldi_region_type AS ENUM ('NORD', 'SUED', 'UNSPECIFIED');
CREATE TYPE unit_type AS ENUM ('COUNT', 'WEIGHT', 'VOLUME', 'OTHER');
CREATE TYPE stock_movement_reason AS ENUM ('PURCHASE', 'CONSUMED', 'CORRECTION', 'INVENTORY', 'EXPIRED', 'DISCARDED', 'TRANSFER', 'INITIAL');
CREATE TYPE shopping_list_status AS ENUM ('ACTIVE', 'COMPLETED', 'ARCHIVED');
CREATE TYPE price_type AS ENUM ('REGULAR', 'OFFER', 'LOYALTY', 'UNKNOWN');
CREATE TYPE source_kind AS ENUM ('USER_PURCHASE', 'USER_ESTIMATE', 'OFFICIAL_RETAILER', 'OFFICIAL_PROSPECT', 'GEMINI_GROUNDED', 'HISTORICAL_AVERAGE', 'AI_ESTIMATE');
CREATE TYPE confidence_level AS ENUM ('HIGH', 'MEDIUM', 'LOW');
CREATE TYPE budget_period AS ENUM ('WEEKLY', 'MONTHLY', 'CUSTOM');
CREATE TYPE inventory_session_status AS ENUM ('OPEN', 'COMPLETED', 'CANCELLED');
CREATE TYPE diet_type AS ENUM ('OMNIVORE', 'VEGETARIAN', 'VEGAN', 'PESCETARIAN', 'CUSTOM');
CREATE TYPE pending_action_status AS ENUM ('PENDING', 'CONFIRMED', 'REJECTED', 'EXPIRED');
-- Trigger to update updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 1. Profiles
CREATE TABLE IF NOT EXISTS public.profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
display_name TEXT NOT NULL,
avatar_path TEXT,
locale TEXT NOT NULL DEFAULT 'de-DE',
timezone TEXT NOT NULL DEFAULT 'Europe/Berlin',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 2. Households
CREATE TABLE IF NOT EXISTS public.households (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
currency TEXT NOT NULL DEFAULT 'EUR',
locale TEXT NOT NULL DEFAULT 'de-DE',
timezone TEXT NOT NULL DEFAULT 'Europe/Berlin',
postal_code TEXT,
city TEXT,
aldi_region aldi_region_type NOT NULL DEFAULT 'UNSPECIFIED',
default_servings INT NOT NULL DEFAULT 2 CHECK (default_servings > 0),
owner_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 3. Household Members
CREATE TABLE IF NOT EXISTS public.household_members (
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
role household_role NOT NULL DEFAULT 'MEMBER',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (household_id, user_id)
);
-- 4. Categories
CREATE TABLE IF NOT EXISTS public.categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
name TEXT NOT NULL,
icon TEXT,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 5. Locations (supports nesting)
CREATE TABLE IF NOT EXISTS public.locations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
parent_location_id UUID REFERENCES public.locations(id) ON DELETE SET NULL,
name TEXT NOT NULL,
icon TEXT,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 6. Units
CREATE TABLE IF NOT EXISTS public.units (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID REFERENCES public.households(id) ON DELETE CASCADE, -- NULL for system defaults
name TEXT NOT NULL,
abbreviation TEXT NOT NULL,
unit_type unit_type NOT NULL DEFAULT 'COUNT',
allows_decimals BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 7. Products
CREATE TABLE IF NOT EXISTS public.products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
category_id UUID REFERENCES public.categories(id) ON DELETE SET NULL,
default_location_id UUID REFERENCES public.locations(id) ON DELETE SET NULL,
default_unit_id UUID REFERENCES public.units(id) ON DELETE SET NULL,
name TEXT NOT NULL,
brand TEXT,
description TEXT,
barcode TEXT,
image_path TEXT,
package_size NUMERIC(10, 3) NOT NULL DEFAULT 1.0,
package_unit TEXT NOT NULL DEFAULT 'Stück',
minimum_quantity NUMERIC(10, 3) NOT NULL DEFAULT 0.0 CHECK (minimum_quantity >= 0),
favorite BOOLEAN NOT NULL DEFAULT false,
preferred_store TEXT,
estimated_price_cents INT CHECK (estimated_price_cents >= 0),
price_updated_at TIMESTAMPTZ,
archived BOOLEAN NOT NULL DEFAULT false,
created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 8. Inventory Batches
CREATE TABLE IF NOT EXISTS public.inventory_batches (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
location_id UUID REFERENCES public.locations(id) ON DELETE SET NULL,
quantity NUMERIC(10, 3) NOT NULL DEFAULT 0.0 CHECK (quantity >= 0),
expiration_date DATE,
purchase_date DATE,
purchase_price_cents INT CHECK (purchase_price_cents >= 0),
store TEXT,
lot_number TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 9. Stock Movements
CREATE TABLE IF NOT EXISTS public.stock_movements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
batch_id UUID REFERENCES public.inventory_batches(id) ON DELETE SET NULL,
quantity_before NUMERIC(10, 3) NOT NULL,
quantity_change NUMERIC(10, 3) NOT NULL,
quantity_after NUMERIC(10, 3) NOT NULL CHECK (quantity_after >= 0),
reason stock_movement_reason NOT NULL,
note TEXT,
created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 10. Shopping Lists
CREATE TABLE IF NOT EXISTS public.shopping_lists (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
name TEXT NOT NULL DEFAULT 'Einkaufsliste',
status shopping_list_status NOT NULL DEFAULT 'ACTIVE',
created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 11. Shopping List Items
CREATE TABLE IF NOT EXISTS public.shopping_list_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shopping_list_id UUID NOT NULL REFERENCES public.shopping_lists(id) ON DELETE CASCADE,
product_id UUID REFERENCES public.products(id) ON DELETE SET NULL,
custom_name TEXT,
quantity NUMERIC(10, 3) NOT NULL DEFAULT 1.0 CHECK (quantity > 0),
unit_id UUID REFERENCES public.units(id) ON DELETE SET NULL,
estimated_unit_price_cents INT CHECK (estimated_unit_price_cents >= 0),
selected_store TEXT,
checked BOOLEAN NOT NULL DEFAULT false,
automatically_added BOOLEAN NOT NULL DEFAULT false,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 12. Purchases
CREATE TABLE IF NOT EXISTS public.purchases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
store TEXT NOT NULL,
purchase_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
total_price_cents INT NOT NULL DEFAULT 0 CHECK (total_price_cents >= 0),
receipt_image_path TEXT,
notes TEXT,
created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 13. Purchase Items
CREATE TABLE IF NOT EXISTS public.purchase_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
purchase_id UUID NOT NULL REFERENCES public.purchases(id) ON DELETE CASCADE,
product_id UUID REFERENCES public.products(id) ON DELETE SET NULL,
quantity NUMERIC(10, 3) NOT NULL DEFAULT 1.0 CHECK (quantity > 0),
unit_price_cents INT NOT NULL CHECK (unit_price_cents >= 0),
total_price_cents INT NOT NULL CHECK (total_price_cents >= 0),
package_size NUMERIC(10, 3),
package_unit TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 14. Price Observations
CREATE TABLE IF NOT EXISTS public.price_observations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
product_id UUID REFERENCES public.products(id) ON DELETE SET NULL,
product_name TEXT NOT NULL,
brand TEXT,
package_size NUMERIC(10, 3),
package_unit TEXT,
price_cents INT NOT NULL CHECK (price_cents >= 0),
base_price_cents INT CHECK (base_price_cents >= 0),
base_price_unit TEXT,
retailer TEXT NOT NULL,
price_type price_type NOT NULL DEFAULT 'REGULAR',
postal_code TEXT,
region TEXT,
source_url TEXT,
source_title TEXT,
observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
valid_from DATE,
valid_until DATE,
confidence confidence_level NOT NULL DEFAULT 'MEDIUM',
exact_match BOOLEAN NOT NULL DEFAULT true,
verified BOOLEAN NOT NULL DEFAULT false,
source_kind source_kind NOT NULL DEFAULT 'USER_ESTIMATE',
created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 15. Budgets
CREATE TABLE IF NOT EXISTS public.budgets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
name TEXT NOT NULL,
period budget_period NOT NULL DEFAULT 'MONTHLY',
amount_cents INT NOT NULL CHECK (amount_cents >= 0),
warning_threshold_percent INT NOT NULL DEFAULT 80 CHECK (warning_threshold_percent BETWEEN 1 AND 100),
start_date DATE NOT NULL,
end_date DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 16. Inventory Sessions
CREATE TABLE IF NOT EXISTS public.inventory_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
location_id UUID REFERENCES public.locations(id) ON DELETE SET NULL,
status inventory_session_status NOT NULL DEFAULT 'OPEN',
started_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 17. Inventory Session Items
CREATE TABLE IF NOT EXISTS public.inventory_session_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL REFERENCES public.inventory_sessions(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES public.products(id) ON DELETE CASCADE,
expected_quantity NUMERIC(10, 3) NOT NULL DEFAULT 0.0,
counted_quantity NUMERIC(10, 3) NOT NULL DEFAULT 0.0,
difference NUMERIC(10, 3) NOT NULL DEFAULT 0.0,
confirmed BOOLEAN NOT NULL DEFAULT false,
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 18. Dietary Preferences
CREATE TABLE IF NOT EXISTS public.dietary_preferences (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL UNIQUE REFERENCES public.households(id) ON DELETE CASCADE,
diet_type diet_type NOT NULL DEFAULT 'OMNIVORE',
excluded_ingredients TEXT[] NOT NULL DEFAULT '{}',
allergens TEXT[] NOT NULL DEFAULT '{}',
preferred_cuisines TEXT[] NOT NULL DEFAULT '{}',
maximum_preparation_minutes INT,
maximum_meal_budget_cents INT,
default_servings INT NOT NULL DEFAULT 2,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 19. Recipes
CREATE TABLE IF NOT EXISTS public.recipes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
title TEXT NOT NULL,
description TEXT,
servings INT NOT NULL DEFAULT 2,
preparation_minutes INT,
difficulty TEXT,
estimated_cost_cents INT,
inventory_coverage_percent INT NOT NULL DEFAULT 0,
ingredients_json JSONB NOT NULL DEFAULT '[]'::jsonb,
steps_json JSONB NOT NULL DEFAULT '[]'::jsonb,
dietary_tags TEXT[] NOT NULL DEFAULT '{}',
allergen_warnings TEXT[] NOT NULL DEFAULT '{}',
source_information_json JSONB,
created_by_ai BOOLEAN NOT NULL DEFAULT false,
created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 20. Meal Plans
CREATE TABLE IF NOT EXISTS public.meal_plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
name TEXT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
estimated_total_cost_cents INT,
plan_json JSONB NOT NULL DEFAULT '[]'::jsonb,
created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 21. AI Settings
CREATE TABLE IF NOT EXISTS public.ai_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL UNIQUE REFERENCES public.households(id) ON DELETE CASCADE,
enabled BOOLEAN NOT NULL DEFAULT true,
preferred_retailers TEXT[] NOT NULL DEFAULT '{"KAUFLAND", "REWE", "ALDI_NORD", "ALDI_SUED", "LIDL"}',
allow_google_search BOOLEAN NOT NULL DEFAULT true,
daily_request_limit INT NOT NULL DEFAULT 30 CHECK (daily_request_limit > 0),
automatic_price_updates BOOLEAN NOT NULL DEFAULT false,
price_update_interval_days INT NOT NULL DEFAULT 14 CHECK (price_update_interval_days > 0),
maximum_products_per_price_request INT NOT NULL DEFAULT 20 CHECK (maximum_products_per_price_request > 0),
save_chat_history BOOLEAN NOT NULL DEFAULT true,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 22. AI Request Logs
CREATE TABLE IF NOT EXISTS public.ai_request_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
user_id UUID REFERENCES public.profiles(id) ON DELETE SET NULL,
feature TEXT NOT NULL,
model TEXT NOT NULL,
grounding_used BOOLEAN NOT NULL DEFAULT false,
input_token_count INT,
output_token_count INT,
latency_ms INT,
successful BOOLEAN NOT NULL DEFAULT true,
error_code TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 23. AI Pending Actions
CREATE TABLE IF NOT EXISTS public.ai_pending_actions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
household_id UUID NOT NULL REFERENCES public.households(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
action_type TEXT NOT NULL,
payload_json JSONB NOT NULL,
status pending_action_status NOT NULL DEFAULT 'PENDING',
expires_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() + INTERVAL '24 hours'),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
confirmed_at TIMESTAMPTZ
);
-- 24. Rate Limit Events
CREATE TABLE IF NOT EXISTS public.rate_limit_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
action TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Triggers for updated_at
CREATE TRIGGER trg_profiles_updated_at BEFORE UPDATE ON public.profiles FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_households_updated_at BEFORE UPDATE ON public.households FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_categories_updated_at BEFORE UPDATE ON public.categories FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_locations_updated_at BEFORE UPDATE ON public.locations FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_products_updated_at BEFORE UPDATE ON public.products FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_inventory_batches_updated_at BEFORE UPDATE ON public.inventory_batches FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_shopping_lists_updated_at BEFORE UPDATE ON public.shopping_lists FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_shopping_list_items_updated_at BEFORE UPDATE ON public.shopping_list_items FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_purchases_updated_at BEFORE UPDATE ON public.purchases FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_budgets_updated_at BEFORE UPDATE ON public.budgets FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_inventory_session_items_updated_at BEFORE UPDATE ON public.inventory_session_items FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_dietary_preferences_updated_at BEFORE UPDATE ON public.dietary_preferences FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_recipes_updated_at BEFORE UPDATE ON public.recipes FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_meal_plans_updated_at BEFORE UPDATE ON public.meal_plans FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER trg_ai_settings_updated_at BEFORE UPDATE ON public.ai_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Indexes for optimal query performance
CREATE INDEX idx_household_members_user ON public.household_members(user_id);
CREATE INDEX idx_products_household ON public.products(household_id);
CREATE INDEX idx_products_category ON public.products(category_id);
CREATE INDEX idx_products_location ON public.products(default_location_id);
CREATE INDEX idx_products_barcode ON public.products(barcode);
CREATE INDEX idx_inventory_batches_product ON public.inventory_batches(product_id);
CREATE INDEX idx_inventory_batches_household ON public.inventory_batches(household_id);
CREATE INDEX idx_inventory_batches_exp ON public.inventory_batches(expiration_date);
CREATE INDEX idx_stock_movements_product ON public.stock_movements(product_id);
CREATE INDEX idx_stock_movements_household ON public.stock_movements(household_id);
CREATE INDEX idx_shopping_list_items_list ON public.shopping_list_items(shopping_list_id);
CREATE INDEX idx_purchases_household_date ON public.purchases(household_id, purchase_date);
CREATE INDEX idx_price_observations_product ON public.price_observations(product_id);
CREATE INDEX idx_price_observations_observed ON public.price_observations(observed_at);
CREATE INDEX idx_rate_limit_events_user_action ON public.rate_limit_events(user_id, action, created_at);
+211
View File
@@ -0,0 +1,211 @@
-- Migration 00002: Row Level Security (RLS) Policies and Helper Functions
-- 1. Helper Functions
CREATE OR REPLACE FUNCTION public.is_household_member(_household_id UUID)
RETURNS BOOLEAN
LANGUAGE sql
SECURITY DEFINER
SET search_path = public
STABLE
AS $$
SELECT EXISTS (
SELECT 1
FROM public.household_members
WHERE household_id = _household_id
AND user_id = auth.uid()
);
$$;
CREATE OR REPLACE FUNCTION public.has_household_role(_household_id UUID, _allowed_roles public.household_role[])
RETURNS BOOLEAN
LANGUAGE sql
SECURITY DEFINER
SET search_path = public
STABLE
AS $$
SELECT EXISTS (
SELECT 1
FROM public.household_members
WHERE household_id = _household_id
AND user_id = auth.uid()
AND role = ANY(_allowed_roles)
);
$$;
-- Enable RLS on all tables
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.households ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.household_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.categories ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.locations ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.units ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.products ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.inventory_batches ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.stock_movements ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.shopping_lists ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.shopping_list_items ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.purchases ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.purchase_items ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.price_observations ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.budgets ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.inventory_sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.inventory_session_items ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.dietary_preferences ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.recipes ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.meal_plans ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ai_settings ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ai_request_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.ai_pending_actions ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.rate_limit_events ENABLE ROW LEVEL SECURITY;
-- 2. Policies for profiles
CREATE POLICY "Users can view all profiles in their households"
ON public.profiles FOR SELECT
USING (
id = auth.uid() OR
EXISTS (
SELECT 1 FROM public.household_members m1
JOIN public.household_members m2 ON m1.household_id = m2.household_id
WHERE m1.user_id = auth.uid() AND m2.user_id = public.profiles.id
)
);
CREATE POLICY "Users can update their own profile"
ON public.profiles FOR UPDATE
USING (id = auth.uid())
WITH CHECK (id = auth.uid());
CREATE POLICY "Users can insert their own profile"
ON public.profiles FOR INSERT
WITH CHECK (id = auth.uid());
-- 3. Policies for households
CREATE POLICY "Members can view their households"
ON public.households FOR SELECT
USING (public.is_household_member(id));
CREATE POLICY "Authenticated users can create households"
ON public.households FOR INSERT
WITH CHECK (owner_id = auth.uid());
CREATE POLICY "OWNER and ADMIN can update household"
ON public.households FOR UPDATE
USING (public.has_household_role(id, ARRAY['OWNER', 'ADMIN']::public.household_role[]));
CREATE POLICY "Only OWNER can delete household"
ON public.households FOR DELETE
USING (owner_id = auth.uid());
-- 4. Policies for household_members
CREATE POLICY "Members can view household members"
ON public.household_members FOR SELECT
USING (public.is_household_member(household_id));
CREATE POLICY "OWNER and ADMIN can add household members"
ON public.household_members FOR INSERT
WITH CHECK (public.has_household_role(household_id, ARRAY['OWNER', 'ADMIN']::public.household_role[]));
CREATE POLICY "OWNER and ADMIN can update member roles"
ON public.household_members FOR UPDATE
USING (public.has_household_role(household_id, ARRAY['OWNER', 'ADMIN']::public.household_role[]));
CREATE POLICY "OWNER and ADMIN can remove members"
ON public.household_members FOR DELETE
USING (public.has_household_role(household_id, ARRAY['OWNER', 'ADMIN']::public.household_role[]) OR user_id = auth.uid());
-- Helper Macro macro-like pattern for tables scoped by household_id
-- Categories, Locations, Units, Products, Inventory Batches, Stock Movements, Shopping Lists, Purchases, Price Observations, Budgets, Inventory Sessions, Dietary Preferences, Recipes, Meal Plans, AI Settings, AI Pending Actions
-- Categories
CREATE POLICY "Members can view categories" ON public.categories FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "MEMBER, ADMIN, OWNER can insert categories" ON public.categories FOR INSERT WITH CHECK (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
CREATE POLICY "MEMBER, ADMIN, OWNER can update categories" ON public.categories FOR UPDATE USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
CREATE POLICY "ADMIN and OWNER can delete categories" ON public.categories FOR DELETE USING (public.has_household_role(household_id, ARRAY['ADMIN', 'OWNER']::public.household_role[]));
-- Locations
CREATE POLICY "Members can view locations" ON public.locations FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "MEMBER, ADMIN, OWNER can insert locations" ON public.locations FOR INSERT WITH CHECK (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
CREATE POLICY "MEMBER, ADMIN, OWNER can update locations" ON public.locations FOR UPDATE USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
CREATE POLICY "ADMIN and OWNER can delete locations" ON public.locations FOR DELETE USING (public.has_household_role(household_id, ARRAY['ADMIN', 'OWNER']::public.household_role[]));
-- Units
CREATE POLICY "Members can view units" ON public.units FOR SELECT USING (household_id IS NULL OR public.is_household_member(household_id));
CREATE POLICY "MEMBER, ADMIN, OWNER can insert units" ON public.units FOR INSERT WITH CHECK (household_id IS NOT NULL AND public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
CREATE POLICY "MEMBER, ADMIN, OWNER can update units" ON public.units FOR UPDATE USING (household_id IS NOT NULL AND public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
-- Products
CREATE POLICY "Members can view products" ON public.products FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "MEMBER, ADMIN, OWNER can insert products" ON public.products FOR INSERT WITH CHECK (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
CREATE POLICY "MEMBER, ADMIN, OWNER can update products" ON public.products FOR UPDATE USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
CREATE POLICY "ADMIN and OWNER can delete products" ON public.products FOR DELETE USING (public.has_household_role(household_id, ARRAY['ADMIN', 'OWNER']::public.household_role[]));
-- Inventory Batches
CREATE POLICY "Members can view inventory batches" ON public.inventory_batches FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "MEMBER, ADMIN, OWNER can modify inventory batches" ON public.inventory_batches FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
-- Stock Movements
CREATE POLICY "Members can view stock movements" ON public.stock_movements FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "MEMBER, ADMIN, OWNER can insert stock movements" ON public.stock_movements FOR INSERT WITH CHECK (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
-- Shopping Lists
CREATE POLICY "Members can view shopping lists" ON public.shopping_lists FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "MEMBER, ADMIN, OWNER can modify shopping lists" ON public.shopping_lists FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
-- Shopping List Items
CREATE POLICY "Members can view shopping list items" ON public.shopping_list_items FOR SELECT USING (
EXISTS (SELECT 1 FROM public.shopping_lists WHERE id = shopping_list_id AND public.is_household_member(household_id))
);
CREATE POLICY "MEMBER, ADMIN, OWNER can modify shopping list items" ON public.shopping_list_items FOR ALL USING (
EXISTS (SELECT 1 FROM public.shopping_lists WHERE id = shopping_list_id AND public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]))
);
-- Purchases & Purchase Items
CREATE POLICY "Members can view purchases" ON public.purchases FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "MEMBER, ADMIN, OWNER can modify purchases" ON public.purchases FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
CREATE POLICY "Members can view purchase items" ON public.purchase_items FOR SELECT USING (
EXISTS (SELECT 1 FROM public.purchases WHERE id = purchase_id AND public.is_household_member(household_id))
);
CREATE POLICY "MEMBER, ADMIN, OWNER can modify purchase items" ON public.purchase_items FOR ALL USING (
EXISTS (SELECT 1 FROM public.purchases WHERE id = purchase_id AND public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]))
);
-- Price Observations
CREATE POLICY "Members can view price observations" ON public.price_observations FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "MEMBER, ADMIN, OWNER can modify price observations" ON public.price_observations FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
-- Budgets
CREATE POLICY "Members can view budgets" ON public.budgets FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "ADMIN and OWNER can modify budgets" ON public.budgets FOR ALL USING (public.has_household_role(household_id, ARRAY['ADMIN', 'OWNER']::public.household_role[]));
-- Inventory Sessions & Session Items
CREATE POLICY "Members can view inventory sessions" ON public.inventory_sessions FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "MEMBER, ADMIN, OWNER can modify inventory sessions" ON public.inventory_sessions FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
CREATE POLICY "Members can view inventory session items" ON public.inventory_session_items FOR SELECT USING (
EXISTS (SELECT 1 FROM public.inventory_sessions WHERE id = session_id AND public.is_household_member(household_id))
);
CREATE POLICY "MEMBER, ADMIN, OWNER can modify inventory session items" ON public.inventory_session_items FOR ALL USING (
EXISTS (SELECT 1 FROM public.inventory_sessions WHERE id = session_id AND public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]))
);
-- Dietary Preferences, Recipes, Meal Plans, AI Settings, AI Pending Actions
CREATE POLICY "Members can view dietary preferences" ON public.dietary_preferences FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "MEMBER, ADMIN, OWNER can update dietary preferences" ON public.dietary_preferences FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
CREATE POLICY "Members can view recipes" ON public.recipes FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "MEMBER, ADMIN, OWNER can modify recipes" ON public.recipes FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
CREATE POLICY "Members can view meal plans" ON public.meal_plans FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "MEMBER, ADMIN, OWNER can modify meal plans" ON public.meal_plans FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
CREATE POLICY "Members can view AI settings" ON public.ai_settings FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "ADMIN and OWNER can modify AI settings" ON public.ai_settings FOR ALL USING (public.has_household_role(household_id, ARRAY['ADMIN', 'OWNER']::public.household_role[]));
CREATE POLICY "Members can view AI logs" ON public.ai_request_logs FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "System can insert AI logs" ON public.ai_request_logs FOR INSERT WITH CHECK (public.is_household_member(household_id));
CREATE POLICY "Members can view pending actions" ON public.ai_pending_actions FOR SELECT USING (public.is_household_member(household_id));
CREATE POLICY "MEMBER, ADMIN, OWNER can modify pending actions" ON public.ai_pending_actions FOR ALL USING (public.has_household_role(household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]));
CREATE POLICY "Users can manage their rate limit events" ON public.rate_limit_events FOR ALL USING (user_id = auth.uid());
+148
View File
@@ -0,0 +1,148 @@
-- Migration 00003: Transactional Stock Adjustment RPC (adjust_product_stock with FEFO)
CREATE OR REPLACE FUNCTION public.adjust_product_stock(
p_household_id UUID,
p_product_id UUID,
p_quantity_change NUMERIC,
p_reason public.stock_movement_reason,
p_batch_id UUID DEFAULT NULL,
p_note TEXT DEFAULT NULL
)
RETURNS NUMERIC
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_user_id UUID;
v_total_before NUMERIC := 0.0;
v_total_after NUMERIC := 0.0;
v_target_batch RECORD;
v_remaining_to_deduct NUMERIC;
v_deduct_amount NUMERIC;
BEGIN
v_user_id := auth.uid();
-- 1. Check permissions
IF NOT public.has_household_role(p_household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]) THEN
RAISE EXCEPTION 'PERMISSION_DENIED: User is not authorized to modify stock in household %', p_household_id;
END IF;
-- 2. Lock product and calculate current total stock
PERFORM 1 FROM public.products WHERE id = p_product_id AND household_id = p_household_id FOR UPDATE;
SELECT COALESCE(SUM(quantity), 0.0) INTO v_total_before
FROM public.inventory_batches
WHERE household_id = p_household_id AND product_id = p_product_id;
-- 3. Check for negative stock
IF (v_total_before + p_quantity_change) < 0 THEN
RAISE EXCEPTION 'INSUFFICIENT_STOCK: Required quantity reduction exceeds current total stock (%)', v_total_before;
END IF;
-- 4. Apply changes
IF p_batch_id IS NOT NULL THEN
-- Target specific batch
SELECT * INTO v_target_batch
FROM public.inventory_batches
WHERE id = p_batch_id AND product_id = p_product_id AND household_id = p_household_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'BATCH_NOT_FOUND: Specified batch % does not exist', p_batch_id;
END IF;
IF (v_target_batch.quantity + p_quantity_change) < 0 THEN
RAISE EXCEPTION 'INSUFFICIENT_BATCH_STOCK: Batch quantity cannot be reduced below 0';
END IF;
UPDATE public.inventory_batches
SET quantity = quantity + p_quantity_change,
updated_at = NOW()
WHERE id = p_batch_id;
ELSE
-- No explicit batch provided
IF p_quantity_change > 0 THEN
-- Increase stock: Add to default batch (batch without expiration_date or newest) or create new batch
SELECT id INTO p_batch_id
FROM public.inventory_batches
WHERE household_id = p_household_id
AND product_id = p_product_id
AND expiration_date IS NULL
ORDER BY created_at DESC
LIMIT 1;
IF p_batch_id IS NOT NULL THEN
UPDATE public.inventory_batches
SET quantity = quantity + p_quantity_change,
updated_at = NOW()
WHERE id = p_batch_id;
ELSE
INSERT INTO public.inventory_batches (household_id, product_id, quantity)
VALUES (p_household_id, p_product_id, p_quantity_change)
RETURNING id INTO p_batch_id;
END IF;
ELSIF p_quantity_change < 0 THEN
-- FEFO Deduction (First Expire, First Out)
v_remaining_to_deduct := ABS(p_quantity_change);
FOR v_target_batch IN
SELECT id, quantity
FROM public.inventory_batches
WHERE household_id = p_household_id
AND product_id = p_product_id
AND quantity > 0
ORDER BY
expiration_date ASC NULLS LAST,
created_at ASC
FOR UPDATE
LOOP
IF v_remaining_to_deduct <= 0 THEN
EXIT;
END IF;
v_deduct_amount := LEAST(v_target_batch.quantity, v_remaining_to_deduct);
UPDATE public.inventory_batches
SET quantity = quantity - v_deduct_amount,
updated_at = NOW()
WHERE id = v_target_batch.id;
v_remaining_to_deduct := v_remaining_to_deduct - v_deduct_amount;
END LOOP;
END IF;
END IF;
-- 5. Calculate new total stock
SELECT COALESCE(SUM(quantity), 0.0) INTO v_total_after
FROM public.inventory_batches
WHERE household_id = p_household_id AND product_id = p_product_id;
-- 6. Insert stock_movements record
INSERT INTO public.stock_movements (
household_id,
product_id,
batch_id,
quantity_before,
quantity_change,
quantity_after,
reason,
note,
created_by
) VALUES (
p_household_id,
p_product_id,
p_batch_id,
v_total_before,
p_quantity_change,
v_total_after,
p_reason,
p_note,
v_user_id
);
RETURN v_total_after;
END;
$$;
@@ -0,0 +1,48 @@
-- Migration 00004: Rate Limiting Function
CREATE OR REPLACE FUNCTION public.check_rate_limit(
p_action TEXT,
p_max_requests INT,
p_window_seconds INT
)
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_user_id UUID;
v_count INT;
v_window_start TIMESTAMPTZ;
BEGIN
v_user_id := auth.uid();
IF v_user_id IS NULL THEN
RAISE EXCEPTION 'UNAUTHORIZED: User must be authenticated to check rate limit';
END IF;
v_window_start := NOW() - (p_window_seconds || ' seconds')::INTERVAL;
-- Clean up old events for this user/action
DELETE FROM public.rate_limit_events
WHERE user_id = v_user_id
AND action = p_action
AND created_at < v_window_start;
-- Count requests in current window
SELECT COUNT(*) INTO v_count
FROM public.rate_limit_events
WHERE user_id = v_user_id
AND action = p_action
AND created_at >= v_window_start;
IF v_count >= p_max_requests THEN
RETURN FALSE;
END IF;
-- Record request
INSERT INTO public.rate_limit_events (user_id, action)
VALUES (v_user_id, p_action);
RETURN TRUE;
END;
$$;
+133
View File
@@ -0,0 +1,133 @@
-- Migration 00005: Seed Demo Data Function
CREATE OR REPLACE FUNCTION public.seed_household_demo_data(p_household_id UUID)
RETURNS VOID
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_user_id UUID;
v_cat_gewuerze UUID;
v_cat_kuehl UUID;
v_cat_tiefkuehl UUID;
v_cat_getraenke UUID;
v_cat_konserven UUID;
v_cat_back UUID;
v_cat_grund UUID;
v_cat_obst_gemuese UUID;
v_cat_haushalt UUID;
v_loc_kuehlschrank UUID;
v_loc_gefrierschrank UUID;
v_loc_gewuerzschrank UUID;
v_loc_vorratsschrank UUID;
v_loc_getraenkelager UUID;
v_unit_stueck UUID;
v_unit_packung UUID;
v_unit_flasche UUID;
v_unit_dose UUID;
v_unit_g UUID;
v_unit_kg UUID;
v_unit_l UUID;
BEGIN
v_user_id := auth.uid();
IF NOT public.has_household_role(p_household_id, ARRAY['MEMBER', 'ADMIN', 'OWNER']::public.household_role[]) THEN
RAISE EXCEPTION 'PERMISSION_DENIED: User is not authorized to seed household %', p_household_id;
END IF;
-- Categories
INSERT INTO public.categories (household_id, name, sort_order) VALUES
(p_household_id, 'Gewürze', 1) RETURNING id INTO v_cat_gewuerze;
INSERT INTO public.categories (household_id, name, sort_order) VALUES
(p_household_id, 'Kühlwaren', 2) RETURNING id INTO v_cat_kuehl;
INSERT INTO public.categories (household_id, name, sort_order) VALUES
(p_household_id, 'Tiefkühlprodukte', 3) RETURNING id INTO v_cat_tiefkuehl;
INSERT INTO public.categories (household_id, name, sort_order) VALUES
(p_household_id, 'Getränke', 4) RETURNING id INTO v_cat_getraenke;
INSERT INTO public.categories (household_id, name, sort_order) VALUES
(p_household_id, 'Konserven', 5) RETURNING id INTO v_cat_konserven;
INSERT INTO public.categories (household_id, name, sort_order) VALUES
(p_household_id, 'Backzutaten', 6) RETURNING id INTO v_cat_back;
INSERT INTO public.categories (household_id, name, sort_order) VALUES
(p_household_id, 'Grundnahrungsmittel', 7) RETURNING id INTO v_cat_grund;
INSERT INTO public.categories (household_id, name, sort_order) VALUES
(p_household_id, 'Obst und Gemüse', 8) RETURNING id INTO v_cat_obst_gemuese;
INSERT INTO public.categories (household_id, name, sort_order) VALUES
(p_household_id, 'Haushalt', 9) RETURNING id INTO v_cat_haushalt;
-- Locations
INSERT INTO public.locations (household_id, name, sort_order) VALUES
(p_household_id, 'Kühlschrank', 1) RETURNING id INTO v_loc_kuehlschrank;
INSERT INTO public.locations (household_id, name, sort_order) VALUES
(p_household_id, 'Gefrierschrank', 2) RETURNING id INTO v_loc_gefrierschrank;
INSERT INTO public.locations (household_id, name, sort_order) VALUES
(p_household_id, 'Gewürzschrank', 3) RETURNING id INTO v_loc_gewuerzschrank;
INSERT INTO public.locations (household_id, name, sort_order) VALUES
(p_household_id, 'Vorratsschrank', 4) RETURNING id INTO v_loc_vorratsschrank;
INSERT INTO public.locations (household_id, name, sort_order) VALUES
(p_household_id, 'Getränkelager', 5) RETURNING id INTO v_loc_getraenkelager;
-- Units
INSERT INTO public.units (household_id, name, abbreviation, unit_type, allows_decimals) VALUES
(p_household_id, 'Stück', 'Stk.', 'COUNT', false) RETURNING id INTO v_unit_stueck;
INSERT INTO public.units (household_id, name, abbreviation, unit_type, allows_decimals) VALUES
(p_household_id, 'Packung', 'Pk.', 'COUNT', false) RETURNING id INTO v_unit_packung;
INSERT INTO public.units (household_id, name, abbreviation, unit_type, allows_decimals) VALUES
(p_household_id, 'Flasche', 'Fl.', 'COUNT', false) RETURNING id INTO v_unit_flasche;
INSERT INTO public.units (household_id, name, abbreviation, unit_type, allows_decimals) VALUES
(p_household_id, 'Dose', 'Ds.', 'COUNT', false) RETURNING id INTO v_unit_dose;
INSERT INTO public.units (household_id, name, abbreviation, unit_type, allows_decimals) VALUES
(p_household_id, 'Gramm', 'g', 'WEIGHT', true) RETURNING id INTO v_unit_g;
INSERT INTO public.units (household_id, name, abbreviation, unit_type, allows_decimals) VALUES
(p_household_id, 'Kilogramm', 'kg', 'WEIGHT', true) RETURNING id INTO v_unit_kg;
INSERT INTO public.units (household_id, name, abbreviation, unit_type, allows_decimals) VALUES
(p_household_id, 'Liter', 'l', 'VOLUME', true) RETURNING id INTO v_unit_l;
-- Standard Products with Batches
-- Paprikapulver
INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by)
VALUES (p_household_id, v_cat_gewuerze, v_loc_gewuerzschrank, v_unit_dose, 'Paprikapulver', 'Ostmann', 1, 'Dose', 1, 199, true, v_user_id);
-- Salz
INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by)
VALUES (p_household_id, v_cat_gewuerze, v_loc_gewuerzschrank, v_unit_packung, 'Salz', 'Bad Reichenhaller', 500, 'g', 1, 99, false, v_user_id);
-- Pfeffer
INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by)
VALUES (p_household_id, v_cat_gewuerze, v_loc_gewuerzschrank, v_unit_dose, 'Pfeffer schwarz', 'Ostmann', 1, 'Dose', 1, 249, false, v_user_id);
-- Mehl
INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by)
VALUES (p_household_id, v_cat_back, v_loc_vorratsschrank, v_unit_kg, 'Weizenmehl Type 405', 'Diamant', 1000, 'g', 2, 129, false, v_user_id);
-- Nudeln
INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by)
VALUES (p_household_id, v_cat_grund, v_loc_vorratsschrank, v_unit_packung, 'Spaghetti', 'Barilla', 500, 'g', 2, 189, true, v_user_id);
-- Reis
INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by)
VALUES (p_household_id, v_cat_grund, v_loc_vorratsschrank, v_unit_packung, 'Basmati Reis', 'Uncle Ben''s', 1000, 'g', 1, 349, false, v_user_id);
-- Milch
INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by)
VALUES (p_household_id, v_cat_kuehl, v_loc_kuehlschrank, v_unit_l, 'Vollmilch 3.5%', 'Weihenstephan', 1, 'l', 2, 119, true, v_user_id);
-- Cola Zero
INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by)
VALUES (p_household_id, v_cat_getraenke, v_loc_getraenkelager, v_unit_flasche, 'Coca-Cola Zero', 'Coca-Cola', 1.5, 'l', 4, 129, false, v_user_id);
-- Tomaten
INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by)
VALUES (p_household_id, v_cat_obst_gemuese, v_loc_kuehlschrank, v_unit_packung, 'Strauchtomaten', 'Bio', 500, 'g', 1, 229, true, v_user_id);
-- Kartoffeln
INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by)
VALUES (p_household_id, v_cat_obst_gemuese, v_loc_vorratsschrank, v_unit_kg, 'Speisekartoffeln festkochend', 'Regional', 2.5, 'kg', 1, 299, false, v_user_id);
-- Zwiebeln
INSERT INTO public.products (household_id, category_id, default_location_id, default_unit_id, name, brand, package_size, package_unit, minimum_quantity, estimated_price_cents, favorite, created_by)
VALUES (p_household_id, v_cat_obst_gemuese, v_loc_vorratsschrank, v_unit_packung, 'Speisezwiebeln', 'Regional', 1, 'kg', 1, 149, true, v_user_id);
END;
$$;