-- ===================================================== -- E-commerce Database Schema for Supabase -- Astro 6 + React + PaySo Multi-vendor Marketplace -- ===================================================== -- Enable UUID extension CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- ===================================================== -- USERS & AUTHENTICATION -- ===================================================== CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, name TEXT, phone TEXT, role TEXT DEFAULT 'customer' CHECK (role IN ('customer', 'vendor', 'admin')), avatar_url TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ===================================================== -- VENDOR PROFILES (Multi-vendor support) -- ===================================================== CREATE TABLE vendor_profiles ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE, store_name TEXT NOT NULL, store_slug TEXT UNIQUE NOT NULL, store_description TEXT, store_logo TEXT, bank_account TEXT, bank_name TEXT, payout_status TEXT DEFAULT 'pending' CHECK (payout_status IN ('pending', 'approved', 'rejected')), total_earnings DECIMAL(12,2) DEFAULT 0, approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ===================================================== -- CATEGORIES (Hierarchical) -- ===================================================== CREATE TABLE categories ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), name TEXT NOT NULL, slug TEXT UNIQUE NOT NULL, description TEXT, image_url TEXT, parent_id UUID REFERENCES categories(id), sort_order INT DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ===================================================== -- PRODUCTS -- ===================================================== CREATE TABLE products ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), vendor_id UUID REFERENCES vendor_profiles(id) ON DELETE CASCADE, category_id UUID REFERENCES categories(id), name TEXT NOT NULL, slug TEXT UNIQUE NOT NULL, description TEXT, price DECIMAL(12,2) NOT NULL, compare_at_price DECIMAL(12,2), cost_price DECIMAL(12,2), sku TEXT UNIQUE, barcode TEXT, inventory INT DEFAULT 0, low_stock_threshold INT DEFAULT 5, track_inventory BOOLEAN DEFAULT TRUE, allow_backorder BOOLEAN DEFAULT FALSE, weight DECIMAL(8,2), images JSONB DEFAULT '[]', metadata JSONB DEFAULT '{}', status TEXT DEFAULT 'draft' CHECK (status IN ('draft', 'active', 'archived')), featured BOOLEAN DEFAULT FALSE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ===================================================== -- PRODUCT VARIANTS -- ===================================================== CREATE TABLE product_variants ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), product_id UUID REFERENCES products(id) ON DELETE CASCADE, name TEXT NOT NULL, sku TEXT UNIQUE, price DECIMAL(12,2), inventory INT DEFAULT 0, attributes JSONB DEFAULT '{}', image_url TEXT, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ===================================================== -- REVIEWS -- ===================================================== CREATE TABLE reviews ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), product_id UUID REFERENCES products(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id), order_id UUID, rating INT CHECK (rating >= 1 AND rating <= 5), title TEXT, comment TEXT, images JSONB DEFAULT '[]', verified_purchase BOOLEAN DEFAULT FALSE, status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')), created_at TIMESTAMPTZ DEFAULT NOW() ); -- ===================================================== -- ORDERS -- ===================================================== CREATE TABLE orders ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), order_number TEXT UNIQUE NOT NULL, user_id UUID REFERENCES users(id), vendor_id UUID REFERENCES vendor_profiles(id), status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded')), payment_status TEXT DEFAULT 'unpaid' CHECK (payment_status IN ('unpaid', 'paid', 'failed', 'refunded')), subtotal DECIMAL(12,2) NOT NULL, tax DECIMAL(12,2) DEFAULT 0, shipping_cost DECIMAL(12,2) DEFAULT 0, total DECIMAL(12,2) NOT NULL, currency TEXT DEFAULT 'THB', payment_method TEXT, payment_provider TEXT, payment_ref TEXT, shipping_name TEXT, shipping_phone TEXT, shipping_address TEXT, shipping_city TEXT, shipping_postal TEXT, shipping_country TEXT DEFAULT 'Thailand', notes TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ===================================================== -- ORDER ITEMS -- ===================================================== CREATE TABLE order_items ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), order_id UUID REFERENCES orders(id) ON DELETE CASCADE, product_id UUID REFERENCES products(id), variant_id UUID REFERENCES product_variants(id), vendor_id UUID REFERENCES vendor_profiles(id), quantity INT NOT NULL, unit_price DECIMAL(12,2) NOT NULL, total_price DECIMAL(12,2) NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ===================================================== -- PAYMENTS -- ===================================================== CREATE TABLE payments ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), order_id UUID REFERENCES orders(id), user_id UUID REFERENCES users(id), provider TEXT NOT NULL, provider_ref TEXT, amount DECIMAL(12,2) NOT NULL, currency TEXT DEFAULT 'THB', status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'completed', 'failed', 'refunded')), payment_data JSONB, paid_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ===================================================== -- VENDOR PAYOUTS -- ===================================================== CREATE TABLE vendor_payouts ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), vendor_id UUID REFERENCES vendor_profiles(id), amount DECIMAL(12,2) NOT NULL, status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')), bank_account TEXT, bank_name TEXT, notes TEXT, processed_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ===================================================== -- SHOPPING CARTS -- ===================================================== CREATE TABLE carts ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE cart_items ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), cart_id UUID REFERENCES carts(id) ON DELETE CASCADE, product_id UUID REFERENCES products(id), variant_id UUID REFERENCES product_variants(id), quantity INT DEFAULT 1, created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(cart_id, product_id, variant_id) ); -- ===================================================== -- INDEXES FOR PERFORMANCE -- ===================================================== -- Products CREATE INDEX idx_products_vendor ON products(vendor_id); CREATE INDEX idx_products_category ON products(category_id); CREATE INDEX idx_products_slug ON products(slug); CREATE INDEX idx_products_status ON products(status); CREATE INDEX idx_products_featured ON products(featured) WHERE featured = TRUE; CREATE INDEX idx_products_inventory ON products(inventory); -- Orders CREATE INDEX idx_orders_user ON orders(user_id); CREATE INDEX idx_orders_vendor ON orders(vendor_id); CREATE INDEX idx_orders_number ON orders(order_number); CREATE INDEX idx_orders_status ON orders(status); CREATE INDEX idx_orders_payment_status ON orders(payment_status); CREATE INDEX idx_orders_created ON orders(created_at DESC); -- Order Items CREATE INDEX idx_order_items_order ON order_items(order_id); CREATE INDEX idx_order_items_product ON order_items(product_id); CREATE INDEX idx_order_items_vendor ON order_items(vendor_id); -- Reviews CREATE INDEX idx_reviews_product ON reviews(product_id); CREATE INDEX idx_reviews_user ON reviews(user_id); CREATE INDEX idx_reviews_product_rating ON reviews(product_id, rating); -- Cart CREATE INDEX idx_cart_items_cart ON cart_items(cart_id); -- Payments CREATE INDEX idx_payments_order ON payments(order_id); CREATE INDEX idx_payments_status ON payments(status); -- Vendor CREATE INDEX idx_vendor_profiles_user ON vendor_profiles(user_id); CREATE INDEX idx_vendor_profiles_slug ON vendor_profiles(store_slug); -- Categories CREATE INDEX idx_categories_parent ON categories(parent_id); -- ===================================================== -- ROW LEVEL SECURITY (RLS) -- ===================================================== -- Enable RLS on all tables ALTER TABLE users ENABLE ROW LEVEL SECURITY; ALTER TABLE vendor_profiles ENABLE ROW LEVEL SECURITY; ALTER TABLE categories ENABLE ROW LEVEL SECURITY; ALTER TABLE products ENABLE ROW LEVEL SECURITY; ALTER TABLE product_variants ENABLE ROW LEVEL SECURITY; ALTER TABLE reviews ENABLE ROW LEVEL SECURITY; ALTER TABLE orders ENABLE ROW LEVEL SECURITY; ALTER TABLE order_items ENABLE ROW LEVEL SECURITY; ALTER TABLE payments ENABLE ROW LEVEL SECURITY; ALTER TABLE vendor_payouts ENABLE ROW LEVEL SECURITY; ALTER TABLE carts ENABLE ROW LEVEL SECURITY; ALTER TABLE cart_items ENABLE ROW LEVEL SECURITY; -- Users: Users can read their own profile CREATE POLICY "Users can view own profile" ON users FOR SELECT USING (auth.uid() = id); CREATE POLICY "Users can update own profile" ON users FOR UPDATE USING (auth.uid() = id); -- Products: Anyone can view active products CREATE POLICY "Anyone can view active products" ON products FOR SELECT USING (status = 'active'); CREATE POLICY "Vendors can manage own products" ON products FOR ALL USING ( vendor_id IN ( SELECT id FROM vendor_profiles WHERE user_id = auth.uid() ) ); -- Categories: Anyone can view categories CREATE POLICY "Anyone can view categories" ON categories FOR SELECT USING (true); -- Reviews: Anyone can view approved reviews CREATE POLICY "Anyone can view approved reviews" ON reviews FOR SELECT USING (status = 'approved'); -- Orders: Users can view their own orders CREATE POLICY "Users can view own orders" ON orders FOR SELECT USING (auth.uid() = user_id); CREATE POLICY "Vendors can view their orders" ON orders FOR SELECT USING ( vendor_id IN ( SELECT id FROM vendor_profiles WHERE user_id = auth.uid() ) ); -- Carts: Users can manage their own cart CREATE POLICY "Users can manage own cart" ON carts FOR ALL USING (user_id = auth.uid()); CREATE POLICY "Users can manage own cart items" ON cart_items FOR ALL USING ( cart_id IN (SELECT id FROM carts WHERE user_id = auth.uid()) ); -- ===================================================== -- FUNCTIONS & TRIGGERS -- ===================================================== -- Auto-update updated_at timestamp CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ language 'plpgsql'; -- Apply to tables with updated_at CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_products_updated_at BEFORE UPDATE ON products FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_orders_updated_at BEFORE UPDATE ON orders FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Function to generate order number CREATE OR REPLACE FUNCTION generate_order_number() RETURNS TRIGGER AS $$ BEGIN NEW.order_number = 'ORD-' || TO_CHAR(NOW(), 'YYYYMMDD') || '-' || UPPER(SUBSTRING(NEW.id::text, 1, 8)); RETURN NEW; END; $$ language 'plpgsql'; CREATE TRIGGER generate_order_number_trigger BEFORE INSERT ON orders FOR EACH ROW EXECUTE FUNCTION generate_order_number(); -- Function to update product inventory on order CREATE OR REPLACE FUNCTION update_inventory_on_order() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'INSERT' THEN UPDATE products SET inventory = inventory - NEW.quantity WHERE id = NEW.product_id AND track_inventory = TRUE; ELSIF TG_OP = 'DELETE' THEN UPDATE products SET inventory = inventory + OLD.quantity WHERE id = OLD.product_id AND track_inventory = TRUE; END IF; RETURN NULL; END; $$ language 'plpgsql'; CREATE TRIGGER update_inventory_trigger AFTER INSERT OR DELETE ON order_items FOR EACH ROW EXECUTE FUNCTION update_inventory_on_order(); -- Function to update vendor earnings on payment CREATE OR REPLACE FUNCTION update_vendor_earnings() RETURNS TRIGGER AS $$ BEGIN IF NEW.status = 'completed' THEN UPDATE vendor_profiles SET total_earnings = total_earnings + NEW.amount WHERE vendor_id IN ( SELECT vendor_id FROM orders WHERE id = NEW.order_id ); END IF; RETURN NEW; END; $$ language 'plpgsql'; CREATE TRIGGER update_vendor_earnings_trigger AFTER UPDATE OF status ON payments FOR EACH ROW EXECUTE FUNCTION update_vendor_earnings(); -- ===================================================== -- SEED DATA (Optional sample categories) -- ===================================================== -- Uncomment to add sample categories /* INSERT INTO categories (name, slug, description, sort_order) VALUES ('Electronics', 'electronics', 'Electronic devices and accessories', 1), ('Clothing', 'clothing', 'Fashion and apparel', 2), ('Home & Garden', 'home-garden', 'Home improvement and garden', 3), ('Sports', 'sports', 'Sports and outdoor equipment', 4), ('Books', 'books', 'Books and media', 5); */