feat: KitchenStock light UI, Gemma 31B AI, supermarket store filters & 130+ vegan products
This commit is contained in:
@@ -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);
|
||||
@@ -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());
|
||||
@@ -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;
|
||||
$$;
|
||||
@@ -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;
|
||||
$$;
|
||||
Reference in New Issue
Block a user