-- ============================================= -- AgendaPRO SaaS - Database Schema -- PostgreSQL 16+ -- ============================================= CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- ============================================= -- 1. PLANOS DE ASSINATURA -- ============================================= CREATE TABLE IF NOT EXISTS plans ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), name VARCHAR(100) NOT NULL, slug VARCHAR(100) UNIQUE NOT NULL, description TEXT, price NUMERIC(10,2) NOT NULL DEFAULT 0, price_monthly NUMERIC(10,2) NOT NULL DEFAULT 0, price_yearly NUMERIC(10,2) NOT NULL DEFAULT 0, max_professionals INT NOT NULL DEFAULT 1, max_services INT NOT NULL DEFAULT 5, max_appointments_month INT DEFAULT NULL, -- NULL = unlimited features JSONB DEFAULT '[]', is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================= -- 2. TENANTS (ESTABELECIMENTOS) -- ============================================= CREATE TABLE IF NOT EXISTS tenants ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), plan_id UUID REFERENCES plans(id), name VARCHAR(200) NOT NULL, slug VARCHAR(200) UNIQUE NOT NULL, business_type VARCHAR(50) NOT NULL DEFAULT 'barbearia', -- barbearia, manicure, massagem, estetica, outro logo_url TEXT, cover_url TEXT, phone VARCHAR(20), email VARCHAR(200), address TEXT, city VARCHAR(100), state VARCHAR(2), zip_code VARCHAR(10), description TEXT, settings JSONB DEFAULT '{ "timezone": "America/Sao_Paulo", "currency": "BRL", "booking_advance_min": 60, "booking_advance_max": 43200, "cancellation_limit_min": 120, "allow_online_booking": true, "require_confirmation": false, "send_reminders": true, "reminder_minutes_before": 60, "theme_color": "#6C63FF", "working_days": [1,2,3,4,5,6] }', subscription_status VARCHAR(20) DEFAULT 'trial', -- trial, active, past_due, cancelled, suspended trial_ends_at TIMESTAMPTZ, subscription_ends_at TIMESTAMPTZ, is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================= -- 3. USUÁRIOS (OWNERS, ADMINS, PROFISSIONAIS) -- ============================================= CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, name VARCHAR(200) NOT NULL, email VARCHAR(200) NOT NULL, password_hash VARCHAR(255) NOT NULL, phone VARCHAR(20), avatar_url TEXT, role VARCHAR(20) NOT NULL DEFAULT 'professional', -- owner, admin, professional is_active BOOLEAN DEFAULT true, last_login_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(email, tenant_id) ); -- ============================================= -- 4. CATEGORIAS DE SERVIÇO -- ============================================= CREATE TABLE IF NOT EXISTS service_categories ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, description TEXT, icon VARCHAR(50), sort_order INT DEFAULT 0, is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================= -- 5. SERVIÇOS -- ============================================= CREATE TABLE IF NOT EXISTS services ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, category_id UUID REFERENCES service_categories(id) ON DELETE SET NULL, name VARCHAR(200) NOT NULL, description TEXT, duration_minutes INT NOT NULL DEFAULT 30, price NUMERIC(10,2) NOT NULL DEFAULT 0, price_type VARCHAR(20) DEFAULT 'fixed', -- fixed, starting_at, per_hour image_url TEXT, is_active BOOLEAN DEFAULT true, sort_order INT DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================= -- 6. PROFISSIONAIS <-> SERVIÇOS -- ============================================= CREATE TABLE IF NOT EXISTS professional_services ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), professional_id UUID REFERENCES users(id) ON DELETE CASCADE, service_id UUID REFERENCES services(id) ON DELETE CASCADE, custom_price NUMERIC(10,2), custom_duration INT, commission_percent NUMERIC(5,2) DEFAULT 0, UNIQUE(professional_id, service_id) ); -- ============================================= -- 7. HORÁRIOS DE TRABALHO -- ============================================= CREATE TABLE IF NOT EXISTS working_hours ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, professional_id UUID REFERENCES users(id) ON DELETE CASCADE, day_of_week INT NOT NULL CHECK (day_of_week BETWEEN 0 AND 6), -- 0=domingo start_time TIME NOT NULL, end_time TIME NOT NULL, break_start TIME, break_end TIME, is_active BOOLEAN DEFAULT true, UNIQUE(professional_id, day_of_week) ); -- ============================================= -- 8. BLOQUEIOS / FOLGAS -- ============================================= CREATE TABLE IF NOT EXISTS schedule_blocks ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, professional_id UUID REFERENCES users(id) ON DELETE CASCADE, title VARCHAR(200), start_at TIMESTAMPTZ NOT NULL, end_at TIMESTAMPTZ NOT NULL, is_all_day BOOLEAN DEFAULT false, recurrence VARCHAR(20) DEFAULT 'none', -- none, daily, weekly, monthly created_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================= -- 9. CLIENTES -- ============================================= CREATE TABLE IF NOT EXISTS clients ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, name VARCHAR(200) NOT NULL, email VARCHAR(200), phone VARCHAR(20), cpf VARCHAR(14), birth_date DATE, gender VARCHAR(20), notes TEXT, avatar_url TEXT, total_visits INT DEFAULT 0, total_spent NUMERIC(10,2) DEFAULT 0, last_visit_at TIMESTAMPTZ, tags JSONB DEFAULT '[]', is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================= -- 10. AGENDAMENTOS -- ============================================= CREATE TABLE IF NOT EXISTS appointments ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, client_id UUID REFERENCES clients(id) ON DELETE SET NULL, professional_id UUID REFERENCES users(id) ON DELETE SET NULL, service_id UUID REFERENCES services(id) ON DELETE SET NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, confirmed, in_progress, completed, cancelled, no_show date DATE NOT NULL, start_time TIME NOT NULL, end_time TIME NOT NULL, price NUMERIC(10,2) NOT NULL DEFAULT 0, discount NUMERIC(10,2) DEFAULT 0, notes TEXT, client_name VARCHAR(200), -- for walk-ins client_phone VARCHAR(20), client_email VARCHAR(200), source VARCHAR(20) DEFAULT 'manual', -- manual, online, whatsapp cancelled_at TIMESTAMPTZ, cancellation_reason TEXT, completed_at TIMESTAMPTZ, reminder_sent BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================= -- 11. PAGAMENTOS -- ============================================= CREATE TABLE IF NOT EXISTS payments ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, appointment_id UUID REFERENCES appointments(id) ON DELETE SET NULL, client_id UUID REFERENCES clients(id) ON DELETE SET NULL, amount NUMERIC(10,2) NOT NULL, method VARCHAR(30) NOT NULL DEFAULT 'cash', -- cash, credit, debit, pix, transfer status VARCHAR(20) DEFAULT 'completed', -- pending, completed, refunded notes TEXT, paid_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================= -- 12. ASSINATURAS SaaS -- ============================================= CREATE TABLE IF NOT EXISTS subscriptions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, plan_id UUID REFERENCES plans(id), status VARCHAR(20) DEFAULT 'active', -- active, past_due, cancelled, suspended billing_cycle VARCHAR(10) DEFAULT 'monthly', -- monthly, yearly current_period_start TIMESTAMPTZ, current_period_end TIMESTAMPTZ, payment_method VARCHAR(30), payment_gateway VARCHAR(30), -- stripe, asaas, manual gateway_subscription_id VARCHAR(200), amount NUMERIC(10,2), last_payment_at TIMESTAMPTZ, next_payment_at TIMESTAMPTZ, cancelled_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================= -- 13. HISTÓRICO DE PAGAMENTOS SaaS -- ============================================= CREATE TABLE IF NOT EXISTS subscription_payments ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), subscription_id UUID REFERENCES subscriptions(id) ON DELETE CASCADE, tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, amount NUMERIC(10,2) NOT NULL, status VARCHAR(20) DEFAULT 'paid', -- paid, failed, refunded gateway_payment_id VARCHAR(200), paid_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================= -- 14. NOTIFICAÇÕES -- ============================================= CREATE TABLE IF NOT EXISTS notifications ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id) ON DELETE CASCADE, type VARCHAR(50) NOT NULL, title VARCHAR(200) NOT NULL, message TEXT, data JSONB DEFAULT '{}', is_read BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================= -- INDEXES -- ============================================= CREATE INDEX IF NOT EXISTS idx_appointments_tenant_date ON appointments(tenant_id, date); CREATE INDEX IF NOT EXISTS idx_appointments_professional ON appointments(professional_id, date); CREATE INDEX IF NOT EXISTS idx_appointments_client ON appointments(client_id); CREATE INDEX IF NOT EXISTS idx_appointments_status ON appointments(status); CREATE INDEX IF NOT EXISTS idx_clients_tenant ON clients(tenant_id); CREATE INDEX IF NOT EXISTS idx_clients_phone ON clients(phone); CREATE INDEX IF NOT EXISTS idx_services_tenant ON services(tenant_id); CREATE INDEX IF NOT EXISTS idx_payments_tenant ON payments(tenant_id); CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id, is_read); CREATE INDEX IF NOT EXISTS idx_users_tenant ON users(tenant_id); CREATE INDEX IF NOT EXISTS idx_tenants_slug ON tenants(slug); -- ============================================= -- SEED: DEFAULT PLANS -- ============================================= INSERT INTO plans (name, slug, description, price_monthly, price_yearly, max_professionals, max_services, max_appointments_month, features) VALUES ('Starter', 'starter', 'Ideal para profissionais autônomos', 49.90, 479.00, 1, 10, 200, '["Agendamento online", "Página de reservas", "Lembretes por e-mail", "Relatórios básicos"]'), ('Professional', 'professional', 'Para pequenos estabelecimentos', 99.90, 959.00, 5, 30, NULL, '["Tudo do Starter", "Múltiplos profissionais", "Comissionamento", "Relatórios avançados", "Personalização da marca"]'), ('Business', 'business', 'Para estabelecimentos em crescimento', 199.90, 1919.00, 15, 100, NULL, '["Tudo do Professional", "API de integração", "Suporte prioritário", "Multi-unidades", "Automações avançadas"]'), ('Enterprise', 'enterprise', 'Solução completa e personalizada', 399.90, 3839.00, 999, 999, NULL, '["Tudo do Business", "Gerente de conta dedicado", "SLA garantido", "Integrações customizadas", "White-label"]') ON CONFLICT (slug) DO NOTHING; -- ============================================= -- SEED: DEMO TENANT -- ============================================= DO $$ DECLARE v_plan_id UUID; v_tenant_id UUID; v_owner_id UUID; v_cat_barba_id UUID; v_cat_cabelo_id UUID; v_cat_combo_id UUID; v_svc1_id UUID; v_svc2_id UUID; v_svc3_id UUID; v_svc4_id UUID; v_svc5_id UUID; v_prof1_id UUID; v_prof2_id UUID; BEGIN SELECT id INTO v_plan_id FROM plans WHERE slug = 'professional'; INSERT INTO tenants (id, plan_id, name, slug, business_type, phone, email, description, subscription_status, trial_ends_at) VALUES ( uuid_generate_v4(), v_plan_id, 'Barbearia Premium', 'barbearia-premium', 'barbearia', '(11) 99999-1234', 'contato@barbeariapremium.com.br', 'A melhor barbearia da cidade. Cortes modernos, barbas impecáveis e ambiente premium.', 'active', NOW() + INTERVAL '30 days' ) ON CONFLICT (slug) DO NOTHING RETURNING id INTO v_tenant_id; IF v_tenant_id IS NOT NULL THEN -- Owner INSERT INTO users (id, tenant_id, name, email, password_hash, role) VALUES (uuid_generate_v4(), v_tenant_id, 'Admin Demo', 'admin@demo.com', crypt('admin123', gen_salt('bf')), 'owner') RETURNING id INTO v_owner_id; -- Professionals INSERT INTO users (id, tenant_id, name, email, password_hash, role, phone) VALUES (uuid_generate_v4(), v_tenant_id, 'Carlos Silva', 'carlos@demo.com', crypt('prof123', gen_salt('bf')), 'professional', '(11) 98888-1111') RETURNING id INTO v_prof1_id; INSERT INTO users (id, tenant_id, name, email, password_hash, role, phone) VALUES (uuid_generate_v4(), v_tenant_id, 'Rafael Santos', 'rafael@demo.com', crypt('prof123', gen_salt('bf')), 'professional', '(11) 98888-2222') RETURNING id INTO v_prof2_id; -- Categories INSERT INTO service_categories (id, tenant_id, name, icon, sort_order) VALUES (uuid_generate_v4(), v_tenant_id, 'Cabelo', 'scissors', 1) RETURNING id INTO v_cat_cabelo_id; INSERT INTO service_categories (id, tenant_id, name, icon, sort_order) VALUES (uuid_generate_v4(), v_tenant_id, 'Barba', 'sparkles', 2) RETURNING id INTO v_cat_barba_id; INSERT INTO service_categories (id, tenant_id, name, icon, sort_order) VALUES (uuid_generate_v4(), v_tenant_id, 'Combos', 'star', 3) RETURNING id INTO v_cat_combo_id; -- Services INSERT INTO services (id, tenant_id, category_id, name, description, duration_minutes, price, sort_order) VALUES (uuid_generate_v4(), v_tenant_id, v_cat_cabelo_id, 'Corte Masculino', 'Corte moderno com lavagem e finalização', 40, 55.00, 1) RETURNING id INTO v_svc1_id; INSERT INTO services (id, tenant_id, category_id, name, description, duration_minutes, price, sort_order) VALUES (uuid_generate_v4(), v_tenant_id, v_cat_cabelo_id, 'Corte Infantil', 'Corte para crianças até 12 anos', 30, 40.00, 2) RETURNING id INTO v_svc2_id; INSERT INTO services (id, tenant_id, category_id, name, description, duration_minutes, price, sort_order) VALUES (uuid_generate_v4(), v_tenant_id, v_cat_barba_id, 'Barba Completa', 'Aparar, modelar e hidratação', 30, 40.00, 3) RETURNING id INTO v_svc3_id; INSERT INTO services (id, tenant_id, category_id, name, description, duration_minutes, price, sort_order) VALUES (uuid_generate_v4(), v_tenant_id, v_cat_barba_id, 'Design de Sobrancelha', 'Alinhamento com navalha', 15, 25.00, 4) RETURNING id INTO v_svc4_id; INSERT INTO services (id, tenant_id, category_id, name, description, duration_minutes, price, sort_order) VALUES (uuid_generate_v4(), v_tenant_id, v_cat_combo_id, 'Combo Corte + Barba', 'Corte masculino + barba completa', 60, 85.00, 5) RETURNING id INTO v_svc5_id; -- Professional <-> Services INSERT INTO professional_services (professional_id, service_id, commission_percent) VALUES (v_prof1_id, v_svc1_id, 40), (v_prof1_id, v_svc2_id, 40), (v_prof1_id, v_svc3_id, 40), (v_prof1_id, v_svc4_id, 40), (v_prof1_id, v_svc5_id, 40), (v_prof2_id, v_svc1_id, 35), (v_prof2_id, v_svc3_id, 35), (v_prof2_id, v_svc5_id, 35); -- Working Hours (Mon-Sat for both professionals) FOR i IN 1..6 LOOP INSERT INTO working_hours (tenant_id, professional_id, day_of_week, start_time, end_time, break_start, break_end) VALUES (v_tenant_id, v_prof1_id, i, '09:00', '19:00', '12:00', '13:00'), (v_tenant_id, v_prof2_id, i, '10:00', '20:00', '13:00', '14:00'); END LOOP; -- Sample Clients INSERT INTO clients (tenant_id, name, email, phone, total_visits) VALUES (v_tenant_id, 'João Mendes', 'joao@email.com', '(11) 97777-1111', 12), (v_tenant_id, 'Pedro Oliveira', 'pedro@email.com', '(11) 97777-2222', 8), (v_tenant_id, 'Lucas Ferreira', 'lucas@email.com', '(11) 97777-3333', 5), (v_tenant_id, 'Mateus Costa', 'mateus@email.com', '(11) 97777-4444', 3), (v_tenant_id, 'Gabriel Souza', 'gabriel@email.com', '(11) 97777-5555', 15); -- Sample appointments for today INSERT INTO appointments (tenant_id, client_id, professional_id, service_id, status, date, start_time, end_time, price, source) SELECT v_tenant_id, c.id, v_prof1_id, v_svc1_id, 'confirmed', CURRENT_DATE, '10:00', '10:40', 55.00, 'online' FROM clients c WHERE c.name = 'João Mendes' AND c.tenant_id = v_tenant_id LIMIT 1; INSERT INTO appointments (tenant_id, client_id, professional_id, service_id, status, date, start_time, end_time, price, source) SELECT v_tenant_id, c.id, v_prof1_id, v_svc5_id, 'confirmed', CURRENT_DATE, '11:00', '12:00', 85.00, 'manual' FROM clients c WHERE c.name = 'Pedro Oliveira' AND c.tenant_id = v_tenant_id LIMIT 1; INSERT INTO appointments (tenant_id, client_id, professional_id, service_id, status, date, start_time, end_time, price, source) SELECT v_tenant_id, c.id, v_prof2_id, v_svc3_id, 'pending', CURRENT_DATE, '14:00', '14:30', 40.00, 'online' FROM clients c WHERE c.name = 'Lucas Ferreira' AND c.tenant_id = v_tenant_id LIMIT 1; INSERT INTO appointments (tenant_id, client_id, professional_id, service_id, status, date, start_time, end_time, price, source) SELECT v_tenant_id, c.id, v_prof2_id, v_svc1_id, 'completed', CURRENT_DATE, '10:00', '10:40', 55.00, 'manual' FROM clients c WHERE c.name = 'Gabriel Souza' AND c.tenant_id = v_tenant_id LIMIT 1; -- Subscription record INSERT INTO subscriptions (tenant_id, plan_id, status, billing_cycle, current_period_start, current_period_end, amount, next_payment_at) VALUES (v_tenant_id, v_plan_id, 'active', 'monthly', NOW(), NOW() + INTERVAL '30 days', 99.90, NOW() + INTERVAL '30 days'); END IF; END $$;