feat: Bottle Split System (Flaschenteilung)

- Add bottle_splits and split_participants tables with RLS
- Implement soft-lock: pending requests count as reserved
- Create /splits/create wizard (3 steps: bottle, pricing, shipping)
- Create /splits/[slug] public page with price calculator
- Create /splits/manage host dashboard with participant workflow
- Add SplitProgressBar component for visual volume display
- Status workflow: PENDING -> APPROVED -> PAID -> SHIPPED
- Forum export (BBCode format)
- Saved defaults in localStorage for glass/shipping costs
This commit is contained in:
2025-12-25 22:36:38 +01:00
parent 75461d7c30
commit 0c7786db90
7 changed files with 1871 additions and 1 deletions

View File

@@ -445,3 +445,76 @@ DROP POLICY IF EXISTS "buddy_invites_redeem_policy" ON buddy_invites;
CREATE POLICY "buddy_invites_redeem_policy" ON buddy_invites
FOR SELECT USING (expires_at > now());
-- ============================================
-- Bottle Splits (Flaschenteilung)
-- ============================================
CREATE TABLE IF NOT EXISTS bottle_splits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bottle_id UUID REFERENCES bottles(id) ON DELETE CASCADE UNIQUE,
host_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
total_volume INTEGER DEFAULT 70, -- in cl
host_share INTEGER DEFAULT 10, -- what the host keeps, in cl
price_bottle DECIMAL(10, 2) NOT NULL,
cost_glass_5cl DECIMAL(10, 2) DEFAULT 0.80,
cost_glass_10cl DECIMAL(10, 2) DEFAULT 1.50,
shipping_options JSONB DEFAULT '[]'::jsonb,
is_active BOOLEAN DEFAULT true,
public_slug TEXT UNIQUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now()),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now())
);
CREATE INDEX IF NOT EXISTS idx_bottle_splits_host_id ON bottle_splits(host_id);
CREATE INDEX IF NOT EXISTS idx_bottle_splits_public_slug ON bottle_splits(public_slug);
CREATE INDEX IF NOT EXISTS idx_bottle_splits_bottle_id ON bottle_splits(bottle_id);
ALTER TABLE bottle_splits ENABLE ROW LEVEL SECURITY;
-- Host can manage their own splits
DROP POLICY IF EXISTS "bottle_splits_host_policy" ON bottle_splits;
CREATE POLICY "bottle_splits_host_policy" ON bottle_splits
FOR ALL USING ((SELECT auth.uid()) = host_id);
-- Anyone can view active splits (for public page)
DROP POLICY IF EXISTS "bottle_splits_public_view" ON bottle_splits;
CREATE POLICY "bottle_splits_public_view" ON bottle_splits
FOR SELECT USING (is_active = true);
-- Split Participants
CREATE TABLE IF NOT EXISTS split_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
split_id UUID REFERENCES bottle_splits(id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
amount_cl INTEGER NOT NULL CHECK (amount_cl IN (5, 10)),
shipping_method TEXT NOT NULL,
total_cost DECIMAL(10, 2) NOT NULL,
status TEXT DEFAULT 'PENDING' CHECK (status IN ('PENDING', 'APPROVED', 'PAID', 'SHIPPED', 'REJECTED', 'WAITLIST')),
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('Europe/Berlin'::text, now()),
UNIQUE(split_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_split_participants_split_id ON split_participants(split_id);
CREATE INDEX IF NOT EXISTS idx_split_participants_user_id ON split_participants(user_id);
CREATE INDEX IF NOT EXISTS idx_split_participants_status ON split_participants(status);
ALTER TABLE split_participants ENABLE ROW LEVEL SECURITY;
-- Users can view their own participations
DROP POLICY IF EXISTS "split_participants_own_policy" ON split_participants;
CREATE POLICY "split_participants_own_policy" ON split_participants
FOR ALL USING ((SELECT auth.uid()) = user_id);
-- Hosts can view/manage participants for their splits
DROP POLICY IF EXISTS "split_participants_host_policy" ON split_participants;
CREATE POLICY "split_participants_host_policy" ON split_participants
FOR ALL USING (
split_id IN (SELECT id FROM bottle_splits WHERE host_id = (SELECT auth.uid()))
);
-- Anyone can view participants for public splits (to show fill-level)
DROP POLICY IF EXISTS "split_participants_public_view" ON split_participants;
CREATE POLICY "split_participants_public_view" ON split_participants
FOR SELECT USING (
split_id IN (SELECT id FROM bottle_splits WHERE is_active = true)
);