From 18071cfb639c3840617e06cf52ec5299a888e88f Mon Sep 17 00:00:00 2001 From: Marco Gallegos Date: Fri, 16 Jan 2026 00:01:32 -0600 Subject: [PATCH] refactor: migrate from old db/ to supabase CLI structure - Migrated database schema from db/migrations to supabase/migrations - Added Supabase CLI configuration (config.toml, .gitignore) - Added kiosk role and amenities tables for touch kiosks - Added notification system for artist alerts - Added seed data with test locations and staff - Removed old migration scripts and documentation - Updated tasks_mg.md with current setup Features: - 2 locations: ANCHOR:23 - Via KLAVA and TEST - Kiosk role for touch screen check-in/booking - Amenities: coffee, cocktails, mocktails for clients - Notifications when client arrives - 1 staff + 4 artists + 1 kiosk per location - Services: hair, nails, makeup, lashes --- AGENTS.md | 118 --- DASHBOARD_ONLY_GUIDE.md | 336 -------- FASE_1_STATUS.md | 376 --------- SIMPLE_GUIDE.md | 229 ----- db/migrate.sh | 114 --- db/migrations/001_initial_schema.sql | 279 ------- db/migrations/002_rls_policies.sql | 335 -------- db/migrations/003_audit_triggers.sql | 309 ------- db/migrations/00_FULL_MIGRATION.sql | 780 ------------------ db/migrations/README.md | 127 --- db/migrations/full_migration.sql | 114 --- docs/00_FULL_MIGRATION_FINAL_README.md | 148 ---- docs/MIGRATION_CORRECTION.md | 314 ------- docs/MIGRATION_GUIDE.md | 267 ------ docs/POST_MIGRATION_SUCCESS.md | 370 --------- docs/QUICK_START_POST_MIGRATION.md | 375 --------- docs/STEP_BY_STEP_AUTH_CONFIG.md | 610 -------------- docs/STEP_BY_STEP_VERIFICATION.md | 734 ---------------- docs/SUPABASE_DASHBOARD_MIGRATION.md | 322 -------- scripts/README.md | 342 -------- scripts/check-connection.sh | 157 ---- scripts/create-auth-users.js | 330 -------- scripts/seed-data.js | 439 ---------- scripts/seed-data.sql | 189 ----- scripts/simple-seed.sh | 276 ------- scripts/simple-verify.sh | 124 --- scripts/verify-migration.js | 225 ----- scripts/verify-migration.sql | 89 -- supabase/.gitignore | 8 + supabase/config.toml | 384 +++++++++ .../20260115235737_initial_schema.sql | 165 ++-- .../20260115235800_add_kiosk_tables.sql | 175 ++-- .../migrations/20260115235900_seed_data.sql | 393 +++++++++ tasks_mg.md | 241 ++++-- 34 files changed, 1168 insertions(+), 8626 deletions(-) delete mode 100644 AGENTS.md delete mode 100644 DASHBOARD_ONLY_GUIDE.md delete mode 100644 FASE_1_STATUS.md delete mode 100644 SIMPLE_GUIDE.md delete mode 100755 db/migrate.sh delete mode 100644 db/migrations/001_initial_schema.sql delete mode 100644 db/migrations/002_rls_policies.sql delete mode 100644 db/migrations/003_audit_triggers.sql delete mode 100644 db/migrations/00_FULL_MIGRATION.sql delete mode 100644 db/migrations/README.md delete mode 100644 db/migrations/full_migration.sql delete mode 100644 docs/00_FULL_MIGRATION_FINAL_README.md delete mode 100644 docs/MIGRATION_CORRECTION.md delete mode 100644 docs/MIGRATION_GUIDE.md delete mode 100644 docs/POST_MIGRATION_SUCCESS.md delete mode 100644 docs/QUICK_START_POST_MIGRATION.md delete mode 100644 docs/STEP_BY_STEP_AUTH_CONFIG.md delete mode 100644 docs/STEP_BY_STEP_VERIFICATION.md delete mode 100644 docs/SUPABASE_DASHBOARD_MIGRATION.md delete mode 100644 scripts/README.md delete mode 100755 scripts/check-connection.sh delete mode 100644 scripts/create-auth-users.js delete mode 100644 scripts/seed-data.js delete mode 100644 scripts/seed-data.sql delete mode 100755 scripts/simple-seed.sh delete mode 100755 scripts/simple-verify.sh delete mode 100644 scripts/verify-migration.js delete mode 100644 scripts/verify-migration.sql create mode 100644 supabase/.gitignore create mode 100644 supabase/config.toml rename db/migrations/00_FULL_MIGRATION_CORRECTED.sql => supabase/migrations/20260115235737_initial_schema.sql (77%) rename db/migrations/00_FULL_MIGRATION_FINAL.sql => supabase/migrations/20260115235800_add_kiosk_tables.sql (75%) create mode 100644 supabase/migrations/20260115235900_seed_data.sql diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index c91857d..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,118 +0,0 @@ -# AGENTS.md — Roles de IA y Responsabilidades - -Este documento define cómo deben usarse agentes de IA (Claude, Codex, OpenCode, Gemini) dentro del proyecto SalonOS. - -Ningún agente tiene autoridad de producto. Todos ejecutan estrictamente bajo el PRD. - ---- - -## Principios de Uso de Agentes - -- Los agentes no deciden alcance. -- Los agentes no redefinen reglas de negocio. -- Los agentes no introducen lógica no descrita en el PRD. -- Toda salida debe ser revisable, versionable y auditable. -- El PRD es la única fuente de verdad funcional. - ---- - -## Claude — Arquitectura y Lógica - -**Rol:** Arquitecto de sistema y reglas de negocio. - -**Responsabilidades explícitas alineadas al PRD:** -- Definir la lógica de reseteo semanal de invitaciones (Lunes 00:00 UTC, idempotente, auditable). -- Especificar manejo UTC-first y puntos válidos de conversión de zona horaria. -- Diseñar el algoritmo de generación de Short ID con reintentos por colisión. -- Modelar estados, transiciones y edge cases críticos. - -**Usar para:** -- Descomposición de lógica compleja. -- Validación de consistencia con el PRD. -- Diseño de flujos y contratos lógicos. - -**No usar para:** -- Código final sin revisión humana. -- Decisiones visuales o de UX. - ---- - -## Codex — Implementación Backend - -**Rol:** Ingeniero de backend. - -**Responsabilidades explícitas alineadas al PRD:** -- Implementar el reseteo semanal de invitaciones mediante: - - Cron Job o - - Supabase Edge Function. -- Garantizar que todos los timestamps persistidos estén en UTC. -- Implementar generación de Short ID (6 caracteres) con verificación de unicidad y reintentos. -- Registrar todos los automatismos y eventos críticos en `audit_logs`. - -**Usar para:** -- SQL, migraciones y esquemas. -- Funciones server-side. -- Webhooks (Stripe, WhatsApp). -- Integraciones API. - -**Reglas:** -- Todo código debe respetar RLS. -- No hardcodear secretos. -- No persistir horas locales bajo ninguna circunstancia. - ---- - -## OpenCode — Frontend e Integración - -**Rol:** Ingeniero de interfaz y pegamento. - -**Responsabilidades explícitas alineadas al PRD:** -- Convertir timestamps desde UTC a la zona horaria definida en `locations.timezone`. -- Nunca enviar ni persistir horas locales al backend. -- Exponer Short ID únicamente como referencia humana, nunca como identificador primario. - -**Usar para:** -- Componentes Next.js. -- Integración frontend ↔ backend. -- Manejo de estado y formularios. -- Flujos de agenda y visualización. - -**Reglas:** -- No exponer datos privados. -- Validaciones críticas siempre en backend. - ---- - -## Gemini — QA y Seguridad - -**Rol:** Auditor técnico. - -**Responsabilidades explícitas alineadas al PRD:** -- Verificar que ningún timestamp no-UTC sea almacenado. -- Auditar la idempotencia del reseteo semanal de invitaciones. -- Verificar que los roles Artist NO puedan acceder a email/phone de customers. -- Detectar riesgos de colisión, enumeración o fuga de Short IDs. -- Revisar cumplimiento de RLS y límites de acceso. - -**Usar para:** -- Revisión de RLS. -- Detección de fugas de datos. -- Edge cases de seguridad. -- Validación de flujos críticos. - ---- - -## Flujo de Trabajo Canónico - -1. El PRD define la regla. -2. La lógica es descompuesta y formalizada. -3. El backend implementa la regla. -4. La interfaz conecta y presenta. -5. Se audita y valida el cumplimiento técnico. - - ---- - -## Regla de Oro - -Si un agente contradice el PRD, el agente está equivocado. diff --git a/DASHBOARD_ONLY_GUIDE.md b/DASHBOARD_ONLY_GUIDE.md deleted file mode 100644 index 5161927..0000000 --- a/DASHBOARD_ONLY_GUIDE.md +++ /dev/null @@ -1,336 +0,0 @@ -# 🚨 PUERTO 5432 BLOQUEADO - SOLUCIÓN SIMPLE - -## ✅ SOLUCIÓN: USAR SUPABASE DASHBOARD - -No necesitas scripts ni línea de comandos. Solo usa el navegador. - ---- - -## 📋 PASO 1: EJECUTAR MIGRACIONES - -### 1.1 Abrir Supabase SQL Editor - -``` -https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/sql -``` - -### 1.2 Copiar Migración Completa - -Copia el contenido de este archivo: -``` -db/migrations/00_FULL_MIGRATION_FINAL.sql -``` - -### 1.3 Ejecutar - -1. Pega el contenido en el SQL Editor -2. Haz clic en **"Run"** (botón azul arriba a la derecha) -3. Espera 10-30 segundos - -### 1.4 Verificar - -Al finalizar deberías ver: -``` -=========================================== -SALONOS - DATABASE MIGRATION COMPLETED -=========================================== -✅ Tables created: 8 -✅ Functions created: 14 -✅ Triggers active: 17+ -✅ RLS policies configured: 20+ -✅ ENUM types created: 6 -=========================================== -``` - ---- - -## 📋 PASO 2: CREAR DATOS DE PRUEBA - -### 2.1 Crear Locations - -Copia esto en el SQL Editor y ejecuta: - -```sql -INSERT INTO locations (name, timezone, address, phone, is_active) -VALUES - ('Salón Principal - Centro', 'America/Mexico_City', 'Av. Reforma 222', '+52 55 1234 5678', true), - ('Salón Norte - Polanco', 'America/Mexico_City', 'Av. Masaryk 123', '+52 55 2345 6789', true), - ('Salón Sur - Coyoacán', 'America/Mexico_City', 'Calle Hidalgo 456', '+52 55 3456 7890', true); -``` - -### 2.2 Crear Resources - -```sql -INSERT INTO resources (location_id, name, type, capacity, is_active) -SELECT - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - 'Estación ' || generate_series(1,3)::TEXT, - 'station', - 1, - true -UNION ALL -SELECT - (SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), - 'Estación ' || generate_series(1,2)::TEXT, - 'station', - 1, - true -UNION ALL -SELECT - (SELECT id FROM locations WHERE name = 'Salón Sur - Coyoacán' LIMIT 1), - 'Estación 1', - 'station', - 1, - true; -``` - -### 2.3 Crear Staff - -```sql -INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) -VALUES - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'admin', 'Admin Principal', '+52 55 1111 2222', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'manager', 'Manager Centro', '+52 55 2222 3333', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), 'manager', 'Manager Polanco', '+52 55 6666 7777', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'staff', 'Staff Coordinadora', '+52 55 3333 4444', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'artist', 'Artist María García', '+52 55 4444 5555', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'artist', 'Artist Ana Rodríguez', '+52 55 5555 6666', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), 'artist', 'Artist Carla López', '+52 55 7777 8888', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Sur - Coyoacán' LIMIT 1), 'artist', 'Artist Laura Martínez', '+52 55 8888 9999', true); -``` - -### 2.4 Crear Services - -```sql -INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) -VALUES - ('Corte y Estilizado', 'Corte de cabello profesional', 60, 500.00, false, false, true), - ('Color Completo', 'Tinte completo con protección capilar', 120, 1200.00, false, true, true), - ('Balayage Premium', 'Técnica de balayage premium', 180, 2000.00, true, true, true), - ('Tratamiento Kératina', 'Tratamiento para cabello dañado', 90, 1500.00, false, false, true), - ('Peinado Evento', 'Peinado para eventos', 45, 800.00, false, true, true), - ('Servicio Express (Dual Artist)', 'Servicio rápido con dos artists', 30, 600.00, true, true, true); -``` - -### 2.5 Crear Customers - -```sql -INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, notes, total_spent, total_visits, last_visit_date, is_active) -VALUES - (uuid_generate_v4(), 'Sofía', 'Ramírez', 'sofia.ramirez@example.com', '+52 55 1111 1111', 'gold', 'Cliente VIP. Prefiere Artists María y Ana.', 15000.00, 25, '2025-12-20', true), - (uuid_generate_v4(), 'Valentina', 'Hernández', 'valentina.hernandez@example.com', '+52 55 2222 2222', 'gold', 'Cliente regular. Prefiere mañanas.', 8500.00, 15, '2025-12-15', true), - (uuid_generate_v4(), 'Camila', 'López', 'camila.lopez@example.com', '+52 55 3333 3333', 'free', 'Nueva cliente. Referida por Valentina.', 500.00, 1, '2025-12-10', true), - (uuid_generate_v4(), 'Isabella', 'García', 'isabella.garcia@example.com', '+52 55 4444 4444', 'gold', 'Cliente VIP. Requiere Balayage.', 22000.00, 30, '2025-12-18', true); -``` - -### 2.6 Crear Invitaciones (para clientes Gold) - -```sql -SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com' LIMIT 1)); -SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'valentina.hernandez@example.com' LIMIT 1)); -SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'isabella.garcia@example.com' LIMIT 1)); -``` - -### 2.7 Crear Bookings - -```sql -INSERT INTO bookings (customer_id, staff_id, location_id, resource_id, service_id, start_time_utc, end_time_utc, status, deposit_amount, total_amount, is_paid, payment_reference, notes) -SELECT - (SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist María García' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Balayage Premium' LIMIT 1), - NOW() + INTERVAL '1 day', - NOW() + INTERVAL '4 hours', - 'confirmed', - 200.00, - 2000.00, - true, - 'pay_test_001', - 'Balayage Premium para Sofía' -UNION ALL -SELECT - (SELECT id FROM customers WHERE email = 'valentina.hernandez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist Ana Rodríguez' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Color Completo' LIMIT 1), - NOW() + INTERVAL '2 days', - NOW() + INTERVAL '4 hours', - 'confirmed', - 200.00, - 1200.00, - true, - 'pay_test_002', - 'Color Completo para Valentina' -UNION ALL -SELECT - (SELECT id FROM customers WHERE email = 'camila.lopez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist María García' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Corte y Estilizado' LIMIT 1), - NOW() + INTERVAL '3 days', - NOW() + INTERVAL '1 hour', - 'confirmed', - 50.00, - 500.00, - true, - 'pay_test_003', - 'Primer corte para Camila'; -``` - -### 2.8 Verificar Datos Creados - -```sql -SELECT 'Locations: ' || COUNT(*) as resumen FROM locations -UNION ALL -SELECT 'Resources: ' || COUNT(*) as resumen FROM resources -UNION ALL -SELECT 'Staff: ' || COUNT(*) as resumen FROM staff -UNION ALL -SELECT 'Services: ' || COUNT(*) as resumen FROM services -UNION ALL -SELECT 'Customers: ' || COUNT(*) as resumen FROM customers -UNION ALL -SELECT 'Invitaciones: ' || COUNT(*) as resumen FROM invitations WHERE status = 'pending' -UNION ALL -SELECT 'Bookings: ' || COUNT(*) as resumen FROM bookings; -``` - -**Resultado esperado:** -``` -resumen -Locations: 3 -Resources: 6 -Staff: 8 -Services: 6 -Customers: 4 -Invitaciones: 15 -Bookings: 3 -``` - ---- - -## 📋 PASO 3: CREAR USUARIOS AUTH - -### 3.1 Ir a Supabase Auth - -``` -https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/auth/users -``` - -### 3.2 Crear Usuarios (Manual) - -Haz clic en **"Add user"** y crea estos usuarios uno por uno: - -#### Admin -- **Email:** `admin@salonos.com` -- **Password:** `Admin123!` -- **Auto Confirm User:** ON -- **User Metadata (opcional):** - ```json - {"role": "admin", "display_name": "Admin Principal"} - ``` - -#### Customer Gold (para probar) -- **Email:** `sofia.ramirez@example.com` -- **Password:** `Customer123!` -- **Auto Confirm User:** ON -- **User Metadata (opcional):** - ```json - {"tier": "gold", "display_name": "Sofía Ramírez"} - ``` - -### 3.3 Actualizar Tablas con User IDs (Opcional) - -Si quieres conectar los usuarios de Auth con las tablas staff/customers: - -1. Ve a **Auth → Users** -2. Copia el **User ID** del usuario -3. En el SQL Editor, ejecuta: - -```sql --- Para actualizar customer -UPDATE customers -SET user_id = 'COPIA_EL_USER_ID_AQUI' -WHERE email = 'sofia.ramirez@example.com'; -``` - ---- - -## 📋 PASO 4: PROBAR FUNCIONALIDADES - -### 4.1 Probar Short ID - -En el SQL Editor: -```sql -SELECT generate_short_id(); -``` - -**Resultado:** Ej: `A3F7X2` - -### 4.2 Probar Código de Invitación - -```sql -SELECT generate_invitation_code(); -``` - -**Resultado:** Ej: `X9J4K2M5N8` - -### 4.3 Verificar Bookings - -```sql -SELECT b.short_id, c.first_name || ' ' || c.last_name as customer, s.display_name as artist, svc.name as service, b.status -FROM bookings b -JOIN customers c ON b.customer_id = c.id -JOIN staff s ON b.staff_id = s.id -JOIN services svc ON b.service_id = svc.id -ORDER BY b.start_time_utc; -``` - ---- - -## ✅ CHECKLIST FINAL - -- [ ] Migraciones ejecutadas en Supabase Dashboard -- [ ] 8 tablas creadas -- [ ] 14 funciones creadas -- [ ] 17+ triggers activos -- [ ] 20+ políticas RLS configuradas -- [ ] 3 locations creadas -- [ ] 6 resources creados -- [ ] 8 staff creados -- [ ] 6 services creados -- [ ] 4 customers creados -- [ ] 15 invitaciones creadas -- [ ] 3+ bookings creados -- [ ] Usuarios de Auth creados (admin + customer) -- [ ] Short ID generable -- [ ] Código de invitación generable - ---- - -## 🎯 PRÓXIMOS PASOS - -Una vez que todo esté completo: - -1. ✅ **Fase 1.1 y 1.2 completadas** -2. 🚀 **Continuar con desarrollo del frontend** (The Boutique / The HQ) -3. 🚀 **Implementar Tarea 1.3** (Short ID & Invitaciones - backend) -4. 🚀 **Implementar Tarea 1.4** (CRM Base - endpoints CRUD) - ---- - -## 💡 NOTA FINAL - -**No necesitas scripts de línea de comandos.** - -Todo lo que necesitas hacer está en **Supabase Dashboard**: - -1. Ir a: https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/sql -2. Copiar y pegar el SQL -3. Hacer clic en **"Run"** - -¡Eso es todo! 🎉 diff --git a/FASE_1_STATUS.md b/FASE_1_STATUS.md deleted file mode 100644 index ba8093b..0000000 --- a/FASE_1_STATUS.md +++ /dev/null @@ -1,376 +0,0 @@ -# SalonOS - Fase 1.1 y 1.2 Completadas con Éxito - -## ✅ Implementado - -### 1. Estructura de Carpetas Next.js 14 - -Se ha creado la estructura completa según el esquema definido en README.md: - -``` -/salonos -├── app/ -│ ├── boutique/ # Frontend cliente -│ ├── hq/ # Dashboard administrativo -│ └── api/ # API routes -├── components/ # Componentes UI -│ ├── boutique/ -│ ├── hq/ -│ └── shared/ -├── lib/ # Lógica de negocio -│ ├── supabase/ -│ ├── db/ -│ └── utils/ -├── db/ # Esquemas y migraciones -│ ├── migrations/ -│ │ ├── 001_initial_schema.sql ✅ -│ │ ├── 002_rls_policies.sql ✅ -│ │ └── 003_audit_triggers.sql ✅ -│ └── seeds/ -├── integrations/ # Stripe, Google, WhatsApp -├── styles/ # Config Tailwind -└── docs/ -``` - -### 2. Esquema de Base de Datos Completo - -#### Migración 001: `001_initial_schema.sql` - -Tablas creadas: -- **locations**: Ubicaciones del salón con timezone -- **resources**: Recursos físicos (estaciones, habitaciones, equipos) -- **staff**: Personal con roles jerárquicos -- **services**: Catálogo de servicios -- **customers**: Información de clientes con tier -- **invitations**: Sistema de invitaciones semanales -- **bookings**: Sistema de reservas con short_id -- **audit_logs**: Registro de auditoría - -Features: -- Todos los timestamps en UTC -- UUID como identificador primario -- Índices optimizados para consultas frecuentes -- Constraints de integridad referencial -- Sistema Doble Capa (Staff + Recurso) - -#### Migración 002: `002_rls_policies.sql` - -Políticas RLS implementadas: - -**Jerarquía de roles:** -``` -Admin > Manager > Staff > Artist > Customer -``` - -**Políticas críticas:** -- **Artist**: Solo puede ver `name` y `notes` de customers - - ❌ NO puede ver `email` - - ❌ NO puede ver `phone` -- **Staff/Manager/Admin**: Pueden ver PII completo -- **Customer**: Solo sus propios datos - -Funciones auxiliares: -- `get_current_user_role()`: Obtiene el rol del usuario autenticado -- `is_staff_or_higher()`: Verifica si es admin, manager o staff -- `is_artist()`: Verifica si es artist -- `is_customer()`: Verifica si es customer -- `is_admin()`: Verifica si es admin - -#### Migración 003: `003_audit_triggers.sql` - -Funciones implementadas: -- `generate_short_id()`: Generador de Short ID (6 caracteres, collision-safe) -- `generate_invitation_code()`: Generador de códigos de invitación (10 caracteres) -- `reset_weekly_invitations_for_customer()`: Reset individual de invitaciones -- `reset_all_weekly_invitations()`: Reset masivo de todas las invitaciones -- `log_audit()`: Trigger automático de auditoría - -Triggers: -- Auditoría automática en tablas críticas (bookings, customers, invitations, staff, services) -- Generación automática de short_id al crear booking - -### 3. Configuración Base del Proyecto - -Archivos creados: -- `package.json`: Dependencias Next.js 14, Supabase, Tailwind, Framer Motion -- `tsconfig.json`: Configuración TypeScript con paths aliases -- `next.config.js`: Configuración Next.js -- `tailwind.config.ts`: Configuración Tailwind con tema personalizado -- `postcss.config.js`: Configuración PostCSS -- `.env.example`: Template de variables de entorno -- `.gitignore`: Archivos ignorados por Git -- `lib/supabase/client.ts`: Cliente Supabase (anon y admin) -- `lib/db/types.ts`: TypeScript types basados en el esquema - -## 📋 Documentación Actualizada - -Archivos modificados: -- **PRD.md**: Reset semanal de invitaciones, jerarquía de roles -- **AGENTS.md**: Referencias a reset semanal, verificación de privacidad -- **TASKS.md**: Roles incluyen Artist, reset semanal, "colaboradoras" → "artists" - -## 🎯 Tareas Completadas (FASE 1) - -### ✅ Tarea 1.1: Infraestructura Base -- [x] Estructura de carpetas Next.js 14 -- [x] Configuración base (package.json, tsconfig, tailwind) -- [x] Template de variables de entorno - -### ✅ Tarea 1.2: Esquema de Base de Datos Inicial -- [x] 8 tablas obligatorias creadas -- [x] Claves foráneas y constraints -- [x] Campos de auditoría (`created_at`, `updated_at`) -- [x] Índices optimizados -- [x] Tipos ENUM definidos -- [x] **MIGRACIONES EJECUTADAS EN SUPABASE ✅** -- [x] Políticas RLS configuradas (20+ políticas) -- [x] Triggers de auditoría activos (17+ triggers) -- [x] Funciones auxiliares creadas (14 funciones) -- [x] Validación de secondary_artist implementada - -### ⏳ Tarea 1.3: Short ID & Invitaciones -- [x] Generador de Short ID (6 chars, collision-safe) -- [x] Generador de códigos de invitación -- [x] Lógica de reset semanal (Lunes 00:00 UTC) -- [ ] Validación de unicidad antes de persistir booking (backend) -- [ ] Tests unitarios - -### ⏳ Tarea 1.4: CRM Base (Customers) -- [ ] Endpoints CRUD -- [ ] Policies RLS por rol (ya implementadas en DB) -- [ ] Cálculo automático de Tier -- [ ] Tracking de referidos - -## 🚀 Próximos Pasos - -### 1. Verificar Instalación de Migraciones ✅ -- [x] Ejecutar migraciones en Supabase ✅ COMPLETADO -- [ ] Ejecutar script de verificación: `scripts/verify-migration.sql` en Supabase Dashboard -- [ ] Ejecutar script de seed: `scripts/seed-data.sql` en Supabase Dashboard -- [ ] Probar políticas RLS - -**Guía completa:** `docs/STEP_BY_STEP_VERIFICATION.md` - -**Contenido:** -- 12 consultas de verificación (tablas, funciones, triggers, políticas RLS, tipos ENUM) -- 9 secciones de seed (locations, resources, staff, services, customers, invitations, bookings) -- Consultas adicionales de prueba -- Checklist de verificación - -**Datos a crear con seed:** -- 3 locations (Centro, Polanco, Coyoacán) -- 6 resources (estaciones) -- 8 staff (1 admin, 2 managers, 1 staff, 4 artists) -- 6 services (catálogo completo) -- 4 customers (mix Free/Gold) -- 15 invitations (5 por cliente Gold) -- 5 bookings de prueba - -### 2. Configurar Auth en Supabase Dashboard -- [ ] Habilitar Email Provider -- [ ] Configurar Site URL y Redirect URLs -- [ ] Crear 8 usuarios de staff en Supabase Auth -- [ ] Crear 4 usuarios de customers en Supabase Auth -- [ ] Actualizar tablas staff y customers con user_ids correctos -- [ ] Configurar Email Templates (opcional) - -**Guía completa:** `docs/STEP_BY_STEP_AUTH_CONFIG.md` - -**Usuarios a crear:** - -**Staff (8):** -- Admin Principal: `admin@salonos.com` -- Manager Centro: `manager.centro@salonos.com` -- Manager Polanco: `manager.polanco@salonos.com` -- Staff Coordinadora: `staff.coordinadora@salonos.com` -- Artist María García: `artist.maria@salonos.com` -- Artist Ana Rodríguez: `artist.ana@salonos.com` -- Artist Carla López: `artist.carla@salonos.com` -- Artist Laura Martínez: `artist.laura@salonos.com` - -**Customers (4):** -- Sofía Ramírez (Gold): `sofia.ramirez@example.com` -- Valentina Hernández (Gold): `valentina.hernandez@example.com` -- Camila López (Free): `camila.lopez@example.com` -- Isabella García (Gold): `isabella.garcia@example.com` - -**Guía rápida:** `docs/QUICK_START_POST_MIGRATION.md` - -### 3. Implementar Tarea 1.3 completa - - Backend API endpoints para Short ID - - Tests unitarios de colisiones - - Edge Function o Cron Job para reset semanal - -3. **Implementar Tarea 1.4**: - - Endpoints CRUD de customers - - Lógica de cálculo automático de Tier - - Sistema de referidos - -## 📝 Notas Importantes - -### UTC-First -Todos los timestamps se almacenan en UTC. La conversión a zona horaria local ocurre solo en: -- Frontend (The Boutique / The HQ) -- Notificaciones (WhatsApp / Email) - -### Sistema Doble Capa -El sistema valida disponibilidad en dos niveles: -1. **Staff/Artist**: Horario laboral + Google Calendar -2. **Recurso**: Disponibilidad de estación física - -### Reset Semanal de Invitaciones -- Ejecutado automáticamente cada Lunes 00:00 UTC -- Solo para clientes Tier Gold -- Cada cliente recibe 5 invitaciones nuevas -- Proceso idempotente y auditado - -### Privacidad de Datos -- **Artist**: ❌ NO puede ver `email` ni `phone` de customers -- **Staff/Manager/Admin**: ✅ Pueden ver PII de customers -- Todas las consultas de Artist a `customers` están filtradas por RLS - -## 🔧 Comandos Útiles - -```bash -# Instalar dependencias -npm install - -# Ejecutar migraciones de base de datos -npm run db:migrate - -# Verificar instalación de migraciones (scripts SQL) -# Ejecutar: scripts/verify-migration.sql en Supabase Dashboard - -# Crear datos de prueba (scripts SQL) -# Ejecutar: scripts/seed-data.sql en Supabase Dashboard - -# Levantar servidor de desarrollo -npm run dev - -# Verificar TypeScript -npm run typecheck - -# Ejecutar linter -npm run lint -``` - -## 🎉 Estado de Migraciones en Supabase - -### ✅ MIGRACIONES EJECUTADAS EXITOSAMENTE - -**Proyecto:** pvvwbnybkadhreuqijsl -**Fecha:** 2026-01-15 -**Estado:** COMPLETADO - -**Tablas Creadas:** -- ✅ locations (3) -- ✅ resources (6) -- ✅ staff (0) -- ✅ services (0) -- ✅ customers (0) -- ✅ invitations (0) -- ✅ bookings (0) -- ✅ audit_logs (0) - -**Funciones Creadas (14):** -- ✅ generate_short_id -- ✅ generate_invitation_code -- ✅ reset_weekly_invitations_for_customer -- ✅ reset_all_weekly_invitations -- ✅ validate_secondary_artist_role -- ✅ log_audit -- ✅ get_current_user_role -- ✅ is_staff_or_higher -- ✅ is_artist -- ✅ is_customer -- ✅ is_admin -- ✅ update_updated_at -- ✅ generate_booking_short_id -- ✅ get_week_start - -**Triggers Activos (17+):** -- ✅ locations_updated_at -- ✅ resources_updated_at -- ✅ staff_updated_at -- ✅ services_updated_at -- ✅ customers_updated_at -- ✅ invitations_updated_at -- ✅ bookings_updated_at -- ✅ validate_booking_secondary_artist -- ✅ audit_bookings -- ✅ audit_customers -- ✅ audit_invitations -- ✅ audit_staff -- ✅ audit_services -- ✅ booking_generate_short_id - -**Políticas RLS Configuradas (20+):** -- ✅ Locations: 2 políticas -- ✅ Resources: 3 políticas -- ✅ Staff: 3 políticas -- ✅ Services: 2 políticas -- ✅ Customers: 5 políticas (incluyendo restricción Artist) -- ✅ Invitations: 3 políticas -- ✅ Bookings: 7 políticas -- ✅ Audit logs: 2 políticas - -**Tipos ENUM (6):** -- ✅ user_role -- ✅ customer_tier -- ✅ booking_status -- ✅ invitation_status -- ✅ resource_type -- ✅ audit_action - -**Correcciones Aplicadas:** -- ✅ Constraint de secondary_artist reemplazado por trigger de validación -- ✅ Variable customer_record declarada en reset_all_weekly_invitations() - -### 📚 Guías de Post-Migración - -1. **Verificación:** Ejecutar `scripts/verify-migration.sql` -2. **Seed de datos:** Ejecutar `scripts/seed-data.sql` -3. **Configuración Auth:** Configurar en Supabase Dashboard -4. **Pruebas:** Probar funcionalidades en `docs/POST_MIGRATION_SUCCESS.md` - -## 📞 Contacto - -Para dudas sobre la implementación, consultar: -- PRD.md: Reglas de negocio -- TASKS.md: Plan de ejecución -- AGENTS.md: Roles y responsabilidades -- db/migrations/README.md: Guía de migraciones - ---- - -## 📞 Documentación Disponible - -- **PRD.md**: Reglas de negocio del sistema -- **TASKS.md**: Plan de ejecución por fases -- **AGENTS.md**: Roles y responsabilidades de IA -- **db/migrations/README.md**: Guía técnica de migraciones -- **docs/MIGRATION_GUIDE.md**: Guía detallada de migraciones -- **docs/00_FULL_MIGRATION_FINAL_README.md**: Guía de migración final -- **docs/MIGRATION_CORRECTION.md**: Detalle de correcciones aplicadas -- **docs/SUPABASE_DASHBOARD_MIGRATION.md**: Guía de ejecución en Dashboard -- **docs/POST_MIGRATION_SUCCESS.md**: Guía post-migración (verificación y seed) -- **scripts/verify-migration.sql**: Script de verificación -- **scripts/seed-data.sql**: Script de datos de prueba - ---- - -**Estado**: ✅ **FASE 1.1 y 1.2 COMPLETADAS EXITOSAMENTE** - -- ✅ Migraciones ejecutadas en Supabase -- ✅ Base de datos completamente configurada -- ✅ Políticas RLS activas (incluyendo restricción Artist) -- ✅ Sistema de auditoría activo -- ✅ Funciones de Short ID e invitaciones funcionales -- ✅ Validación de secondary_artist implementada -- ✅ Listo para continuar con Tarea 1.3 y 1.4 - -**Próximos pasos:** -1. Ejecutar script de verificación en Supabase Dashboard -2. Ejecutar script de seed para crear datos de prueba -3. Configurar Auth en Supabase Dashboard -4. Implementar Tarea 1.3 (Short ID & Invitaciones - backend) -5. Implementar Tarea 1.4 (CRM Base) diff --git a/SIMPLE_GUIDE.md b/SIMPLE_GUIDE.md deleted file mode 100644 index a43f915..0000000 --- a/SIMPLE_GUIDE.md +++ /dev/null @@ -1,229 +0,0 @@ -# 🚀 GUÍA SIMPLE - SALONOS - -## ✅ ESTADO - -- ✅ Migraciones ejecutadas exitosamente en Supabase -- ✅ Scripts simples creados para facilitar el setup -- ✅ Base de datos lista para desarrollo - ---- - -## 📋 PASOS RÁPIDOS (EN ORDEN) - -### Paso 1: Verificar Conexión - -```bash -npm run simple:check -``` - -**Qué hace:** Verifica si puedes conectarte a Supabase desde la línea de comandos. - -**Si dice "Puerto 5432 está bloqueado":** -- No te preocupes -- Usa Supabase Dashboard: https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/sql -- Ignora los pasos 2 y 3, ve directo al paso "ALTERNATIVA: USAR SUPABASE DASHBOARD" - ---- - -### Paso 2: Verificar Migraciones - -```bash -npm run simple:verify -``` - -**Qué hace:** Verifica que todo esté correcto en la base de datos. - -**Output esperado:** -``` -🎉 TODAS LAS MIGRACIONES ESTÁN CORRECTAS -``` - ---- - -### Paso 3: Crear Datos de Prueba - -```bash -npm run simple:seed -``` - -**Qué hace:** Crea locations, staff, services, customers, invitations, bookings. - -**Output esperado:** -``` -🎉 SEED DE DATOS COMPLETADO EXITOSAMENTE -``` - ---- - -### Paso 4: Crear Usuarios de Auth - -```bash -npm run auth:create -``` - -**Qué hace:** Crea usuarios de staff y customers en Supabase Auth automáticamente. - -**Output esperado:** -``` -🎉 TODOS LOS USUARIOS HAN SIDO CREADOS Y ACTUALIZADOS - -📝 Credenciales de prueba: - -ADMIN: - Email: admin@salonos.com - Password: Admin123! - -CUSTOMER (Gold): - Email: sofia.ramirez@example.com - Password: Customer123! -``` - ---- - -## 🚨 ALTERNATIVA: USAR SUPABASE DASHBOARD - -Si el puerto 5432 está bloqueado (común en empresas con firewall): - -### Opción 1: Ejecutar Migraciones Completas -1. Ve a: https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/sql -2. Copia el contenido de: `db/migrations/00_FULL_MIGRATION_FINAL.sql` -3. Pega en el SQL Editor -4. Haz clic en **"Run"** - -### Opción 2: Crear Usuarios Manualmente -1. Ve a: https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/auth/users -2. Haz clic en **"Add user"** -3. Crea estos usuarios: - -**Admin:** -- Email: `admin@salonos.com` -- Password: `Admin123!` -- Auto Confirm: ON - -**Staff (Manager Centro):** -- Email: `manager.centro@salonos.com` -- Password: `Manager123!` -- Auto Confirm: ON - -**Customer (Gold):** -- Email: `sofia.ramirez@example.com` -- Password: `Customer123!` -- Auto Confirm: ON - ---- - -## 📚 GUÍAS DETALLADAS - -Si necesitas más detalles: - -- **`scripts/README.md`** - Documentación completa de todos los scripts -- **`docs/STEP_BY_STEP_VERIFICATION.md`** - Guía paso a paso detallada -- **`docs/STEP_BY_STEP_AUTH_CONFIG.md`** - Guía de configuración de Auth -- **`docs/QUICK_START_POST_MIGRATION.md`** - Guía rápida de referencia - ---- - -## ✅ CHECKLIST - -Después de ejecutar todos los pasos: - -- [ ] Conexión verificada (o usando Dashboard) -- [ ] Migraciones verificadas (8 tablas, 14 funciones, 17+ triggers) -- [ ] Datos de prueba creados (3 locations, 6 resources, 8 staff, 6 services, 4 customers, 15 invitations, 5 bookings) -- [ ] Usuarios de Auth creados (8 staff + 4 customers) -- [ ] Credenciales de prueba guardadas - ---- - -## 🎯 PRÓXIMOS PASOS - -### Probar el Login - -1. Ve a Supabase Dashboard: https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/auth/users -2. Verifica que los usuarios estén creados -3. Intenta hacer login con una de las credenciales de prueba - -### Verificar Políticas RLS - -En Supabase Dashboard, ejecuta esta consulta: - -```sql --- Verificar que Artist no puede ver email/phone de customers -SELECT - c.first_name, - c.email, -- Debería ser NULL si eres Artist - c.phone -- Debería ser NULL si eres Artist -FROM customers c -LIMIT 1; -``` - -### Continuar con el Desarrollo - -Una vez que todo esté configurado: - -1. **Implementar Tarea 1.3:** Short ID & Invitaciones (backend) -2. **Implementar Tarea 1.4:** CRM Base (endpoints CRUD) -3. **Iniciar desarrollo del frontend** (The Boutique / The HQ) - ---- - -## 💡 TIPS - -### Tip 1: Scripts vs Dashboard -- **Scripts** son más rápidos pero requieren puerto 5432 abierto -- **Dashboard** es más lento pero siempre funciona (si el puerto está bloqueado) - -### Tip 2: Guardar las Credenciales -Guarda estas credenciales en un lugar seguro: - -**Admin:** -- Email: `admin@salonos.com` -- Password: `Admin123!` - -**Customer (Gold):** -- Email: `sofia.ramirez@example.com` -- Password: `Customer123!` - -### Tip 3: Verificar Cada Paso -No continúes al siguiente paso hasta verificar que el anterior esté correcto. - -### Tip 4: Consultar los Logs -Si algo falla, consulta los logs en Supabase Dashboard. - ---- - -## 🆘 AYUDA - -Si encuentras problemas: - -1. **Revisa los logs de Supabase Dashboard** -2. **Ejecuta el script de verificación** (`npm run simple:verify`) -3. **Consulta las guías detalladas** en `docs/` -4. **Si el puerto está bloqueado**, usa Supabase Dashboard - ---- - -## 📞 CONTACTO - -Para dudas sobre la implementación, consultar: -- **PRD.md**: Reglas de negocio -- **TASKS.md**: Plan de ejecución -- **AGENTS.md**: Roles y responsabilidades -- **scripts/README.md**: Documentación completa de scripts - ---- - -## 🎉 ¡LISTO PARA COMENZAR! - -Todo está preparado para que empieces el desarrollo de SalonOS. - -**¿Qué deseas hacer ahora?** - -1. **Ejecutar los scripts simples** (si el puerto está abierto) -2. **Usar Supabase Dashboard** (si el puerto está bloqueado) -3. **Comenzar el desarrollo del frontend** (Next.js) -4. **Implementar las tareas de backend** (Tarea 1.3 y 1.4) - ---- - -**¡El futuro es tuyo!** 🚀 diff --git a/db/migrate.sh b/db/migrate.sh deleted file mode 100755 index fef97e6..0000000 --- a/db/migrate.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/bin/bash - -# ============================================ -# SALONOS - DATABASE MIGRATION SCRIPT -# ============================================ -# Ejecuta todas las migraciones de base de datos -# ============================================ - -set -e # Detener en errores - -echo "==========================================" -echo "SALONOS - DATABASE MIGRATION" -echo "==========================================" -echo "" - -# Verificar que .env.local existe -if [ ! -f .env.local ]; then - echo "❌ ERROR: .env.local no encontrado" - echo "Por favor, crea el archivo .env.local con tus credenciales de Supabase" - echo "Puedes copiar el archivo .env.example:" - echo " cp .env.local.example .env.local" - exit 1 -fi - -# Cargar variables de entorno desde .env.local -echo "📂 Cargando variables de entorno desde .env.local..." -export $(grep -v '^#' .env.local | xargs) - -# Verificar que las variables de Supabase estén configuradas -if [ -z "$NEXT_PUBLIC_SUPABASE_URL" ] || [ -z "$SUPABASE_SERVICE_ROLE_KEY" ]; then - echo "❌ ERROR: Faltan variables de entorno de Supabase" - echo "Verifica que tu archivo .env.local contenga:" - echo " NEXT_PUBLIC_SUPABASE_URL" - echo " NEXT_PUBLIC_SUPABASE_ANON_KEY" - echo " SUPABASE_SERVICE_ROLE_KEY" - exit 1 -fi - -echo "✅ Variables de entorno cargadas" -echo "" - -# Extraer DATABASE_URL de NEXT_PUBLIC_SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY -# Formato esperado: postgresql://postgres:[password]@[project-id].supabase.co:5432/postgres - -echo "🔍 Verificando conexión a Supabase..." -echo " URL: ${NEXT_PUBLIC_SUPABASE_URL:0:30}..." -echo "" - -# Verificar si psql está instalado -if ! command -v psql &> /dev/null; then - echo "❌ ERROR: psql no está instalado" - echo "Por favor, instala PostgreSQL client:" - echo " macOS: brew install postgresql" - echo " Ubuntu/Debian: sudo apt-get install postgresql-client" - echo " Windows: Descargar desde https://www.postgresql.org/download/windows/" - exit 1 -fi - -echo "✅ psql encontrado" -echo "" - -# Ejecutar migraciones -echo "🚀 Iniciando migraciones..." -echo "" - -echo "📦 MIGRACIÓN 001: Esquema inicial..." -if psql "${NEXT_PUBLIC_SUPABASE_URL/https:\/\//postgresql:\/\/postgres:}${SUPABASE_SERVICE_ROLE_KEY}@${NEXT_PUBLIC_SUPABASE_URL#https://}" -f db/migrations/001_initial_schema.sql; then - echo "✅ MIGRACIÓN 001 completada" -else - echo "❌ ERROR en MIGRACIÓN 001" - exit 1 -fi - -echo "" -echo "📦 MIGRACIÓN 002: Políticas RLS..." -if psql "${NEXT_PUBLIC_SUPABASE_URL/https:\/\//postgresql:\/\/postgres:}${SUPABASE_SERVICE_ROLE_KEY}@${NEXT_PUBLIC_SUPABASE_URL#https://}" -f db/migrations/002_rls_policies.sql; then - echo "✅ MIGRACIÓN 002 completada" -else - echo "❌ ERROR en MIGRACIÓN 002" - exit 1 -fi - -echo "" -echo "📦 MIGRACIÓN 003: Triggers de auditoría..." -if psql "${NEXT_PUBLIC_SUPABASE_URL/https:\/\//postgresql:\/\/postgres:}${SUPABASE_SERVICE_ROLE_KEY}@${NEXT_PUBLIC_SUPABASE_URL#https://}" -f db/migrations/003_audit_triggers.sql; then - echo "✅ MIGRACIÓN 003 completada" -else - echo "❌ ERROR en MIGRACIÓN 003" - exit 1 -fi - -echo "" -echo "==========================================" -echo "✅ TODAS LAS MIGRACIONES COMPLETADAS" -echo "==========================================" -echo "" -echo "📊 Verificación del esquema:" -echo "" - -# Verificación básica -psql "${NEXT_PUBLIC_SUPABASE_URL/https:\/\//postgresql:\/\/postgres:}${SUPABASE_SERVICE_ROLE_KEY}@${NEXT_PUBLIC_SUPABASE_URL#https://}" -c "SELECT 'Tablas creadas: ' || COUNT(*) as info FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN ('locations', 'resources', 'staff', 'services', 'customers', 'invitations', 'bookings', 'audit_logs');" - -psql "${NEXT_PUBLIC_SUPABASE_URL/https:\/\//postgresql:\/\/postgres:}${SUPABASE_SERVICE_ROLE_KEY}@${NEXT_PUBLIC_SUPABASE_URL#https://}" -c "SELECT 'Funciones creadas: ' || COUNT(*) as info FROM information_schema.routines WHERE routine_schema = 'public';" - -psql "${NEXT_PUBLIC_SUPABASE_URL/https:\/\//postgresql:\/\/postgres:}${SUPABASE_SERVICE_ROLE_KEY}@${NEXT_PUBLIC_SUPABASE_URL#https://}" -c "SELECT 'Políticas RLS: ' || COUNT(*) as info FROM pg_policies WHERE schemaname = 'public';" - -echo "" -echo "🎉 Setup de base de datos completado exitosamente" -echo "" -echo "📝 Próximos pasos:" -echo " 1. Configurar Auth en Supabase Dashboard" -echo " 2. Crear usuarios de prueba con roles específicos" -echo " 3. Ejecutar seeds de datos de prueba" -echo "" diff --git a/db/migrations/001_initial_schema.sql b/db/migrations/001_initial_schema.sql deleted file mode 100644 index adaaf6e..0000000 --- a/db/migrations/001_initial_schema.sql +++ /dev/null @@ -1,279 +0,0 @@ --- Migración 001: Esquema base de datos SalonOS --- Version: 001 --- Fecha: 2026-01-15 --- Descripción: Creación de tablas principales con jerarquía de roles y sistema doble capa - --- Habilitar UUID extension -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- ============================================ --- ENUMS --- ============================================ - -CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer'); -CREATE TYPE customer_tier AS ENUM ('free', 'gold'); -CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'cancelled', 'completed', 'no_show'); -CREATE TYPE invitation_status AS ENUM ('pending', 'used', 'expired'); -CREATE TYPE resource_type AS ENUM ('station', 'room', 'equipment'); -CREATE TYPE audit_action AS ENUM ('create', 'update', 'delete', 'reset_invitations', 'payment', 'status_change'); - --- ============================================ --- LOCATIONS --- ============================================ - -CREATE TABLE locations ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - name VARCHAR(100) NOT NULL, - timezone VARCHAR(50) NOT NULL DEFAULT 'UTC', - address TEXT, - phone VARCHAR(20), - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- ============================================ --- RESOURCES --- ============================================ - -CREATE TABLE resources ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE, - name VARCHAR(100) NOT NULL, - type resource_type NOT NULL, - capacity INTEGER DEFAULT 1, - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- ============================================ --- STAFF --- ============================================ - -CREATE TABLE staff ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - user_id UUID NOT NULL, - location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE, - role user_role NOT NULL CHECK (role IN ('admin', 'manager', 'staff', 'artist')), - display_name VARCHAR(100) NOT NULL, - phone VARCHAR(20), - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - UNIQUE(user_id, location_id) -); - --- ============================================ --- SERVICES --- ============================================ - -CREATE TABLE services ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - name VARCHAR(100) NOT NULL, - description TEXT, - duration_minutes INTEGER NOT NULL CHECK (duration_minutes > 0), - base_price DECIMAL(10, 2) NOT NULL CHECK (base_price >= 0), - requires_dual_artist BOOLEAN DEFAULT false, - premium_fee_enabled BOOLEAN DEFAULT false, - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- ============================================ --- CUSTOMERS --- ============================================ - -CREATE TABLE customers ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - user_id UUID UNIQUE, - first_name VARCHAR(100) NOT NULL, - last_name VARCHAR(100) NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL, - phone VARCHAR(20), - tier customer_tier DEFAULT 'free', - notes TEXT, - total_spent DECIMAL(10, 2) DEFAULT 0, - total_visits INTEGER DEFAULT 0, - last_visit_date DATE, - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- ============================================ --- INVITATIONS --- ============================================ - -CREATE TABLE invitations ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - inviter_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, - code VARCHAR(10) UNIQUE NOT NULL, - email VARCHAR(255), - status invitation_status DEFAULT 'pending', - week_start_date DATE NOT NULL, - expiry_date DATE NOT NULL, - used_at TIMESTAMPTZ, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- ============================================ --- BOOKINGS --- ============================================ - -CREATE TABLE bookings ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - short_id VARCHAR(6) UNIQUE NOT NULL, - customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, - staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT, - secondary_artist_id UUID REFERENCES staff(id) ON DELETE SET NULL, - location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE, - resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE, - service_id UUID NOT NULL REFERENCES services(id) ON DELETE RESTRICT, - start_time_utc TIMESTAMPTZ NOT NULL, - end_time_utc TIMESTAMPTZ NOT NULL, - status booking_status DEFAULT 'pending', - deposit_amount DECIMAL(10, 2) DEFAULT 0, - total_amount DECIMAL(10, 2) NOT NULL, - is_paid BOOLEAN DEFAULT false, - payment_reference VARCHAR(50), - notes TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- ============================================ --- AUDIT LOGS --- ============================================ - -CREATE TABLE audit_logs ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - entity_type VARCHAR(50) NOT NULL, - entity_id UUID NOT NULL, - action audit_action NOT NULL, - old_values JSONB, - new_values JSONB, - performed_by UUID, - performed_by_role user_role, - ip_address INET, - user_agent TEXT, - metadata JSONB, - created_at TIMESTAMPTZ DEFAULT NOW() -); - --- ============================================ --- INDEXES --- ============================================ - --- Locations -CREATE INDEX idx_locations_active ON locations(is_active); - --- Resources -CREATE INDEX idx_resources_location ON resources(location_id); -CREATE INDEX idx_resources_active ON resources(location_id, is_active); - --- Staff -CREATE INDEX idx_staff_user ON staff(user_id); -CREATE INDEX idx_staff_location ON staff(location_id); -CREATE INDEX idx_staff_role ON staff(location_id, role, is_active); - --- Services -CREATE INDEX idx_services_active ON services(is_active); - --- Customers -CREATE INDEX idx_customers_tier ON customers(tier); -CREATE INDEX idx_customers_email ON customers(email); -CREATE INDEX idx_customers_active ON customers(is_active); - --- Invitations -CREATE INDEX idx_invitations_inviter ON invitations(inviter_id); -CREATE INDEX idx_invitations_code ON invitations(code); -CREATE INDEX idx_invitations_week ON invitations(week_start_date, status); - --- Bookings -CREATE INDEX idx_bookings_customer ON bookings(customer_id); -CREATE INDEX idx_bookings_staff ON bookings(staff_id); -CREATE INDEX idx_bookings_secondary_artist ON bookings(secondary_artist_id); -CREATE INDEX idx_bookings_location ON bookings(location_id); -CREATE INDEX idx_bookings_resource ON bookings(resource_id); -CREATE INDEX idx_bookings_time ON bookings(start_time_utc, end_time_utc); -CREATE INDEX idx_bookings_status ON bookings(status); -CREATE INDEX idx_bookings_short_id ON bookings(short_id); - --- Audit logs -CREATE INDEX idx_audit_entity ON audit_logs(entity_type, entity_id); -CREATE INDEX idx_audit_action ON audit_logs(action, created_at); -CREATE INDEX idx_audit_performed ON audit_logs(performed_by); - --- ============================================ --- TRIGGERS FOR UPDATED_AT --- ============================================ - -CREATE OR REPLACE FUNCTION update_updated_at() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER locations_updated_at BEFORE UPDATE ON locations - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -CREATE TRIGGER resources_updated_at BEFORE UPDATE ON resources - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -CREATE TRIGGER staff_updated_at BEFORE UPDATE ON staff - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -CREATE TRIGGER services_updated_at BEFORE UPDATE ON services - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -CREATE TRIGGER customers_updated_at BEFORE UPDATE ON customers - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -CREATE TRIGGER invitations_updated_at BEFORE UPDATE ON invitations - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -CREATE TRIGGER bookings_updated_at BEFORE UPDATE ON bookings - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - --- ============================================ --- CONSTRAINTS --- ============================================ - --- Constraint: Booking time validation -ALTER TABLE bookings ADD CONSTRAINT check_booking_time - CHECK (end_time_utc > start_time_utc); - --- Constraint: Booking cannot overlap for same resource (enforced in app layer with proper locking) --- This is documented for future constraint implementation - --- Trigger for secondary_artist validation (PostgreSQL doesn't allow subqueries in CHECK constraints) -CREATE OR REPLACE FUNCTION validate_secondary_artist_role() -RETURNS TRIGGER AS $$ -BEGIN - IF NEW.secondary_artist_id IS NOT NULL THEN - IF NOT EXISTS ( - SELECT 1 FROM staff s - WHERE s.id = NEW.secondary_artist_id AND s.role = 'artist' AND s.is_active = true - ) THEN - RAISE EXCEPTION 'secondary_artist_id must reference an active staff member with role ''artist'''; - END IF; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER validate_booking_secondary_artist BEFORE INSERT OR UPDATE ON bookings - FOR EACH ROW EXECUTE FUNCTION validate_secondary_artist_role(); - --- Constraint: Invitation week_start_date must be Monday -ALTER TABLE invitations ADD CONSTRAINT check_week_start_is_monday - CHECK (EXTRACT(ISODOW FROM week_start_date) = 1); - --- ============================================ --- END OF MIGRATION 001 --- ============================================ \ No newline at end of file diff --git a/db/migrations/002_rls_policies.sql b/db/migrations/002_rls_policies.sql deleted file mode 100644 index 4537b0f..0000000 --- a/db/migrations/002_rls_policies.sql +++ /dev/null @@ -1,335 +0,0 @@ --- Migración 002: Políticas RLS por rol --- Version: 002 --- Fecha: 2026-01-15 --- Descripción: Configuración de Row Level Security con jerarquía de roles y restricciones de privacidad - --- ============================================ --- HELPER FUNCTIONS --- ============================================ - --- Función para obtener el rol del usuario actual -CREATE OR REPLACE FUNCTION get_current_user_role() -RETURNS user_role AS $$ - DECLARE - current_staff_role user_role; - current_user_id UUID := auth.uid(); - BEGIN - SELECT s.role INTO current_staff_role - FROM staff s - WHERE s.user_id = current_user_id - LIMIT 1; - - IF current_staff_role IS NOT NULL THEN - RETURN current_staff_role; - END IF; - - -- Si es customer, verificar si existe en customers - IF EXISTS (SELECT 1 FROM customers WHERE user_id = current_user_id) THEN - RETURN 'customer'; - END IF; - - RETURN NULL; - END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Función para verificar si el usuario es staff o superior (admin, manager, staff) -CREATE OR REPLACE FUNCTION is_staff_or_higher() -RETURNS BOOLEAN AS $$ - DECLARE - user_role user_role := get_current_user_role(); - BEGIN - RETURN user_role IN ('admin', 'manager', 'staff'); - END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Función para verificar si el usuario es artist -CREATE OR REPLACE FUNCTION is_artist() -RETURNS BOOLEAN AS $$ - DECLARE - user_role user_role := get_current_user_role(); - BEGIN - RETURN user_role = 'artist'; - END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Función para verificar si el usuario es customer -CREATE OR REPLACE FUNCTION is_customer() -RETURNS BOOLEAN AS $$ - DECLARE - user_role user_role := get_current_user_role(); - BEGIN - RETURN user_role = 'customer'; - END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Función para verificar si el usuario es admin -CREATE OR REPLACE FUNCTION is_admin() -RETURNS BOOLEAN AS $$ - DECLARE - user_role user_role := get_current_user_role(); - BEGIN - RETURN user_role = 'admin'; - END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- ============================================ --- ENABLE RLS ON ALL TABLES --- ============================================ - -ALTER TABLE locations ENABLE ROW LEVEL SECURITY; -ALTER TABLE resources ENABLE ROW LEVEL SECURITY; -ALTER TABLE staff ENABLE ROW LEVEL SECURITY; -ALTER TABLE services ENABLE ROW LEVEL SECURITY; -ALTER TABLE customers ENABLE ROW LEVEL SECURITY; -ALTER TABLE invitations ENABLE ROW LEVEL SECURITY; -ALTER TABLE bookings ENABLE ROW LEVEL SECURITY; -ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY; - --- ============================================ --- LOCATIONS POLICIES --- ============================================ - --- Admin/Manager/Staff: Ver todas las locations activas -CREATE POLICY "locations_select_staff_higher" ON locations - FOR SELECT - USING (is_staff_or_higher() OR is_admin() OR is_admin()); - --- Admin/Manager: Insertar, actualizar, eliminar locations -CREATE POLICY "locations_modify_admin_manager" ON locations - FOR ALL - USING (get_current_user_role() IN ('admin', 'manager')); - --- ============================================ --- RESOURCES POLICIES --- ============================================ - --- Staff o superior: Ver recursos activos -CREATE POLICY "resources_select_staff_higher" ON resources - FOR SELECT - USING (is_staff_or_higher() OR is_admin()); - --- Artist: Ver recursos activos (necesario para ver disponibilidad) -CREATE POLICY "resources_select_artist" ON resources - FOR SELECT - USING (is_artist()); - --- Admin/Manager: Modificar recursos -CREATE POLICY "resources_modify_admin_manager" ON resources - FOR ALL - USING (get_current_user_role() IN ('admin', 'manager')); - --- ============================================ --- STAFF POLICIES --- ============================================ - --- Admin/Manager: Ver todo el staff -CREATE POLICY "staff_select_admin_manager" ON staff - FOR SELECT - USING (get_current_user_role() IN ('admin', 'manager')); - --- Staff: Ver staff en su misma ubicación -CREATE POLICY "staff_select_same_location" ON staff - FOR SELECT - USING ( - is_staff_or_higher() AND - EXISTS ( - SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = staff.location_id - ) - ); - --- Artist: Ver solo otros artists en su misma ubicación -CREATE POLICY "staff_select_artist_view_artists" ON staff - FOR SELECT - USING ( - is_artist() AND - EXISTS ( - SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = staff.location_id - ) AND - staff.role = 'artist' - ); - --- Admin/Manager: Modificar staff -CREATE POLICY "staff_modify_admin_manager" ON staff - FOR ALL - USING (get_current_user_role() IN ('admin', 'manager')); - --- ============================================ --- SERVICES POLICIES --- ============================================ - --- Todos los usuarios autenticados: Ver servicios activos -CREATE POLICY "services_select_all" ON services - FOR SELECT - USING (is_active = true); - --- Admin/Manager: Ver y modificar todos los servicios -CREATE POLICY "services_all_admin_manager" ON services - FOR ALL - USING (get_current_user_role() IN ('admin', 'manager')); - --- ============================================ --- CUSTOMERS POLICIES --- ============================================ - --- Admin/Manager: Ver todo (incluyendo PII) -CREATE POLICY "customers_select_admin_manager" ON customers - FOR SELECT - USING (get_current_user_role() IN ('admin', 'manager')); - --- Staff: Ver todo (incluyendo PII) - Pueden ver email/phone según PRD actualizado -CREATE POLICY "customers_select_staff" ON customers - FOR SELECT - USING (is_staff_or_higher()); - --- Artist: Solo nombre y notas, NO email ni phone -CREATE POLICY "customers_select_artist_restricted" ON customers - FOR SELECT - USING (is_artist()); - --- Customer: Ver solo sus propios datos -CREATE POLICY "customers_select_own" ON customers - FOR SELECT - USING (is_customer() AND user_id = auth.uid()); - --- Admin/Manager: Modificar cualquier cliente -CREATE POLICY "customers_modify_admin_manager" ON customers - FOR ALL - USING (get_current_user_role() IN ('admin', 'manager')); - --- Staff: Modificar cualquier cliente -CREATE POLICY "customers_modify_staff" ON customers - FOR ALL - USING (is_staff_or_higher()); - --- Customer: Actualizar solo sus propios datos -CREATE POLICY "customers_update_own" ON customers - FOR UPDATE - USING (is_customer() AND user_id = auth.uid()); - --- ============================================ --- INVITATIONS POLICIES --- ============================================ - --- Admin/Manager: Ver todas las invitaciones -CREATE POLICY "invitations_select_admin_manager" ON invitations - FOR SELECT - USING (get_current_user_role() IN ('admin', 'manager')); - --- Staff: Ver todas las invitaciones -CREATE POLICY "invitations_select_staff" ON invitations - FOR SELECT - USING (is_staff_or_higher()); - --- Customer: Ver solo sus propias invitaciones (como inviter) -CREATE POLICY "invitations_select_own" ON invitations - FOR SELECT - USING (is_customer() AND inviter_id = (SELECT id FROM customers WHERE user_id = auth.uid())); - --- Admin/Manager: Modificar cualquier invitación -CREATE POLICY "invitations_modify_admin_manager" ON invitations - FOR ALL - USING (get_current_user_role() IN ('admin', 'manager')); - --- Staff: Modificar invitaciones -CREATE POLICY "invitations_modify_staff" ON invitations - FOR ALL - USING (is_staff_or_higher()); - --- ============================================ --- BOOKINGS POLICIES --- ============================================ - --- Admin/Manager: Ver todos los bookings -CREATE POLICY "bookings_select_admin_manager" ON bookings - FOR SELECT - USING (get_current_user_role() IN ('admin', 'manager')); - --- Staff: Ver bookings de su ubicación -CREATE POLICY "bookings_select_staff_location" ON bookings - FOR SELECT - USING ( - is_staff_or_higher() AND - EXISTS ( - SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = bookings.location_id - ) - ); - --- Artist: Ver bookings donde es el artist asignado o secondary_artist -CREATE POLICY "bookings_select_artist_own" ON bookings - FOR SELECT - USING ( - is_artist() AND - (staff_id = (SELECT id FROM staff WHERE user_id = auth.uid()) OR - secondary_artist_id = (SELECT id FROM staff WHERE user_id = auth.uid())) - ); - --- Customer: Ver solo sus propios bookings -CREATE POLICY "bookings_select_own" ON bookings - FOR SELECT - USING (is_customer() AND customer_id = (SELECT id FROM customers WHERE user_id = auth.uid())); - --- Admin/Manager: Modificar cualquier booking -CREATE POLICY "bookings_modify_admin_manager" ON bookings - FOR ALL - USING (get_current_user_role() IN ('admin', 'manager')); - --- Staff: Modificar bookings de su ubicación -CREATE POLICY "bookings_modify_staff_location" ON bookings - FOR ALL - USING ( - is_staff_or_higher() AND - EXISTS ( - SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = bookings.location_id - ) - ); - --- Artist: No puede modificar bookings, solo ver -CREATE POLICY "bookings_no_modify_artist" ON bookings - FOR ALL - USING (NOT is_artist()); - --- Customer: Crear y actualizar sus propios bookings -CREATE POLICY "bookings_create_own" ON bookings - FOR INSERT - WITH CHECK ( - is_customer() AND - customer_id = (SELECT id FROM customers WHERE user_id = auth.uid()) - ); - -CREATE POLICY "bookings_update_own" ON bookings - FOR UPDATE - USING ( - is_customer() AND - customer_id = (SELECT id FROM customers WHERE user_id = auth.uid()) - ); - --- ============================================ --- AUDIT LOGS POLICIES --- ============================================ - --- Admin/Manager: Ver todos los audit logs -CREATE POLICY "audit_logs_select_admin_manager" ON audit_logs - FOR SELECT - USING (get_current_user_role() IN ('admin', 'manager')); - --- Staff: Ver logs de su ubicación -CREATE POLICY "audit_logs_select_staff_location" ON audit_logs - FOR SELECT - USING ( - is_staff_or_higher() AND - EXISTS ( - SELECT 1 FROM bookings b - JOIN staff s ON s.user_id = auth.uid() - WHERE b.id = audit_logs.entity_id - AND b.location_id = s.location_id - ) - ); - --- Solo backend puede insertar audit logs -CREATE POLICY "audit_logs_no_insert" ON audit_logs - FOR INSERT - WITH CHECK (false); - --- ============================================ --- END OF MIGRATION 002 --- ============================================ \ No newline at end of file diff --git a/db/migrations/003_audit_triggers.sql b/db/migrations/003_audit_triggers.sql deleted file mode 100644 index c0b8634..0000000 --- a/db/migrations/003_audit_triggers.sql +++ /dev/null @@ -1,309 +0,0 @@ --- Migración 003: Funciones auxiliares y triggers de auditoría --- Version: 003 --- Fecha: 2026-01-15 --- Descripción: Generador de Short ID, funciones de reset semanal de invitaciones y triggers de auditoría - --- ============================================ --- SHORT ID GENERATOR --- ============================================ - -CREATE OR REPLACE FUNCTION generate_short_id() -RETURNS VARCHAR(6) AS $$ -DECLARE - chars VARCHAR(36) := '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - short_id VARCHAR(6); - attempts INT := 0; - max_attempts INT := 10; -BEGIN - LOOP - short_id := ''; - FOR i IN 1..6 LOOP - short_id := short_id || substr(chars, floor(random() * 36 + 1)::INT, 1); - END LOOP; - - IF NOT EXISTS (SELECT 1 FROM bookings WHERE short_id = short_id) THEN - RETURN short_id; - END IF; - - attempts := attempts + 1; - IF attempts >= max_attempts THEN - RAISE EXCEPTION 'Failed to generate unique short_id after % attempts', max_attempts; - END IF; - END LOOP; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- ============================================ --- INVITATION CODE GENERATOR --- ============================================ - -CREATE OR REPLACE FUNCTION generate_invitation_code() -RETURNS VARCHAR(10) AS $$ -DECLARE - chars VARCHAR(36) := '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - code VARCHAR(10); - attempts INT := 0; - max_attempts INT := 10; -BEGIN - LOOP - code := ''; - FOR i IN 1..10 LOOP - code := code || substr(chars, floor(random() * 36 + 1)::INT, 1); - END LOOP; - - IF NOT EXISTS (SELECT 1 FROM invitations WHERE code = code) THEN - RETURN code; - END IF; - - attempts := attempts + 1; - IF attempts >= max_attempts THEN - RAISE EXCEPTION 'Failed to generate unique invitation code after % attempts', max_attempts; - END IF; - END LOOP; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- ============================================ --- WEEKLY INVITATION RESET --- ============================================ - -CREATE OR REPLACE FUNCTION get_week_start(date_param DATE DEFAULT CURRENT_DATE) -RETURNS DATE AS $$ -BEGIN - RETURN date_param - (EXTRACT(ISODOW FROM date_param)::INT - 1); -END; -$$ LANGUAGE plpgsql IMMUTABLE; - -CREATE OR REPLACE FUNCTION reset_weekly_invitations_for_customer(customer_uuid UUID) -RETURNS INTEGER AS $$ -DECLARE - week_start DATE; - invitations_remaining INTEGER := 5; - invitations_created INTEGER := 0; -BEGIN - week_start := get_week_start(CURRENT_DATE); - - -- Verificar si ya existen invitaciones para esta semana - SELECT COUNT(*) INTO invitations_created - FROM invitations - WHERE inviter_id = customer_uuid - AND week_start_date = week_start; - - -- Si no hay invitaciones para esta semana, crear las 5 nuevas - IF invitations_created = 0 THEN - INSERT INTO invitations (inviter_id, code, week_start_date, expiry_date, status) - SELECT - customer_uuid, - generate_invitation_code(), - week_start, - week_start + INTERVAL '6 days', - 'pending' - FROM generate_series(1, 5); - - invitations_created := 5; - - -- Registrar en audit_logs - INSERT INTO audit_logs ( - entity_type, - entity_id, - action, - old_values, - new_values, - performed_by, - performed_by_role, - metadata - ) - VALUES ( - 'invitations', - customer_uuid, - 'reset_invitations', - '{"week_start": null}'::JSONB, - '{"week_start": "' || week_start || '", "count": 5}'::JSONB, - NULL, - 'system', - '{"reset_type": "weekly", "invitations_created": 5}'::JSONB - ); - END IF; - - RETURN invitations_created; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - -CREATE OR REPLACE FUNCTION reset_all_weekly_invitations() -RETURNS JSONB AS $$ -DECLARE - customers_count INTEGER := 0; - invitations_created INTEGER := 0; - result JSONB; - customer_record RECORD; -BEGIN - -- Resetear invitaciones solo para clientes Gold - FOR customer_record IN - SELECT id FROM customers WHERE tier = 'gold' AND is_active = true - LOOP - invitations_created := invitations_created + reset_weekly_invitations_for_customer(customer_record.id); - customers_count := customers_count + 1; - END LOOP; - - result := jsonb_build_object( - 'customers_processed', customers_count, - 'invitations_created', invitations_created, - 'executed_at', NOW()::TEXT - ); - - -- Registrar ejecución masiva - INSERT INTO audit_logs ( - entity_type, - entity_id, - action, - old_values, - new_values, - performed_by, - performed_by_role, - metadata - ) - VALUES ( - 'invitations', - uuid_generate_v4(), - 'reset_invitations', - '{}'::JSONB, - result, - NULL, - 'system', - '{"reset_type": "weekly_batch"}'::JSONB - ); - - RETURN result; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- ============================================ --- AUDIT LOG TRIGGER FUNCTION --- ============================================ - -CREATE OR REPLACE FUNCTION log_audit() -RETURNS TRIGGER AS $$ -DECLARE - current_user_role_val user_role; -BEGIN - -- Obtener rol del usuario actual - current_user_role_val := get_current_user_role(); - - -- Solo auditar tablas críticas - IF TG_TABLE_NAME IN ('bookings', 'customers', 'invitations', 'staff', 'services') THEN - IF TG_OP = 'INSERT' THEN - INSERT INTO audit_logs ( - entity_type, - entity_id, - action, - old_values, - new_values, - performed_by, - performed_by_role, - metadata - ) - VALUES ( - TG_TABLE_NAME, - NEW.id, - 'create', - NULL, - row_to_json(NEW)::JSONB, - auth.uid(), - current_user_role_val, - jsonb_build_object('operation', TG_OP, 'table_name', TG_TABLE_NAME) - ); - ELSIF TG_OP = 'UPDATE' THEN - -- Solo auditar si hubo cambios relevantes - IF NEW IS DISTINCT FROM OLD THEN - INSERT INTO audit_logs ( - entity_type, - entity_id, - action, - old_values, - new_values, - performed_by, - performed_by_role, - metadata - ) - VALUES ( - TG_TABLE_NAME, - NEW.id, - 'update', - row_to_json(OLD)::JSONB, - row_to_json(NEW)::JSONB, - auth.uid(), - current_user_role_val, - jsonb_build_object('operation', TG_OP, 'table_name', TG_TABLE_NAME) - ); - END IF; - ELSIF TG_OP = 'DELETE' THEN - INSERT INTO audit_logs ( - entity_type, - entity_id, - action, - old_values, - new_values, - performed_by, - performed_by_role, - metadata - ) - VALUES ( - TG_TABLE_NAME, - OLD.id, - 'delete', - row_to_json(OLD)::JSONB, - NULL, - auth.uid(), - current_user_role_val, - jsonb_build_object('operation', TG_OP, 'table_name', TG_TABLE_NAME) - ); - END IF; - END IF; - - IF TG_OP = 'DELETE' THEN - RETURN OLD; - ELSE - RETURN NEW; - END IF; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- ============================================ --- APPLY AUDIT LOG TRIGGERS --- ============================================ - -CREATE TRIGGER audit_bookings AFTER INSERT OR UPDATE OR DELETE ON bookings - FOR EACH ROW EXECUTE FUNCTION log_audit(); - -CREATE TRIGGER audit_customers AFTER INSERT OR UPDATE OR DELETE ON customers - FOR EACH ROW EXECUTE FUNCTION log_audit(); - -CREATE TRIGGER audit_invitations AFTER INSERT OR UPDATE OR DELETE ON invitations - FOR EACH ROW EXECUTE FUNCTION log_audit(); - -CREATE TRIGGER audit_staff AFTER INSERT OR UPDATE OR DELETE ON staff - FOR EACH ROW EXECUTE FUNCTION log_audit(); - -CREATE TRIGGER audit_services AFTER INSERT OR UPDATE OR DELETE ON services - FOR EACH ROW EXECUTE FUNCTION log_audit(); - --- ============================================ --- AUTOMATIC SHORT ID GENERATION FOR BOOKINGS --- ============================================ - -CREATE OR REPLACE FUNCTION generate_booking_short_id() -RETURNS TRIGGER AS $$ -BEGIN - IF NEW.short_id IS NULL OR NEW.short_id = '' THEN - NEW.short_id := generate_short_id(); - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER booking_generate_short_id BEFORE INSERT ON bookings - FOR EACH ROW EXECUTE FUNCTION generate_booking_short_id(); - --- ============================================ --- END OF MIGRATION 003 --- ============================================ \ No newline at end of file diff --git a/db/migrations/00_FULL_MIGRATION.sql b/db/migrations/00_FULL_MIGRATION.sql deleted file mode 100644 index c957f9c..0000000 --- a/db/migrations/00_FULL_MIGRATION.sql +++ /dev/null @@ -1,780 +0,0 @@ --- ============================================ --- SALONOS - FULL DATABASE MIGRATION --- Ejecutar TODO este archivo en Supabase SQL Editor --- URL: https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/sql --- ============================================ - --- ============================================ --- BEGIN MIGRATION 001: INITIAL SCHEMA --- ============================================ - --- Habilitar UUID extension -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- ENUMS -CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer'); -CREATE TYPE customer_tier AS ENUM ('free', 'gold'); -CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'cancelled', 'completed', 'no_show'); -CREATE TYPE invitation_status AS ENUM ('pending', 'used', 'expired'); -CREATE TYPE resource_type AS ENUM ('station', 'room', 'equipment'); -CREATE TYPE audit_action AS ENUM ('create', 'update', 'delete', 'reset_invitations', 'payment', 'status_change'); - --- LOCATIONS -CREATE TABLE locations ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - name VARCHAR(100) NOT NULL, - timezone VARCHAR(50) NOT NULL DEFAULT 'UTC', - address TEXT, - phone VARCHAR(20), - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- RESOURCES -CREATE TABLE resources ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE, - name VARCHAR(100) NOT NULL, - type resource_type NOT NULL, - capacity INTEGER DEFAULT 1, - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- STAFF -CREATE TABLE staff ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - user_id UUID NOT NULL, - location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE, - role user_role NOT NULL CHECK (role IN ('admin', 'manager', 'staff', 'artist')), - display_name VARCHAR(100) NOT NULL, - phone VARCHAR(20), - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW(), - UNIQUE(user_id, location_id) -); - --- SERVICES -CREATE TABLE services ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - name VARCHAR(100) NOT NULL, - description TEXT, - duration_minutes INTEGER NOT NULL CHECK (duration_minutes > 0), - base_price DECIMAL(10, 2) NOT NULL CHECK (base_price >= 0), - requires_dual_artist BOOLEAN DEFAULT false, - premium_fee_enabled BOOLEAN DEFAULT false, - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- CUSTOMERS -CREATE TABLE customers ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - user_id UUID UNIQUE, - first_name VARCHAR(100) NOT NULL, - last_name VARCHAR(100) NOT NULL, - email VARCHAR(255) UNIQUE NOT NULL, - phone VARCHAR(20), - tier customer_tier DEFAULT 'free', - notes TEXT, - total_spent DECIMAL(10, 2) DEFAULT 0, - total_visits INTEGER DEFAULT 0, - last_visit_date DATE, - is_active BOOLEAN DEFAULT true, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- INVITATIONS -CREATE TABLE invitations ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - inviter_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, - code VARCHAR(10) UNIQUE NOT NULL, - email VARCHAR(255), - status invitation_status DEFAULT 'pending', - week_start_date DATE NOT NULL, - expiry_date DATE NOT NULL, - used_at TIMESTAMPTZ, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- BOOKINGS -CREATE TABLE bookings ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - short_id VARCHAR(6) UNIQUE NOT NULL, - customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, - staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT, - secondary_artist_id UUID REFERENCES staff(id) ON DELETE SET NULL, - location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE, - resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE, - service_id UUID NOT NULL REFERENCES services(id) ON DELETE RESTRICT, - start_time_utc TIMESTAMPTZ NOT NULL, - end_time_utc TIMESTAMPTZ NOT NULL, - status booking_status DEFAULT 'pending', - deposit_amount DECIMAL(10, 2) DEFAULT 0, - total_amount DECIMAL(10, 2) NOT NULL, - is_paid BOOLEAN DEFAULT false, - payment_reference VARCHAR(50), - notes TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - updated_at TIMESTAMPTZ DEFAULT NOW() -); - --- AUDIT LOGS -CREATE TABLE audit_logs ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - entity_type VARCHAR(50) NOT NULL, - entity_id UUID NOT NULL, - action audit_action NOT NULL, - old_values JSONB, - new_values JSONB, - performed_by UUID, - performed_by_role user_role, - ip_address INET, - user_agent TEXT, - metadata JSONB, - created_at TIMESTAMPTZ DEFAULT NOW() -); - --- INDEXES -CREATE INDEX idx_locations_active ON locations(is_active); -CREATE INDEX idx_resources_location ON resources(location_id); -CREATE INDEX idx_resources_active ON resources(location_id, is_active); -CREATE INDEX idx_staff_user ON staff(user_id); -CREATE INDEX idx_staff_location ON staff(location_id); -CREATE INDEX idx_staff_role ON staff(location_id, role, is_active); -CREATE INDEX idx_services_active ON services(is_active); -CREATE INDEX idx_customers_tier ON customers(tier); -CREATE INDEX idx_customers_email ON customers(email); -CREATE INDEX idx_customers_active ON customers(is_active); -CREATE INDEX idx_invitations_inviter ON invitations(inviter_id); -CREATE INDEX idx_invitations_code ON invitations(code); -CREATE INDEX idx_invitations_week ON invitations(week_start_date, status); -CREATE INDEX idx_bookings_customer ON bookings(customer_id); -CREATE INDEX idx_bookings_staff ON bookings(staff_id); -CREATE INDEX idx_bookings_location ON bookings(location_id); -CREATE INDEX idx_bookings_resource ON bookings(resource_id); -CREATE INDEX idx_bookings_time ON bookings(start_time_utc, end_time_utc); -CREATE INDEX idx_bookings_status ON bookings(status); -CREATE INDEX idx_bookings_short_id ON bookings(short_id); -CREATE INDEX idx_audit_entity ON audit_logs(entity_type, entity_id); -CREATE INDEX idx_audit_action ON audit_logs(action, created_at); -CREATE INDEX idx_audit_performed ON audit_logs(performed_by); - --- UPDATED_AT TRIGGER FUNCTION -CREATE OR REPLACE FUNCTION update_updated_at() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- UPDATED_AT TRIGGERS -CREATE TRIGGER locations_updated_at BEFORE UPDATE ON locations - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -CREATE TRIGGER resources_updated_at BEFORE UPDATE ON resources - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -CREATE TRIGGER staff_updated_at BEFORE UPDATE ON staff - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -CREATE TRIGGER services_updated_at BEFORE UPDATE ON services - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -CREATE TRIGGER customers_updated_at BEFORE UPDATE ON customers - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -CREATE TRIGGER invitations_updated_at BEFORE UPDATE ON invitations - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - -CREATE TRIGGER bookings_updated_at BEFORE UPDATE ON bookings - FOR EACH ROW EXECUTE FUNCTION update_updated_at(); - --- CONSTRAINTS -ALTER TABLE bookings ADD CONSTRAINT check_booking_time - CHECK (end_time_utc > start_time_utc); - -ALTER TABLE bookings ADD CONSTRAINT check_secondary_artist_role - CHECK (secondary_artist_id IS NULL OR EXISTS ( - SELECT 1 FROM staff s - WHERE s.id = secondary_artist_id AND s.role = 'artist' - )); - -ALTER TABLE invitations ADD CONSTRAINT check_week_start_is_monday - CHECK (EXTRACT(ISODOW FROM week_start_date) = 1); - --- ============================================ --- BEGIN MIGRATION 002: RLS POLICIES --- ============================================ - --- HELPER FUNCTIONS -CREATE OR REPLACE FUNCTION get_current_user_role() -RETURNS user_role AS $$ - DECLARE - current_staff_role user_role; - current_user_id UUID := auth.uid(); - BEGIN - SELECT s.role INTO current_staff_role - FROM staff s - WHERE s.user_id = current_user_id - LIMIT 1; - - IF current_staff_role IS NOT NULL THEN - RETURN current_staff_role; - END IF; - - IF EXISTS (SELECT 1 FROM customers WHERE user_id = current_user_id) THEN - RETURN 'customer'; - END IF; - - RETURN NULL; - END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - -CREATE OR REPLACE FUNCTION is_staff_or_higher() -RETURNS BOOLEAN AS $$ - DECLARE - user_role user_role := get_current_user_role(); - BEGIN - RETURN user_role IN ('admin', 'manager', 'staff'); - END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - -CREATE OR REPLACE FUNCTION is_artist() -RETURNS BOOLEAN AS $$ - DECLARE - user_role user_role := get_current_user_role(); - BEGIN - RETURN user_role = 'artist'; - END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - -CREATE OR REPLACE FUNCTION is_customer() -RETURNS BOOLEAN AS $$ - DECLARE - user_role user_role := get_current_user_role(); - BEGIN - RETURN user_role = 'customer'; - END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - -CREATE OR REPLACE FUNCTION is_admin() -RETURNS BOOLEAN AS $$ - DECLARE - user_role user_role := get_current_user_role(); - BEGIN - RETURN user_role = 'admin'; - END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- ENABLE RLS ON ALL TABLES -ALTER TABLE locations ENABLE ROW LEVEL SECURITY; -ALTER TABLE resources ENABLE ROW LEVEL SECURITY; -ALTER TABLE staff ENABLE ROW LEVEL SECURITY; -ALTER TABLE services ENABLE ROW LEVEL SECURITY; -ALTER TABLE customers ENABLE ROW LEVEL SECURITY; -ALTER TABLE invitations ENABLE ROW LEVEL SECURITY; -ALTER TABLE bookings ENABLE ROW LEVEL SECURITY; -ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY; - --- LOCATIONS POLICIES -CREATE POLICY "locations_select_staff_higher" ON locations - FOR SELECT - USING (is_staff_or_higher() OR is_admin()); - -CREATE POLICY "locations_modify_admin_manager" ON locations - FOR ALL - USING (get_current_user_role() IN ('admin', 'manager')); - --- RESOURCES POLICIES -CREATE POLICY "resources_select_staff_higher" ON resources - FOR SELECT - USING (is_staff_or_higher() OR is_admin()); - -CREATE POLICY "resources_select_artist" ON resources - FOR SELECT - USING (is_artist()); - -CREATE POLICY "resources_modify_admin_manager" ON resources - FOR ALL - USING (get_current_user_role() IN ('admin', 'manager')); - --- STAFF POLICIES -CREATE POLICY "staff_select_admin_manager" ON staff - FOR SELECT - USING (get_current_user_role() IN ('admin', 'manager')); - -CREATE POLICY "staff_select_same_location" ON staff - FOR SELECT - USING ( - is_staff_or_higher() AND - EXISTS ( - SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = staff.location_id - ) - ); - -CREATE POLICY "staff_select_artist_view_artists" ON staff - FOR SELECT - USING ( - is_artist() AND - EXISTS ( - SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = staff.location_id - ) AND - staff.role = 'artist' - ); - -CREATE POLICY "staff_modify_admin_manager" ON staff - FOR ALL - USING (get_current_user_role() IN ('admin', 'manager')); - --- SERVICES POLICIES -CREATE POLICY "services_select_all" ON services - FOR SELECT - USING (is_active = true); - -CREATE POLICY "services_all_admin_manager" ON services - FOR ALL - USING (get_current_user_role() IN ('admin', 'manager')); - --- CUSTOMERS POLICIES (RESTRICTED FOR ARTISTS) -CREATE POLICY "customers_select_admin_manager" ON customers - FOR SELECT - USING (get_current_user_role() IN ('admin', 'manager')); - -CREATE POLICY "customers_select_staff" ON customers - FOR SELECT - USING (is_staff_or_higher()); - -CREATE POLICY "customers_select_artist_restricted" ON customers - FOR SELECT - USING (is_artist()); - -CREATE POLICY "customers_select_own" ON customers - FOR SELECT - USING (is_customer() AND user_id = auth.uid()); - -CREATE POLICY "customers_modify_admin_manager" ON customers - FOR ALL - USING (get_current_user_role() IN ('admin', 'manager')); - -CREATE POLICY "customers_modify_staff" ON customers - FOR ALL - USING (is_staff_or_higher()); - -CREATE POLICY "customers_update_own" ON customers - FOR UPDATE - USING (is_customer() AND user_id = auth.uid()); - --- INVITATIONS POLICIES -CREATE POLICY "invitations_select_admin_manager" ON invitations - FOR SELECT - USING (get_current_user_role() IN ('admin', 'manager')); - -CREATE POLICY "invitations_select_staff" ON invitations - FOR SELECT - USING (is_staff_or_higher()); - -CREATE POLICY "invitations_select_own" ON invitations - FOR SELECT - USING (is_customer() AND inviter_id = (SELECT id FROM customers WHERE user_id = auth.uid())); - -CREATE POLICY "invitations_modify_admin_manager" ON invitations - FOR ALL - USING (get_current_user_role() IN ('admin', 'manager')); - -CREATE POLICY "invitations_modify_staff" ON invitations - FOR ALL - USING (is_staff_or_higher()); - --- BOOKINGS POLICIES -CREATE POLICY "bookings_select_admin_manager" ON bookings - FOR SELECT - USING (get_current_user_role() IN ('admin', 'manager')); - -CREATE POLICY "bookings_select_staff_location" ON bookings - FOR SELECT - USING ( - is_staff_or_higher() AND - EXISTS ( - SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = bookings.location_id - ) - ); - -CREATE POLICY "bookings_select_artist_own" ON bookings - FOR SELECT - USING ( - is_artist() AND - (staff_id = (SELECT id FROM staff WHERE user_id = auth.uid()) OR - secondary_artist_id = (SELECT id FROM staff WHERE user_id = auth.uid())) - ); - -CREATE POLICY "bookings_select_own" ON bookings - FOR SELECT - USING (is_customer() AND customer_id = (SELECT id FROM customers WHERE user_id = auth.uid())); - -CREATE POLICY "bookings_modify_admin_manager" ON bookings - FOR ALL - USING (get_current_user_role() IN ('admin', 'manager')); - -CREATE POLICY "bookings_modify_staff_location" ON bookings - FOR ALL - USING ( - is_staff_or_higher() AND - EXISTS ( - SELECT 1 FROM staff s WHERE s.user_id = auth.uid() AND s.location_id = bookings.location_id - ) - ); - -CREATE POLICY "bookings_no_modify_artist" ON bookings - FOR ALL - USING (NOT is_artist()); - -CREATE POLICY "bookings_create_own" ON bookings - FOR INSERT - WITH CHECK ( - is_customer() AND - customer_id = (SELECT id FROM customers WHERE user_id = auth.uid()) - ); - -CREATE POLICY "bookings_update_own" ON bookings - FOR UPDATE - USING ( - is_customer() AND - customer_id = (SELECT id FROM customers WHERE user_id = auth.uid()) - ); - --- AUDIT LOGS POLICIES -CREATE POLICY "audit_logs_select_admin_manager" ON audit_logs - FOR SELECT - USING (get_current_user_role() IN ('admin', 'manager')); - -CREATE POLICY "audit_logs_select_staff_location" ON audit_logs - FOR SELECT - USING ( - is_staff_or_higher() AND - EXISTS ( - SELECT 1 FROM bookings b - JOIN staff s ON s.user_id = auth.uid() - WHERE b.id = audit_logs.entity_id - AND b.location_id = s.location_id - ) - ); - -CREATE POLICY "audit_logs_no_insert" ON audit_logs - FOR INSERT - WITH CHECK (false); - --- ============================================ --- BEGIN MIGRATION 003: AUDIT TRIGGERS --- ============================================ - --- SHORT ID GENERATOR -CREATE OR REPLACE FUNCTION generate_short_id() -RETURNS VARCHAR(6) AS $$ -DECLARE - chars VARCHAR(36) := '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - short_id VARCHAR(6); - attempts INT := 0; - max_attempts INT := 10; -BEGIN - LOOP - short_id := ''; - FOR i IN 1..6 LOOP - short_id := short_id || substr(chars, floor(random() * 36 + 1)::INT, 1); - END LOOP; - - IF NOT EXISTS (SELECT 1 FROM bookings WHERE short_id = short_id) THEN - RETURN short_id; - END IF; - - attempts := attempts + 1; - IF attempts >= max_attempts THEN - RAISE EXCEPTION 'Failed to generate unique short_id after % attempts', max_attempts; - END IF; - END LOOP; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- INVITATION CODE GENERATOR -CREATE OR REPLACE FUNCTION generate_invitation_code() -RETURNS VARCHAR(10) AS $$ -DECLARE - chars VARCHAR(36) := '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - code VARCHAR(10); - attempts INT := 0; - max_attempts INT := 10; -BEGIN - LOOP - code := ''; - FOR i IN 1..10 LOOP - code := code || substr(chars, floor(random() * 36 + 1)::INT, 1); - END LOOP; - - IF NOT EXISTS (SELECT 1 FROM invitations WHERE code = code) THEN - RETURN code; - END IF; - - attempts := attempts + 1; - IF attempts >= max_attempts THEN - RAISE EXCEPTION 'Failed to generate unique invitation code after % attempts', max_attempts; - END IF; - END LOOP; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- WEEK FUNCTIONS -CREATE OR REPLACE FUNCTION get_week_start(date_param DATE DEFAULT CURRENT_DATE) -RETURNS DATE AS $$ -BEGIN - RETURN date_param - (EXTRACT(ISODOW FROM date_param)::INT - 1); -END; -$$ LANGUAGE plpgsql IMMUTABLE; - --- WEEKLY INVITATION RESET -CREATE OR REPLACE FUNCTION reset_weekly_invitations_for_customer(customer_uuid UUID) -RETURNS INTEGER AS $$ -DECLARE - week_start DATE; - invitations_remaining INTEGER := 5; - invitations_created INTEGER := 0; -BEGIN - week_start := get_week_start(CURRENT_DATE); - - SELECT COUNT(*) INTO invitations_created - FROM invitations - WHERE inviter_id = customer_uuid - AND week_start_date = week_start; - - IF invitations_created = 0 THEN - INSERT INTO invitations (inviter_id, code, week_start_date, expiry_date, status) - SELECT - customer_uuid, - generate_invitation_code(), - week_start, - week_start + INTERVAL '6 days', - 'pending' - FROM generate_series(1, 5); - - invitations_created := 5; - - INSERT INTO audit_logs ( - entity_type, - entity_id, - action, - old_values, - new_values, - performed_by, - performed_by_role, - metadata - ) - VALUES ( - 'invitations', - customer_uuid, - 'reset_invitations', - '{"week_start": null}'::JSONB, - '{"week_start": "' || week_start || '", "count": 5}'::JSONB, - NULL, - 'system', - '{"reset_type": "weekly", "invitations_created": 5}'::JSONB - ); - END IF; - - RETURN invitations_created; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - -CREATE OR REPLACE FUNCTION reset_all_weekly_invitations() -RETURNS JSONB AS $$ -DECLARE - customers_count INTEGER := 0; - invitations_created INTEGER := 0; - result JSONB; -BEGIN - FOR customer_record IN - SELECT id FROM customers WHERE tier = 'gold' AND is_active = true - LOOP - invitations_created := invitations_created + reset_weekly_invitations_for_customer(customer_record.id); - customers_count := customers_count + 1; - END LOOP; - - result := jsonb_build_object( - 'customers_processed', customers_count, - 'invitations_created', invitations_created, - 'executed_at', NOW()::TEXT - ); - - INSERT INTO audit_logs ( - entity_type, - entity_id, - action, - old_values, - new_values, - performed_by, - performed_by_role, - metadata - ) - VALUES ( - 'invitations', - uuid_generate_v4(), - 'reset_invitations', - '{}'::JSONB, - result, - NULL, - 'system', - '{"reset_type": "weekly_batch"}'::JSONB - ); - - RETURN result; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- AUDIT LOG TRIGGER FUNCTION -CREATE OR REPLACE FUNCTION log_audit() -RETURNS TRIGGER AS $$ -DECLARE - current_user_role_val user_role; -BEGIN - current_user_role_val := get_current_user_role(); - - IF TG_TABLE_NAME IN ('bookings', 'customers', 'invitations', 'staff', 'services') THEN - IF TG_OP = 'INSERT' THEN - INSERT INTO audit_logs ( - entity_type, - entity_id, - action, - old_values, - new_values, - performed_by, - performed_by_role, - metadata - ) - VALUES ( - TG_TABLE_NAME, - NEW.id, - 'create', - NULL, - row_to_json(NEW)::JSONB, - auth.uid(), - current_user_role_val, - jsonb_build_object('operation', TG_OP, 'table_name', TG_TABLE_NAME) - ); - ELSIF TG_OP = 'UPDATE' THEN - IF NEW IS DISTINCT FROM OLD THEN - INSERT INTO audit_logs ( - entity_type, - entity_id, - action, - old_values, - new_values, - performed_by, - performed_by_role, - metadata - ) - VALUES ( - TG_TABLE_NAME, - NEW.id, - 'update', - row_to_json(OLD)::JSONB, - row_to_json(NEW)::JSONB, - auth.uid(), - current_user_role_val, - jsonb_build_object('operation', TG_OP, 'table_name', TG_TABLE_NAME) - ); - END IF; - ELSIF TG_OP = 'DELETE' THEN - INSERT INTO audit_logs ( - entity_type, - entity_id, - action, - old_values, - new_values, - performed_by, - performed_by_role, - metadata - ) - VALUES ( - TG_TABLE_NAME, - OLD.id, - 'delete', - row_to_json(OLD)::JSONB, - NULL, - auth.uid(), - current_user_role_val, - jsonb_build_object('operation', TG_OP, 'table_name', TG_TABLE_NAME) - ); - END IF; - END IF; - - IF TG_OP = 'DELETE' THEN - RETURN OLD; - ELSE - RETURN NEW; - END IF; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- APPLY AUDIT LOG TRIGGERS -CREATE TRIGGER audit_bookings AFTER INSERT OR UPDATE OR DELETE ON bookings - FOR EACH ROW EXECUTE FUNCTION log_audit(); - -CREATE TRIGGER audit_customers AFTER INSERT OR UPDATE OR DELETE ON customers - FOR EACH ROW EXECUTE FUNCTION log_audit(); - -CREATE TRIGGER audit_invitations AFTER INSERT OR UPDATE OR DELETE ON invitations - FOR EACH ROW EXECUTE FUNCTION log_audit(); - -CREATE TRIGGER audit_staff AFTER INSERT OR UPDATE OR DELETE ON staff - FOR EACH ROW EXECUTE FUNCTION log_audit(); - -CREATE TRIGGER audit_services AFTER INSERT OR UPDATE OR DELETE ON services - FOR EACH ROW EXECUTE FUNCTION log_audit(); - --- AUTOMATIC SHORT ID GENERATION FOR BOOKINGS -CREATE OR REPLACE FUNCTION generate_booking_short_id() -RETURNS TRIGGER AS $$ -BEGIN - IF NEW.short_id IS NULL OR NEW.short_id = '' THEN - NEW.short_id := generate_short_id(); - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER booking_generate_short_id BEFORE INSERT ON bookings - FOR EACH ROW EXECUTE FUNCTION generate_booking_short_id(); - --- ============================================ --- VERIFICATION --- ============================================ - -DO $$ -BEGIN - RAISE NOTICE '==========================================='; - RAISE NOTICE 'SALONOS - DATABASE MIGRATION COMPLETED'; - RAISE NOTICE '==========================================='; - RAISE NOTICE '✅ Tables created: 8'; - RAISE NOTICE '✅ Functions created: 13'; - RAISE NOTICE '✅ Triggers active: 15+'; - RAISE NOTICE '✅ RLS policies configured: 20+'; - RAISE NOTICE '✅ ENUM types created: 6'; - RAISE NOTICE '==========================================='; - RAISE NOTICE 'NEXT STEPS:'; - RAISE NOTICE '1. Configure Auth in Supabase Dashboard'; - RAISE NOTICE '2. Create test users with specific roles'; - RAISE NOTICE '3. Test Short ID generation:'; - RAISE NOTICE ' SELECT generate_short_id();'; - RAISE NOTICE '4. Test invitation code generation:'; - RAISE NOTICE ' SELECT generate_invitation_code();'; - RAISE NOTICE '5. Verify tables:'; - RAISE NOTICE ' SELECT table_name FROM information_schema.tables'; - RAISE NOTICE ' WHERE table_schema = ''public'' ORDER BY table_name;'; - RAISE NOTICE '==========================================='; -END -$$; diff --git a/db/migrations/README.md b/db/migrations/README.md deleted file mode 100644 index 72cdf69..0000000 --- a/db/migrations/README.md +++ /dev/null @@ -1,127 +0,0 @@ -# SalonOS - Database Migrations - -Este directorio contiene todas las migraciones de base de datos para Supabase. - -## Orden de Ejecución - -Las migraciones deben ejecutarse en orden numérico: - -1. **001_initial_schema.sql** - - Crea todas las tablas del sistema - - Define tipos ENUM (roles, tiers, estados) - - Crea índices y constraints - - Implementa el sistema "Doble Capa" (Staff + Recurso) - -2. **002_rls_policies.sql** - - Habilita Row Level Security - - Define políticas de acceso por rol - - **Restricción crítica**: Artist solo ve nombre+notas de customers - - Jerarquía de roles: Admin > Manager > Staff > Artist > Customer - -3. **003_audit_triggers.sql** - - Generador de Short ID (6 caracteres, collision-safe) - - Funciones de reset semanal de invitaciones - - Triggers de auditoría automática - - Generación automática de invitation codes - -## Ejecución Manual - -### Vía Supabase Dashboard - -1. Ir a SQL Editor -2. Copiar y ejecutar cada migración en orden -3. Verificar que no haya errores - -### Vía CLI - -```bash -# Instalar Supabase CLI si no está instalado -npm install -g supabase - -# Login -supabase login - -# Ejecutar migración -supabase db push --db-url="postgresql://user:pass@host:port/db" - -# O para ejecutar archivo específico -psql $DATABASE_URL -f db/migrations/001_initial_schema.sql -``` - -## Notas Importantes - -### UTC-First -Todos los timestamps se almacenan en UTC. La conversión a zona horaria local ocurre solo en: -- Frontend (The Boutique / The HQ) -- Notificaciones (WhatsApp / Email) - -### Sistema Doble Capa -El sistema valida disponibilidad en dos niveles: -1. **Staff/Artist**: Horario laboral + Google Calendar -2. **Recurso**: Disponibilidad de estación física - -### Reset Semanal de Invitaciones -- Ejecutado automáticamente cada Lunes 00:00 UTC -- Solo para clientes Tier Gold -- Cada cliente recibe 5 invitaciones nuevas -- Proceso idempotente y auditado - -### Privacidad de Datos -- **Artist**: NO puede ver `email` ni `phone` de customers -- **Staff/Manager/Admin**: Pueden ver PII de customers -- Todas las consultas de Artist a `customers` están filtradas por RLS - -## Verificación de Migraciones - -```sql --- Verificar tablas creadas -SELECT table_name FROM information_schema.tables -WHERE table_schema = 'public' -ORDER BY table_name; - --- Verificar funciones creadas -SELECT routine_name FROM information_schema.routines -WHERE routine_schema = 'public' -ORDER BY routine_name; - --- Verificar triggers activos -SELECT trigger_name, event_object_table -FROM information_schema.triggers -WHERE trigger_schema = 'public' -ORDER BY event_object_table, trigger_name; - --- Verificar políticas RLS -SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check -FROM pg_policies -WHERE schemaname = 'public' -ORDER BY tablename, policyname; -``` - -## Troubleshooting - -### Error: "relation already exists" -Una tabla ya existe. Verificar si la migración anterior falló parcialmente. - -### Error: "must be owner of table" -Necesitas permisos de superusuario o owner de la tabla. - -### Error: RLS no funciona -Verificar que: -1. RLS está habilitado en la tabla (`ALTER TABLE table_name ENABLE ROW LEVEL SECURITY`) -2. El usuario tiene un rol asignado en `staff` o `customers` -3. Las políticas están correctamente definidas - -## Próximos Migraciones - -Las futuras migraciones incluirán: -- Integración con Stripe (webhook processing tables) -- Integración con Google Calendar (sync tables) -- Notificaciones WhatsApp (queue tables) -- Storage buckets para The Vault - -## Contacto - -Para dudas sobre las migraciones, consultar: -- PRD.md: Reglas de negocio -- TASKS.md: Plan de ejecución -- AGENTS.md: Roles y responsabilidades diff --git a/db/migrations/full_migration.sql b/db/migrations/full_migration.sql deleted file mode 100644 index 400b499..0000000 --- a/db/migrations/full_migration.sql +++ /dev/null @@ -1,114 +0,0 @@ --- ============================================ --- SALONOS - FULL DATABASE MIGRATION --- ============================================ --- Ejecuta todas las migraciones en orden --- Fecha: 2026-01-15 --- ============================================ - --- Ejecutar cada migración en orden: --- 1. 001_initial_schema.sql --- 2. 002_rls_policies.sql --- 3. 003_audit_triggers.sql - --- Para ejecutar desde psql: --- psql $DATABASE_URL -f db/migrations/001_initial_schema.sql --- psql $DATABASE_URL -f db/migrations/002_rls_policies.sql --- psql $DATABASE_URL -f db/migrations/003_audit_triggers.sql - --- O ejecutar este archivo completo: --- psql $DATABASE_URL -f db/migrations/full_migration.sql - --- ============================================ --- BEGIN MIGRATION 001 --- ============================================ -\i db/migrations/001_initial_schema.sql - --- ============================================ --- BEGIN MIGRATION 002 --- ============================================ -\i db/migrations/002_rls_policies.sql - --- ============================================ --- BEGIN MIGRATION 003 --- ============================================ -\i db/migrations/003_audit_triggers.sql - --- ============================================ --- VERIFICATION QUERIES --- ============================================ - --- Verificar tablas creadas -DO $$ -DECLARE - table_count INTEGER; -BEGIN - SELECT COUNT(*) INTO table_count - FROM information_schema.tables - WHERE table_schema = 'public' - AND table_name IN ('locations', 'resources', 'staff', 'services', 'customers', 'invitations', 'bookings', 'audit_logs'); - - RAISE NOTICE '✅ Tablas creadas: % de 8 esperadas', table_count; -END -$$; - --- Verificar funciones creadas -DO $$ -DECLARE - func_count INTEGER; -BEGIN - SELECT COUNT(*) INTO func_count - FROM information_schema.routines - WHERE routine_schema = 'public' - AND routine_name IN ('generate_short_id', 'generate_invitation_code', 'reset_weekly_invitations_for_customer', 'reset_all_weekly_invitations', 'log_audit', 'get_current_user_role', 'is_staff_or_higher', 'is_artist', 'is_customer', 'is_admin', 'update_updated_at', 'generate_booking_short_id', 'get_week_start'); - - RAISE NOTICE '✅ Funciones creadas: % de 13 esperadas', func_count; -END -$$; - --- Verificar triggers activos -DO $$ -DECLARE - trigger_count INTEGER; -BEGIN - SELECT COUNT(*) INTO trigger_count - FROM information_schema.triggers - WHERE trigger_schema = 'public'; - - RAISE NOTICE '✅ Triggers activos: % (se esperan múltiples)', trigger_count; -END -$$; - --- Verificar políticas RLS -DO $$ -DECLARE - policy_count INTEGER; -BEGIN - SELECT COUNT(*) INTO policy_count - FROM pg_policies - WHERE schemaname = 'public'; - - RAISE NOTICE '✅ Políticas RLS: % (se esperan múltiples)', policy_count; -END -$$; - --- Verificar tipos ENUM -DO $$ -DECLARE - enum_count INTEGER; -BEGIN - SELECT COUNT(*) INTO enum_count - FROM pg_type - WHERE typtype = 'e' - AND typname IN ('user_role', 'customer_tier', 'booking_status', 'invitation_status', 'resource_type', 'audit_action'); - - RAISE NOTICE '✅ Tipos ENUM: % de 6 esperados', enum_count; -END -$$; - -RAISE NOTICE '==========================================='; -RAISE NOTICE '✅ MIGRACIÓN COMPLETADA EXITOSAMENTE'; -RAISE NOTICE '==========================================='; -RAISE NOTICE 'Verificar el esquema ejecutando:'; -RAISE NOTICE ' SELECT table_name FROM information_schema.tables WHERE table_schema = ''public'' ORDER BY table_name;'; -RAISE NOTICE ' SELECT routine_name FROM information_schema.routines WHERE routine_schema = ''public'' ORDER BY routine_name;'; -RAISE NOTICE '==========================================='; diff --git a/docs/00_FULL_MIGRATION_FINAL_README.md b/docs/00_FULL_MIGRATION_FINAL_README.md deleted file mode 100644 index 93cff98..0000000 --- a/docs/00_FULL_MIGRATION_FINAL_README.md +++ /dev/null @@ -1,148 +0,0 @@ -# 🎉 MIGRACIÓN FINAL - SalonOS - -## ✅ Estado: Listo para Ejecutar - -Este archivo contiene la **versión final corregida** de todas las migraciones de base de datos de SalonOS. - -## 🐛 Correcciones Aplicadas - -### 1. Constraint Reemplazado por Trigger -- **Problema:** PostgreSQL no permite subqueries en constraints CHECK -- **Solución:** Reemplazado por trigger de validación `validate_secondary_artist_role()` - -### 2. Variable de Loop Declarada -- **Problema:** Variable `customer_record` no declarada en función `reset_all_weekly_invitations()` -- **Solución:** Declarada como `customer_record RECORD;` en bloque `DECLARE` - -## 📋 Contenido del Archivo - -Este archivo incluye: - -- ✅ **Migración 001**: Esquema inicial (8 tablas, 6 tipos ENUM, índices, constraints, triggers) -- ✅ **Migración 002**: Políticas RLS (20+ políticas, 4 funciones auxiliares) -- ✅ **Migración 003**: Triggers de auditoría (13 funciones, triggers automáticos) -- ✅ **Corrección 1**: Trigger de validación en lugar de constraint con subquery -- ✅ **Corrección 2**: Variable de loop declarada correctamente - -## 🚀 Cómo Ejecutar - -### Paso 1: Abrir Supabase SQL Editor -``` -https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/sql -``` - -### Paso 2: Copiar el Archivo -Copia **TODO** el contenido de: -``` -db/migrations/00_FULL_MIGRATION_FINAL.sql -``` - -### Paso 3: Ejecutar -1. Pega el contenido en el SQL Editor -2. Haz clic en **"Run"** -3. Espera 10-30 segundos - -## 📊 Resultado Esperado - -Al completar la ejecución, deberías ver: - -``` -=========================================== -SALONOS - DATABASE MIGRATION COMPLETED -=========================================== -✅ Tables created: 8 -✅ Functions created: 14 -✅ Triggers active: 17+ -✅ RLS policies configured: 20+ -✅ ENUM types created: 6 -=========================================== -``` - -## 🔍 Verificación - -### Verificar Tablas -```sql -SELECT table_name -FROM information_schema.tables -WHERE table_schema = 'public' -ORDER BY table_name; -``` - -**Esperado:** 8 tablas - -### Verificar Funciones -```sql -SELECT routine_name -FROM information_schema.routines -WHERE routine_schema = 'public' -ORDER BY routine_name; -``` - -**Esperado:** 14 funciones - -### Verificar Triggers -```sql -SELECT trigger_name, event_object_table -FROM information_schema.triggers -WHERE trigger_schema = 'public' -ORDER BY event_object_table, trigger_name; -``` - -**Esperado:** 17+ triggers - -### Verificar Políticas RLS -```sql -SELECT schemaname, tablename, policyname -FROM pg_policies -WHERE schemaname = 'public' -ORDER BY tablename, policyname; -``` - -**Esperado:** 20+ políticas - -### Probar Short ID -```sql -SELECT generate_short_id(); -``` - -**Esperado:** String de 6 caracteres alfanuméricos (ej: "A3F7X2") - -### Probar Código de Invitación -```sql -SELECT generate_invitation_code(); -``` - -**Esperado:** String de 10 caracteres alfanuméricos (ej: "X9J4K2M5N8") - -## 🎯 Próximos Pasos - -Después de ejecutar exitosamente la migración: - -1. ✅ **Configurar Auth** en Supabase Dashboard -2. ✅ **Crear usuarios de prueba** con roles específicos -3. ✅ **Probar el sistema** con consultas de verificación -4. ✅ **Ejecutar seed de datos** (opcional): `npm run db:seed` -5. ✅ **Continuar desarrollo** de Tarea 1.3 y 1.4 - -## 📚 Documentación Adicional - -- **docs/MIGRATION_CORRECTION.md** - Detalle de las correcciones aplicadas -- **docs/SUPABASE_DASHBOARD_MIGRATION.md** - Guía completa de ejecución -- **docs/MIGRATION_GUIDE.md** - Guía técnica de migraciones -- **db/migrations/README.md** - Documentación técnica de migraciones -- **scripts/README.md** - Documentación de scripts de utilidad - -## 🆘 Soporte - -Si encuentras problemas: - -1. Revisa los logs de Supabase Dashboard -2. Ejecuta las consultas de verificación arriba -3. Consulta `docs/MIGRATION_CORRECTION.md` para detalles de las correcciones -4. Consulta `docs/SUPABASE_DASHBOARD_MIGRATION.md` para guía paso a paso - ---- - -**Última actualización:** 2026-01-15 -**Versión:** FINAL (Correcciones aplicadas) -**Estado:** ✅ Listo para producción diff --git a/docs/MIGRATION_CORRECTION.md b/docs/MIGRATION_CORRECTION.md deleted file mode 100644 index 7078de2..0000000 --- a/docs/MIGRATION_CORRECTION.md +++ /dev/null @@ -1,314 +0,0 @@ -# ✅ CORRECCIÓN DE MIGRACIÓN - PostgreSQL Constraint Error - -## 🐛 Problemas Detectados - -### Problema 1: Subquery en CHECK Constraint - -Al ejecutar la migración en Supabase Dashboard, se produjo el siguiente error: - -``` -Error: Failed to run sql query: ERROR: 0A000: cannot use subquery in check constraint -``` - -**Causa:** PostgreSQL **no permite subqueries** en los constraints de CHECK. - -### Problema 2: Variable no declarada en Loop - -``` -Error: Failed to run sql query: ERROR: 42601: loop variable of loop over rows must be a record variable or list of scalar variables -``` - -**Causa:** La variable `customer_record` no estaba declarada en el bloque `DECLARE` de la función `reset_all_weekly_invitations()`. - -## 🔍 Causas Detalladas - -### Problema 1: Subquery en CHECK Constraint - -En la migración original, teníamos: - -```sql --- Constraint problemático (NO permitido en PostgreSQL) -ALTER TABLE bookings ADD CONSTRAINT check_secondary_artist_role - CHECK (secondary_artist_id IS NULL OR EXISTS ( - SELECT 1 FROM staff s - WHERE s.id = secondary_artist_id AND s.role = 'artist' - )); -``` - -## ✅ Soluciones Aplicadas - -### Solución 1: Reemplazar Constraint con Trigger - -Se ha reemplazado el constraint problemático con un **trigger de validación** que hace exactamente la misma validación: - -```sql --- Nueva función de validación (PERMITIDO en PostgreSQL) -CREATE OR REPLACE FUNCTION validate_secondary_artist_role() -RETURNS TRIGGER AS $$ -BEGIN - IF NEW.secondary_artist_id IS NOT NULL THEN - IF NOT EXISTS ( - SELECT 1 FROM staff s - WHERE s.id = NEW.secondary_artist_id AND s.role = 'artist' AND s.is_active = true - ) THEN - RAISE EXCEPTION 'secondary_artist_id must reference an active staff member with role ''artist'''; - END IF; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -CREATE TRIGGER validate_booking_secondary_artist BEFORE INSERT OR UPDATE ON bookings - FOR EACH ROW EXECUTE FUNCTION validate_secondary_artist_role(); -``` - -### Solución 2: Declarar Variable en Bloque DECLARE - -Se ha añadido la declaración de `customer_record RECORD;` en el bloque `DECLARE` de la función `reset_all_weekly_invitations()`: - -**Antes:** -```sql -CREATE OR REPLACE FUNCTION reset_all_weekly_invitations() -RETURNS JSONB AS $$ -DECLARE - customers_count INTEGER := 0; - invitations_created INTEGER := 0; - result JSONB; -BEGIN - FOR customer_record IN -- ❌ Variable no declarada - SELECT id FROM customers WHERE tier = 'gold' AND is_active = true - LOOP - ... - END LOOP; -``` - -**Después:** -```sql -CREATE OR REPLACE FUNCTION reset_all_weekly_invitations() -RETURNS JSONB AS $$ -DECLARE - customers_count INTEGER := 0; - invitations_created INTEGER := 0; - result JSONB; - customer_record RECORD; -- ✅ Variable declarada -BEGIN - FOR customer_record IN -- ✅ Ahora la variable existe - SELECT id FROM customers WHERE tier = 'gold' AND is_active = true - LOOP - ... - END LOOP; -``` - -## 📁 Archivos Actualizados - -### Archivos Corregidos: -1. **`db/migrations/00_FULL_MIGRATION_CORRECTED.sql`** (NUEVO) - - Archivo consolidado con todas las migraciones - - Constraint reemplazado por trigger de validación - - Índice adicional para `secondary_artist_id` - -2. **`db/migrations/001_initial_schema.sql`** (ACTUALIZADO) - - Constraint problemático eliminado - - Trigger de validación añadido - - Índice para `secondary_artist_id` añadido - -3. **`docs/SUPABASE_DASHBOARD_MIGRATION.md`** (ACTUALIZADO) - - Referencias actualizadas al archivo corregido - - Documentación actualizada con el nuevo trigger - -## 🎯 Cómo Proceder - -### Paso 1: Usar el Archivo Corregido - -Copia **TODO** el contenido de: -``` -db/migrations/00_FULL_MIGRATION_CORRECTED.sql -``` - -### Paso 2: Ejecutar en Supabase Dashboard - -1. Ve a: **https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/sql** -2. Crea un nuevo query -3. Pega el contenido completo del archivo corregido -4. Haz clic en **"Run"** - -### Paso 3: Verificar la Ejecución - -Al finalizar, deberías ver: -- ✅ Un mensaje de éxito -- ✅ 8 tablas creadas -- ✅ 14 funciones creadas (incluye `validate_secondary_artist_role`) -- ✅ 17+ triggers activos (incluye `validate_booking_secondary_artist`) -- ✅ 20+ políticas RLS configuradas -- ✅ 6 tipos ENUM creados - -## 🔍 Verificación del Trigger - -Para verificar que el trigger de validación funciona correctamente, ejecuta: - -```sql --- Verificar que el trigger existe -SELECT trigger_name, event_object_table, action_statement -FROM information_schema.triggers -WHERE trigger_name = 'validate_booking_secondary_artist'; - --- Verificar la función de validación -SELECT routine_name, routine_definition -FROM information_schema.routines -WHERE routine_name = 'validate_secondary_artist_role'; -``` - -## 🧪 Probar la Validación - -### Prueba 1: Booking válido con secondary_artist válido - -```sql --- Primero crear datos de prueba -INSERT INTO locations (name, timezone, is_active) -VALUES ('Test Location', 'America/Mexico_City', true); - -INSERT INTO staff (user_id, location_id, role, display_name, is_active) -VALUES (uuid_generate_v4(), (SELECT id FROM locations LIMIT 1), 'artist', 'Test Artist', true); - -INSERT INTO staff (user_id, location_id, role, display_name, is_active) -VALUES (uuid_generate_v4(), (SELECT id FROM locations LIMIT 1), 'staff', 'Test Staff', true); - -INSERT INTO resources (location_id, name, type, is_active) -VALUES ((SELECT id FROM locations LIMIT 1), 'Test Station', 'station', true); - -INSERT INTO services (name, duration_minutes, base_price, is_active) -VALUES ('Test Service', 60, 100.00, true); - -INSERT INTO customers (user_id, first_name, last_name, email, tier, is_active) -VALUES (uuid_generate_v4(), 'Test', 'Customer', 'test@example.com', 'free', true); - --- Ahora intentar crear un booking válido -INSERT INTO bookings ( - customer_id, - staff_id, - secondary_artist_id, - location_id, - resource_id, - service_id, - start_time_utc, - end_time_utc, - status, - deposit_amount, - total_amount, - is_paid -) -SELECT - (SELECT id FROM customers LIMIT 1), - (SELECT id FROM staff WHERE role = 'staff' LIMIT 1), - (SELECT id FROM staff WHERE role = 'artist' LIMIT 1), - (SELECT id FROM locations LIMIT 1), - (SELECT id FROM resources LIMIT 1), - (SELECT id FROM services LIMIT 1), - NOW() + INTERVAL '1 day', - NOW() + INTERVAL '2 days', - 'confirmed', - 50.00, - 100.00, - true; -``` - -**Resultado esperado:** ✅ Booking creado exitosamente - -### Prueba 2: Booking inválido con secondary_artist no válido - -```sql --- Intentar crear un booking con secondary_artist que no es 'artist' -INSERT INTO bookings ( - customer_id, - staff_id, - secondary_artist_id, - location_id, - resource_id, - service_id, - start_time_utc, - end_time_utc, - status, - deposit_amount, - total_amount, - is_paid -) -SELECT - (SELECT id FROM customers LIMIT 1), - (SELECT id FROM staff WHERE role = 'staff' LIMIT 1), - (SELECT id FROM staff WHERE role = 'staff' LIMIT 1), -- ❌ Esto es 'staff', no 'artist' - (SELECT id FROM locations LIMIT 1), - (SELECT id FROM resources LIMIT 1), - (SELECT id FROM services LIMIT 1), - NOW() + INTERVAL '1 day', - NOW() + INTERVAL '2 days', - 'confirmed', - 50.00, - 100.00, - true; -``` - -**Resultado esperado:** ❌ Error: `secondary_artist_id must reference an active staff member with role 'artist'` - -### Prueba 3: Booking sin secondary_artist - -```sql --- Crear un booking sin secondary_artist (debe ser válido) -INSERT INTO bookings ( - customer_id, - staff_id, - location_id, - resource_id, - service_id, - start_time_utc, - end_time_utc, - status, - deposit_amount, - total_amount, - is_paid -) -SELECT - (SELECT id FROM customers LIMIT 1), - (SELECT id FROM staff WHERE role = 'staff' LIMIT 1), - (SELECT id FROM locations LIMIT 1), - (SELECT id FROM resources LIMIT 1), - (SELECT id FROM services LIMIT 1), - NOW() + INTERVAL '1 day', - NOW() + INTERVAL '2 days', - 'confirmed', - 50.00, - 100.00, - true; -``` - -**Resultado esperado:** ✅ Booking creado exitosamente (secondary_artist es opcional) - -## 📊 Resumen de Cambios - -| Elemento | Antes | Después | -|----------|--------|---------| -| **Constraint** | `check_secondary_artist_role` con subquery | Eliminado | -| **Trigger** | No existía | `validate_booking_secondary_artist` añadido | -| **Función** | No existía | `validate_secondary_artist_role()` añadida | -| **Índice** | No existía para `secondary_artist_id` | `idx_bookings_secondary_artist` añadido | -| **Loop variable** | No declarada en `reset_all_weekly_invitations()` | `customer_record RECORD;` añadido | -| **Total funciones** | 13 | 14 | -| **Total triggers** | 15+ | 17+ | - -## 🎓 Lecciones Aprendidas - -1. **PostgreSQL no permite subqueries** en constraints CHECK -2. **Los triggers de validación** son la alternativa correcta para validaciones complejas -3. **Los loops en PL/pgSQL** requieren declarar las variables en el bloque `DECLARE` -4. **Los triggers** pueden hacer validaciones que no son posibles con constraints -5. **La documentación** debe mantenerse sincronizada con el código SQL -6. **La sintaxis de PostgreSQL** es estricta y requiere declaraciones explícitas - -## 📚 Referencias - -- [PostgreSQL Constraints Documentation](https://www.postgresql.org/docs/current/ddl-constraints.html) -- [PostgreSQL Triggers Documentation](https://www.postgresql.org/docs/current/sql-createtrigger.html) -- [PostgreSQL ERROR: 0A000](https://www.postgresql.org/docs/current/errcodes-appendix.html) - ---- - -**¿Necesitas ayuda adicional?** Una vez que hayas ejecutado la migración corregida, avísame para verificar que todo esté funcionando correctamente. diff --git a/docs/MIGRATION_GUIDE.md b/docs/MIGRATION_GUIDE.md deleted file mode 100644 index d4cdc3d..0000000 --- a/docs/MIGRATION_GUIDE.md +++ /dev/null @@ -1,267 +0,0 @@ -# 🚀 Guía de Ejecución de Migraciones - SalonOS - -Esta guía explica cómo ejecutar las migraciones de base de datos en Supabase. - -## ⚠️ Requisitos Previos - -1. **Cuenta de Supabase** con un proyecto creado -2. **PostgreSQL client (psql)** instalado en tu máquina -3. **Variables de entorno** configuradas en `.env.local` - -## 📋 Paso 1: Configurar Variables de Entorno - -Copia el archivo `.env.example` a `.env.local`: - -```bash -cp .env.example .env.local -``` - -Edita el archivo `.env.local` con tus credenciales de Supabase: - -```bash -# Supabase -NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here -SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here -``` - -### Cómo obtener las credenciales de Supabase - -1. Ve a [Supabase Dashboard](https://supabase.com/dashboard) -2. Selecciona tu proyecto -3. Ve a **Settings → API** -4. Copia: - - **Project URL** → `NEXT_PUBLIC_SUPABASE_URL` - - **anon public** → `NEXT_PUBLIC_SUPABASE_ANON_KEY` - - **service_role** → `SUPABASE_SERVICE_ROLE_KEY` - -## 🎯 Paso 2: Ejecutar Migraciones - -### Opción A: Automática (Recomendada) - -Usa el script de migración automatizado: - -```bash -# Dar permisos de ejecución al script -chmod +x db/migrate.sh - -# Ejecutar el script -./db/migrate.sh -``` - -### Opción B: Manual con psql - -Si prefieres ejecutar las migraciones manualmente: - -```bash -# Exportar DATABASE_URL -export DATABASE_URL="postgresql://postgres:[PASSWORD]@[PROJECT-ID].supabase.co:5432/postgres" - -# Ejecutar cada migración en orden -psql $DATABASE_URL -f db/migrations/001_initial_schema.sql -psql $DATABASE_URL -f db/migrations/002_rls_policies.sql -psql $DATABASE_URL -f db/migrations/003_audit_triggers.sql -``` - -### Opción C: Vía Supabase Dashboard - -1. Ve a [Supabase Dashboard → SQL Editor](https://supabase.com/dashboard/project/[PROJECT-ID]/sql) -2. Copia el contenido de cada migración en orden -3. Ejecuta `001_initial_schema.sql` primero -4. Luego `002_rls_policies.sql` -5. Finalmente `003_audit_triggers.sql` - -## ✅ Paso 3: Verificar la Instalación - -Ejecuta estas consultas para verificar que todo esté correcto: - -### Verificar Tablas - -```sql -SELECT table_name -FROM information_schema.tables -WHERE table_schema = 'public' -ORDER BY table_name; -``` - -**Esperado:** 8 tablas (locations, resources, staff, services, customers, invitations, bookings, audit_logs) - -### Verificar Funciones - -```sql -SELECT routine_name -FROM information_schema.routines -WHERE routine_schema = 'public' -ORDER BY routine_name; -``` - -**Esperado:** 13 funciones incluyendo `generate_short_id`, `reset_weekly_invitations_for_customer`, etc. - -### Verificar Triggers - -```sql -SELECT trigger_name, event_object_table -FROM information_schema.triggers -WHERE trigger_schema = 'public' -ORDER BY event_object_table, trigger_name; -``` - -**Esperado:** Múltiples triggers para auditoría y timestamps - -### Verificar Políticas RLS - -```sql -SELECT schemaname, tablename, policyname, permissive, roles, cmd -FROM pg_policies -WHERE schemaname = 'public' -ORDER BY tablename, policyname; -``` - -**Esperado:** Múltiples políticas por rol (admin, manager, staff, artist, customer) - -### Verificar Tipos ENUM - -```sql -SELECT typname, enumlabel -FROM pg_enum e -JOIN pg_type t ON e.enumtypid = t.oid -WHERE t.typtype = 'e' -ORDER BY t.typname, e.enumsortorder; -``` - -**Esperado:** 6 tipos ENUM (user_role, customer_tier, booking_status, invitation_status, resource_type, audit_action) - -## 🔍 Paso 4: Probar Funcionalidad - -### Generar Short ID - -```sql -SELECT generate_short_id(); -``` - -**Esperado:** Un string de 6 caracteres alfanuméricos (ej: "A3F7X2") - -### Generar Código de Invitación - -```sql -SELECT generate_invitation_code(); -``` - -**Esperado:** Un string de 10 caracteres alfanuméricos (ej: "X9J4K2M5N8") - -### Obtener Inicio de Semana - -```sql -SELECT get_week_start(CURRENT_DATE); -``` - -**Esperado:** El lunes de la semana actual - -### Resetear Invitaciones de un Cliente - -```sql --- Primero necesitas un cliente Gold en la base de datos --- Esto creará 5 invitaciones nuevas para la semana actual -SELECT reset_weekly_invitations_for_customer('[CUSTOMER_UUID]'); -``` - -## 🚨 Solución de Problemas - -### Error: "FATAL: password authentication failed" - -**Causa:** La contraseña en DATABASE_URL es incorrecta. - -**Solución:** Verifica que estés usando el `SUPABASE_SERVICE_ROLE_KEY` como contraseña en la URL de conexión. - -### Error: "relation already exists" - -**Causa:** Una tabla ya existe. La migración anterior puede haber fallado parcialmente. - -**Solución:** Elimina las tablas existentes o ejecuta una limpieza completa: - -```sql -DROP TABLE IF EXISTS audit_logs CASCADE; -DROP TABLE IF EXISTS bookings CASCADE; -DROP TABLE IF EXISTS invitations CASCADE; -DROP TABLE IF EXISTS customers CASCADE; -DROP TABLE IF EXISTS services CASCADE; -DROP TABLE IF EXISTS staff CASCADE; -DROP TABLE IF EXISTS resources CASCADE; -DROP TABLE IF EXISTS locations CASCADE; - -DROP FUNCTION IF EXISTS generate_short_id(); -DROP FUNCTION IF EXISTS generate_invitation_code(); -DROP FUNCTION IF EXISTS reset_weekly_invitations_for_customer(UUID); -DROP FUNCTION IF EXISTS reset_all_weekly_invitations(); -DROP FUNCTION IF EXISTS log_audit(); -DROP FUNCTION IF EXISTS get_current_user_role(); -DROP FUNCTION IF EXISTS is_staff_or_higher(); -DROP FUNCTION IF EXISTS is_artist(); -DROP FUNCTION IF EXISTS is_customer(); -DROP FUNCTION IF EXISTS is_admin(); -DROP FUNCTION IF EXISTS update_updated_at(); -DROP FUNCTION IF EXISTS generate_booking_short_id(); -DROP FUNCTION IF EXISTS get_week_start(DATE); -``` - -### Error: "must be owner of table" - -**Causa:** No tienes permisos de superusuario o owner de la tabla. - -**Solución:** Asegúrate de estar usando el `SUPABASE_SERVICE_ROLE_KEY` (no el anon key). - -### Error: RLS no funciona - -**Causa:** RLS no está habilitado o el usuario no tiene un rol asignado. - -**Solución:** -1. Verifica que RLS está habilitado: `SELECT tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public';` -2. Verifica que el usuario tenga un registro en `staff` o `customers` -3. Verifica las políticas RLS: `SELECT * FROM pg_policies WHERE schemaname = 'public';` - -## 📚 Documentación Adicional - -- **PRD.md:** Reglas de negocio del sistema -- **TASKS.md:** Plan de ejecución por fases -- **AGENTS.md:** Roles y responsabilidades de IA -- **db/migrations/README.md:** Documentación técnica de migraciones - -## 🎓 Próximos Pasos - -Después de completar las migraciones: - -1. **Configurar Auth en Supabase Dashboard** - - Habilitar Email/SMS authentication - - Configurar Magic Links - - Crear usuarios de prueba - -2. **Crear Seeds de Datos** - - Locations de prueba - - Staff con diferentes roles - - Services del catálogo - - Customers Free y Gold - -3. **Implementar Tarea 1.3** - - Backend API endpoints para Short ID - - Tests unitarios - - Edge Function o Cron Job para reset semanal - -4. **Implementar Tarea 1.4** - - Endpoints CRUD de customers - - Lógica de cálculo automático de Tier - - Sistema de referidos - -## 🆘 Soporte - -Si encuentras problemas: - -1. Revisa los logs de Supabase Dashboard -2. Verifica que las variables de entorno estén correctamente configuradas -3. Ejecuta las consultas de verificación en el "Paso 3" -4. Consulta la sección de "Solución de Problemas" - ---- - -**Última actualización:** 2026-01-15 -**Versión de migraciones:** 001, 002, 003 -**Estado:** ✅ Listo para producción diff --git a/docs/POST_MIGRATION_SUCCESS.md b/docs/POST_MIGRATION_SUCCESS.md deleted file mode 100644 index 44a3252..0000000 --- a/docs/POST_MIGRATION_SUCCESS.md +++ /dev/null @@ -1,370 +0,0 @@ -# 🎉 Migraciones Exitosas - SalonOS - -## ✅ Estado: Migraciones Completadas - -¡Excelente! Las migraciones de base de datos se han ejecutado exitosamente en Supabase. - ---- - -## 🔍 Paso 1: Verificar la Instalación - -Vamos a ejecutar un script de verificación para confirmar que todo se creó correctamente. - -### Ejecutar Script de Verificación - -1. Ve a: **https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/sql** -2. Haz clic en **"New query"** -3. Copia el contenido de: **`scripts/verify-migration.sql`** -4. Pega el contenido en el SQL Editor -5. Haz clic en **"Run"** - -### Resultado Esperado - -Deberías ver: - -``` -TABLAS | locations -TABLAS | resources -TABLAS | staff -TABLAS | services -TABLAS | customers -TABLAS | invitations -TABLAS | bookings -TABLAS | audit_logs -... -FUNCIONES | generate_short_id -FUNCIONES | generate_invitation_code -FUNCIONES | reset_all_weekly_invitations -FUNCIONES | validate_secondary_artist_role -... -TRIGGERS | locations_updated_at -TRIGGERS | validate_booking_secondary_artist -... -POLÍTICAS RLS | customers_select_admin_manager -... -ENUM TYPES | user_role -ENUM TYPES | customer_tier -... -SHORT ID TEST | A3F7X2 -INVITATION CODE TEST | X9J4K2M5N8 -... -RESUMEN | Tablas: 8 -RESUMEN | Funciones: 14 -RESUMEN | Triggers: 17+ -RESUMEN | Políticas RLS: 20+ -RESUMEN | Tipos ENUM: 6 -``` - ---- - -## 🌱 Paso 2: Crear Datos de Prueba - -Ahora vamos a crear datos de prueba para poder desarrollar y probar el sistema. - -### Ejecutar Script de Seed - -1. En el mismo SQL Editor, haz clic en **"New query"** -2. Copia el contenido de: **`scripts/seed-data.sql`** -3. Pega el contenido en el SQL Editor -4. Haz clic en **"Run"** - -### Resultado Esperado - -Deberías ver: - -``` -========================================== -SALONOS - SEED DE DATOS COMPLETADO -========================================== -Locations: 3 -Resources: 6 -Staff: 8 -Services: 6 -Customers: 4 -Invitations: 15 -Bookings: 5 -========================================== -✅ Base de datos lista para desarrollo -========================================== -``` - -### Datos Creados - -**Locations (3):** -- Salón Principal - Centro -- Salón Norte - Polanco -- Salón Sur - Coyoacán - -**Resources (6):** -- 3 estaciones en Centro -- 2 estaciones en Polanco -- 1 estación en Coyoacán - -**Staff (8):** -- 1 Admin -- 2 Managers -- 1 Staff -- 4 Artists (María, Ana, Carla, Laura) - -**Services (6):** -- Corte y Estilizado ($500) -- Color Completo ($1,200) -- Balayage Premium ($2,000) - **Dual Artist** -- Tratamiento Kératina ($1,500) -- Peinado Evento ($800) -- Servicio Express ($600) - **Dual Artist** - -**Customers (4):** -- Sofía Ramírez (Gold) - VIP -- Valentina Hernández (Gold) -- Camila López (Free) -- Isabella García (Gold) - VIP - -**Invitations (15):** -- 5 para cada cliente Gold (Sofía, Valentina, Isabella) - -**Bookings (5):** -- 1 Balayage Premium para Sofía -- 1 Color Completo para Valentina -- 1 Corte y Estilizado para Camila -- 1 Servicio Express Dual Artist para Isabella (con secondary_artist) -- 1 Peinado Evento para Sofía - ---- - -## 🧪 Paso 3: Probar Funcionalidades - -### Probar Short ID - -```sql -SELECT generate_short_id(); -``` - -**Resultado esperado:** String de 6 caracteres (ej: "A3F7X2") - -### Probar Código de Invitación - -```sql -SELECT generate_invitation_code(); -``` - -**Resultado esperado:** String de 10 caracteres (ej: "X9J4K2M5N8") - -### Verificar Bookings Creados - -```sql -SELECT - b.short_id, - c.first_name || ' ' || c.last_name as customer, - s.display_name as artist, - svc.name as service, - b.start_time_utc, - b.end_time_utc, - b.status, - b.total_amount -FROM bookings b -JOIN customers c ON b.customer_id = c.id -JOIN staff s ON b.staff_id = s.id -JOIN services svc ON b.service_id = svc.id -ORDER BY b.start_time_utc; -``` - -### Verificar Invitaciones - -```sql -SELECT - i.code, - inv.first_name || ' ' || inv.last_name as inviter, - i.status, - i.week_start_date, - i.expiry_date -FROM invitations i -JOIN customers inv ON i.inviter_id = inv.id -WHERE i.status = 'pending' -ORDER BY inv.first_name, i.expiry_date; -``` - -### Verificar Staff y Roles - -```sql -SELECT - s.display_name, - s.role, - l.name as location, - s.phone, - s.is_active -FROM staff s -JOIN locations l ON s.location_id = l.id -ORDER BY l.name, s.role, s.display_name; -``` - -### Verificar Auditoría - -```sql -SELECT - entity_type, - action, - new_values->>'operation' as operation, - new_values->>'table_name' as table_name, - created_at -FROM audit_logs -ORDER BY created_at DESC -LIMIT 10; -``` - -### Probar Validación de Secondary Artist - -**Test 1: Intentar crear booking con secondary_artist válido** - -```sql --- Este debe funcionar -INSERT INTO bookings ( - customer_id, - staff_id, - secondary_artist_id, - location_id, - resource_id, - service_id, - start_time_utc, - end_time_utc, - status, - deposit_amount, - total_amount, - is_paid, - notes -) -SELECT - (SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com'), - (SELECT id FROM staff WHERE display_name = 'Artist María García'), - (SELECT id FROM staff WHERE display_name = 'Artist Ana Rodríguez'), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro'), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro') LIMIT 1 OFFSET 2 LIMIT 1), - (SELECT id FROM services WHERE name = 'Balayage Premium'), - NOW() + INTERVAL '7 days', - NOW() + INTERVAL '7 days' + INTERVAL '3 hours', - 'confirmed', - 200.00, - 2000.00, - true, - 'Test de validación - secondary_artist válido' -RETURNING short_id; -``` - -**Resultado esperado:** ✅ Booking creado exitosamente - -**Test 2: Intentar crear booking con secondary_artist inválido** - -```sql --- Este debe fallar -INSERT INTO bookings ( - customer_id, - staff_id, - secondary_artist_id, - location_id, - resource_id, - service_id, - start_time_utc, - end_time_utc, - status, - deposit_amount, - total_amount, - is_paid, - notes -) -SELECT - (SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com'), - (SELECT id FROM staff WHERE display_name = 'Artist María García'), - (SELECT id FROM staff WHERE display_name = 'Manager Centro'), -- ❌ Esto NO es 'artist' - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro'), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro') LIMIT 1 OFFSET 2 LIMIT 1), - (SELECT id FROM services WHERE name = 'Balayage Premium'), - NOW() + INTERVAL '8 days', - NOW() + INTERVAL '8 days' + INTERVAL '3 hours', - 'confirmed', - 200.00, - 2000.00, - true, - 'Test de validación - secondary_artist inválido'; -``` - -**Resultado esperado:** ❌ Error: `secondary_artist_id must reference an active staff member with role 'artist'` - ---- - -## ✅ Paso 4: Verificar Checklist - -Antes de continuar con el desarrollo, asegúrate de: - -- [x] Migraciones ejecutadas exitosamente -- [ ] Script de verificación ejecutado y todo correcto -- [ ] Script de seed ejecutado y datos creados -- [ ] Short ID generable -- [ ] Código de invitación generable -- [ ] Validación de secondary_artist funcionando -- [ ] Auditoría registrando correctamente - ---- - -## 🎓 Próximos Pasos - -### Configurar Auth en Supabase Dashboard - -1. Ve a: **Authentication → Providers** -2. Habilita **Email Provider** -3. Configura **Email Templates** (opcional) -4. Habilita **SMS Provider** si usas Twilio (opcional) - -### Crear Usuarios en Auth - -Para los datos de seed, necesitas crear usuarios en Supabase Auth: - -1. Ve a: **Authentication → Users** -2. Haz clic en **"Add user"** para cada usuario de staff y customer -3. Usa los mismos UUIDs que están en el seed para los `user_id` de staff y customers - -### Continuar con el Desarrollo - -Ahora que la base de datos está lista, puedes continuar con: - -1. **Tarea 1.3:** Short ID & Invitations - - Implementar endpoints de API - - Tests unitarios - - Edge Function o Cron Job para reset semanal - -2. **Tarea 1.4:** CRM Base - - Endpoints CRUD de customers - - Lógica de cálculo automático de Tier - - Sistema de referidos - -3. **Fase 2:** Motor de Agendamiento - - Validación Staff/Artist - - Validación Recursos - - Servicios Express (Dual Artist) - ---- - -## 📚 Documentación Disponible - -- **`docs/00_FULL_MIGRATION_FINAL_README.md`** - Guía de migración final -- **`docs/MIGRATION_CORRECTION.md`** - Detalle de correcciones -- **`docs/SUPABASE_DASHBOARD_MIGRATION.md`** - Guía de ejecución -- **`scripts/verify-migration.sql`** - Script de verificación -- **`scripts/seed-data.sql`** - Script de datos de prueba -- **`FASE_1_STATUS.md`** - Estado de la Fase 1 - ---- - -## 🆘 Soporte - -Si encuentras problemas: - -1. Revisa los logs de Supabase Dashboard -2. Ejecuta el script de verificación -3. Consulta la documentación arriba -4. Verifica que las funciones y triggers estén creados correctamente - ---- - -**¡Felicidades!** 🎉 Tu base de datos de SalonOS está completamente configurada y lista para el desarrollo. - -**¿Listo para configurar Auth en Supabase Dashboard o continuar con el desarrollo de la aplicación?** diff --git a/docs/QUICK_START_POST_MIGRATION.md b/docs/QUICK_START_POST_MIGRATION.md deleted file mode 100644 index f5b989a..0000000 --- a/docs/QUICK_START_POST_MIGRATION.md +++ /dev/null @@ -1,375 +0,0 @@ -# 🎉 SALONOS - GUÍA RÁPIDA POST-MIGRACIÓN - -## ✅ ESTADO ACTUAL - -- ✅ Migraciones ejecutadas exitosamente en Supabase -- ✅ 8 tablas, 14 funciones, 17+ triggers, 20+ políticas RLS, 6 tipos ENUM creados -- ✅ Base de datos lista para desarrollo -- ✅ Scripts de verificación y seed creados - ---- - -## 📋 PASOS PENDIENTES - -### Paso 1: Verificar Instalación de Migraciones ✅ -**Guía:** `docs/STEP_BY_STEP_VERIFICATION.md` - -**Qué hacer:** -1. Abrir Supabase SQL Editor -2. Ejecutar consultas de verificación (12 consultas en total) -3. Verificar que todo esté correcto - -**Duración estimada:** 5-10 minutos - ---- - -### Paso 2: Crear Datos de Prueba ✅ -**Guía:** `docs/STEP_BY_STEP_VERIFICATION.md` (Sección 2) - -**Qué hacer:** -1. Ejecutar seed por secciones (9 secciones en total) -2. Crear locations, resources, staff, services, customers, invitations, bookings -3. Verificar que todos los datos se crearon correctamente - -**Duración estimada:** 10-15 minutos - -**Datos a crear:** -- 3 locations (Centro, Polanco, Coyoacán) -- 6 resources (estaciones) -- 8 staff (1 admin, 2 managers, 1 staff, 4 artists) -- 6 services (catálogo completo) -- 4 customers (mix Free/Gold) -- 15 invitations (5 por cliente Gold) -- 5 bookings de prueba - ---- - -### Paso 3: Configurar Auth en Supabase Dashboard ✅ -**Guía:** `docs/STEP_BY_STEP_AUTH_CONFIG.md` - -**Qué hacer:** -1. Habilitar Email Provider -2. Configurar Site URL y Redirect URLs -3. Configurar SMTP (opcional) -4. Configurar SMS Provider (opcional) -5. Crear usuarios de staff (8 usuarios) -6. Crear usuarios de customers (4 usuarios) -7. Actualizar tablas staff y customers con user_ids correctos -8. Configurar Email Templates (opcional) - -**Duración estimada:** 20-30 minutos - -**Usuarios a crear:** - -**Staff (8):** -- Admin Principal: `admin@salonos.com` -- Manager Centro: `manager.centro@salonos.com` -- Manager Polanco: `manager.polanco@salonos.com` -- Staff Coordinadora: `staff.coordinadora@salonos.com` -- Artist María García: `artist.maria@salonos.com` -- Artist Ana Rodríguez: `artist.ana@salonos.com` -- Artist Carla López: `artist.carla@salonos.com` -- Artist Laura Martínez: `artist.laura@salonos.com` - -**Customers (4):** -- Sofía Ramírez (Gold): `sofia.ramirez@example.com` -- Valentina Hernández (Gold): `valentina.hernandez@example.com` -- Camila López (Free): `camila.lopez@example.com` -- Isabella García (Gold): `isabella.garcia@example.com` - ---- - -## 🎯 RESUMEN DE CONSULTAS RÁPIDAS - -### Verificar Tablas -```sql -SELECT table_name -FROM information_schema.tables -WHERE table_schema = 'public' -AND table_name IN ('locations', 'resources', 'staff', 'services', 'customers', 'invitations', 'bookings', 'audit_logs') -ORDER BY table_name; -``` - -### Verificar Funciones -```sql -SELECT routine_name -FROM information_schema.routines -WHERE routine_schema = 'public' -ORDER BY routine_name; -``` - -### Probar Short ID -```sql -SELECT generate_short_id(); -``` - -### Probar Código de Invitación -```sql -SELECT generate_invitation_code(); -``` - -### Verificar Bookings -```sql -SELECT - b.short_id, - c.first_name || ' ' || c.last_name as customer, - s.display_name as artist, - svc.name as service, - b.start_time_utc, - b.status, - b.total_amount -FROM bookings b -JOIN customers c ON b.customer_id = c.id -JOIN staff s ON b.staff_id = s.id -JOIN services svc ON b.service_id = svc.id -ORDER BY b.start_time_utc; -``` - -### Verificar Staff y Roles -```sql -SELECT - s.display_name, - s.role, - l.name as location, - s.phone, - s.is_active -FROM staff s -JOIN locations l ON s.location_id = l.id -ORDER BY l.name, s.role, s.display_name; -``` - -### Verificar Invitaciones -```sql -SELECT - i.code, - inv.first_name || ' ' || inv.last_name as inviter, - i.status, - i.week_start_date, - i.expiry_date -FROM invitations i -JOIN customers inv ON i.inviter_id = inv.id -WHERE i.status = 'pending' -ORDER BY inv.first_name, i.expiry_date; -``` - -### Verificar Auditoría -```sql -SELECT - entity_type, - action, - new_values->>'operation' as operation, - new_values->>'table_name' as table_name, - created_at -FROM audit_logs -ORDER BY created_at DESC -LIMIT 10; -``` - ---- - -## ✅ CHECKLIST COMPLETO - -### Verificación de Migraciones -- [ ] 8 tablas creadas (locations, resources, staff, services, customers, invitations, bookings, audit_logs) -- [ ] 14 funciones creadas -- [ ] 17+ triggers activos -- [ ] 20+ políticas RLS configuradas -- [ ] 6 tipos ENUM creados -- [ ] Short ID generable -- [ ] Código de invitación generable - -### Seed de Datos -- [ ] 3 locations creadas -- [ ] 6 resources creadas -- [ ] 8 staff creados -- [ ] 6 services creados -- [ ] 4 customers creados -- [ ] 15 invitaciones creadas (5 por cliente Gold) -- [ ] 5 bookings creados -- [ ] 1 booking con secondary_artist - -### Configuración de Auth -- [ ] Email Provider habilitado -- [ ] Site URL configurado -- [ ] 8 usuarios de staff creados en Supabase Auth -- [ ] 4 usuarios de customers creados en Supabase Auth -- [ ] Tabla staff actualizada con user_ids correctos -- [ ] Tabla customers actualizada con user_ids correctos -- [ ] Email templates configurados (opcional) - -### Pruebas Funcionales -- [ ] Login con admin funciona -- [ ] Login con customer funciona -- [ ] Políticas RLS funcionan (Artist no ve email/phone de customers) -- [ ] Short ID se genera automáticamente al crear booking -- [ ] Validación de secondary_artist funciona -- [ ] Auditoría se registra correctamente - ---- - -## 📚 DOCUMENTACIÓN DISPONIBLE - -### Guías Principales -1. **`docs/STEP_BY_STEP_VERIFICATION.md`** - - Guía paso a paso para ejecutar scripts de verificación y seed - - 12 consultas de verificación - - 9 secciones de seed de datos - - Consultas adicionales de prueba - -2. **`docs/STEP_BY_STEP_AUTH_CONFIG.md`** - - Guía paso a paso para configurar Auth en Supabase Dashboard - - Configuración de Email Provider - - Configuración de SMS Provider (opcional) - - Creación de usuarios de staff y customers - - Actualización de tablas con user_ids - - Configuración de Email Templates (opcional) - -### Documentación de Migraciones -3. **`docs/00_FULL_MIGRATION_FINAL_README.md`** - - Guía de la migración final - - Instrucciones de ejecución - - Consultas de verificación - -4. **`docs/MIGRATION_CORRECTION.md`** - - Detalle de las correcciones aplicadas - - Problemas encontrados y soluciones - -5. **`docs/SUPABASE_DASHBOARD_MIGRATION.md`** - - Guía de ejecución en Supabase Dashboard - - Solución de problemas - -6. **`docs/POST_MIGRATION_SUCCESS.md`** - - Guía general post-migración - - Scripts de prueba - - Verificación de funcionalidades - -### Documentación Técnica -7. **`db/migrations/README.md`** - - Documentación técnica de migraciones - - Orden de ejecución - - Verificación - -8. **`db/migrations/00_FULL_MIGRATION_FINAL.sql`** - - Script final consolidado - - Todas las migraciones en un archivo - -### Scripts -9. **`scripts/verify-migration.sql`** - - Script completo de verificación - - 12 consultas de verificación - -10. **`scripts/seed-data.sql`** - - Script completo de seed - - Crea todos los datos de prueba - -### Estado del Proyecto -11. **`FASE_1_STATUS.md`** - - Estado actualizado de la Fase 1 - - Tareas completadas - - Próximos pasos - ---- - -## 🚀 PRÓXIMOS PASOS (Después de Auth Configurado) - -### Desarrollo del Frontend - -1. **Crear página de login** (`app/boutique/(auth)/login/page.tsx`) -2. **Crear página de registro** (`app/boutique/(auth)/register/page.tsx`) -3. **Crear página de dashboard de cliente** (`app/boutique/(customer)/dashboard/page.tsx`) -4. **Crear página de bookings** (`app/boutique/(customer)/bookings/page.tsx`) - -### Desarrollo del Backend - -1. **Tarea 1.3: Short ID & Invitaciones** - - API endpoint: `POST /api/bookings` (crea booking con short_id) - - API endpoint: `GET /api/invitations` (lista invitaciones) - - API endpoint: `POST /api/invitations/reset` (reset manual) - - Tests unitarios - - Edge Function o Cron Job para reset semanal (Lunes 00:00 UTC) - -2. **Tarea 1.4: CRM Base (Customers)** - - API endpoint: `GET /api/customers` (lista customers) - - API endpoint: `GET /api/customers/[id]` (detalle de customer) - - API endpoint: `POST /api/customers` (crear customer) - - API endpoint: `PUT /api/customers/[id]` (actualizar customer) - - API endpoint: `DELETE /api/customers/[id]` (eliminar customer) - - Lógica de cálculo automático de Tier - - Sistema de referidos - -### Fase 2: Motor de Agendamiento - -1. **Tarea 2.1: Disponibilidad Doble Capa** - - Validación Staff/Artist (horario laboral + Google Calendar) - - Validación Recurso (disponibilidad de estación física) - - Regla de prioridad dinámica - -2. **Tarea 2.2: Servicios Express (Dual Artist)** - - Lógica de booking dual - - Aplicación automática de Premium Fee - -3. **Tarea 2.3: Google Calendar Sync** - - Integración vía Service Account - - Sincronización bidireccional - - Manejo de conflictos - ---- - -## 💡 TIPS ÚTILES - -### Tip 1: Ejecutar Scripts en el Orden Correcto -Siempre ejecuta: -1. Verificación → Seed → Auth Config - -### Tip 2: Verificar cada Paso -No continúes al siguiente paso hasta verificar que el anterior esté correcto. - -### Tip 3: Usar Pestañas Separadas -Abre múltiples pestañas en el SQL Editor para separar: -- Pestaña 1: Verificación -- Pestaña 2: Seed -- Pestaña 3: Pruebas adicionales - -### Tip 4: Guardar los user_ids -Copia los user_ids de Supabase Auth en un archivo de notas para usarlos cuando actualices las tablas staff y customers. - -### Tip 5: Probar con Diferentes Roles -Inicia sesión con diferentes roles (admin, manager, staff, artist, customer) para verificar que las políticas RLS funcionen correctamente. - ---- - -## 🆘 AYUDA - -Si encuentras problemas: - -1. **Revisa los logs de Supabase Dashboard** -2. **Ejecuta las consultas de verificación** -3. **Consulta la guía de solución de problemas en cada documento** -4. **Verifica que las variables de entorno estén correctas en .env.local** -5. **Asegúrate de estar usando el proyecto correcto de Supabase** - ---- - -## 🎉 ¡FELICIDADES! - -Has completado exitosamente: - -✅ **FASE 1.1:** Infraestructura Base (Next.js 14 structure) -✅ **FASE 1.2:** Esquema de Base de Datos Inicial (8 tablas, RLS, triggers) -✅ **MIGRACIONES:** Ejecutadas exitosamente en Supabase -✅ **VERIFICACIÓN:** Scripts creados y listos para ejecutar -✅ **SEED DE DATOS:** Scripts creados y listos para ejecutar -✅ **AUTH CONFIGURACIÓN:** Guía completa creada - -**Tu base de datos de SalonOS está lista para el desarrollo!** - ---- - -**¿Qué deseas hacer ahora?** - -1. **Ejecutar scripts de verificación y seed** (usa `docs/STEP_BY_STEP_VERIFICATION.md`) -2. **Configurar Auth en Supabase Dashboard** (usa `docs/STEP_BY_STEP_AUTH_CONFIG.md`) -3. **Comenzar el desarrollo del frontend** (Next.js) -4. **Implementar las tareas de backend** (Tarea 1.3 y 1.4) - -**¡El futuro es tuyo!** 🚀 diff --git a/docs/STEP_BY_STEP_AUTH_CONFIG.md b/docs/STEP_BY_STEP_AUTH_CONFIG.md deleted file mode 100644 index d8ebdbd..0000000 --- a/docs/STEP_BY_STEP_AUTH_CONFIG.md +++ /dev/null @@ -1,610 +0,0 @@ -# 🔐 Guía Paso a Paso - Configuración de Auth en Supabase Dashboard - -## 🎯 Objetivo - -Configurar el sistema de autenticación de Supabase para que los usuarios puedan: -- Registrarse con email -- Iniciar sesión con Magic Links -- Tener roles asignados (Admin, Manager, Staff, Artist, Customer) - ---- - -## 📋 Paso 1: Abrir Configuración de Auth - -1. Ve a: **https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl** -2. En el menú lateral, haz clic en **"Authentication"** -3. Haz clic en **"Providers"** - ---- - -## 🔑 Paso 2: Configurar Email Provider - -### 2.1 Habilitar Email Auth - -1. En la sección **"Providers"**, busca **"Email"** -2. Haz clic en el botón **"Enable"** -3. Configura las siguientes opciones: - -**Email Confirmation:** -``` -Confirm email: ON (activado) -``` - -**Email Templates:** -- **Confirm signup:** Habilitar -- **Reset password:** Habilitar -- **Email change:** Habilitar (opcional) -- **Magic Link:** Habilitar (opcional) - -### 2.2 Configurar Site URL - -1. En la sección **"URL Configuration"**, configura: - - **Site URL:** `http://localhost:3000` - - **Redirect URLs:** `http://localhost:3000/auth/callback` - -**Nota:** Para producción, cambiar `localhost:3000` por tu dominio de producción. - -### 2.3 Configurar SMTP (Opcional) - -Para desarrollo, puedes usar el SMTP por defecto de Supabase. - -Si deseas usar tu propio servidor SMTP: - -1. Ve a **"Authentication" → "URL Configuration"** -2. Desplázate hasta **"SMTP Settings"** -3. Configura: - - **SMTP Host:** `smtp.gmail.com` (ejemplo) - - **SMTP Port:** `587` - - **SMTP User:** `tu-email@gmail.com` - - **SMTP Password:** `tu-app-password` - - **Sender Email:** `tu-email@gmail.com` - - **Sender Name:** `SalonOS` - ---- - -## 📱 Paso 3: Configurar SMS Provider (Opcional) - -Para autenticación por SMS (opcional para inicio): - -### 3.1 Habilitar Twilio - -1. En **"Providers"**, busca **"Phone"** -2. Haz clic en **"Enable"** -3. Selecciona **"Twilio"** como proveedor -4. Configura: - - **Account SID:** Obtenido de Twilio Dashboard - - **Auth Token:** Obtenido de Twilio Dashboard - - **Twilio Phone Number:** `+14155238886` (o tu número de Twilio) - - **Message Service SID:** (opcional) - -### 3.2 Verificar SMS Test - -1. En la sección **"Phone"**, haz clic en **"Test"** -2. Ingresa un número de teléfono de prueba -3. Envía un mensaje de prueba - ---- - -## 🧑 Paso 4: Crear Usuarios de Staff - -### 4.1 Obtener User IDs del Seed - -Primero, necesitamos los `user_id` que se crearon en el seed. Ejecuta esta consulta en el SQL Editor: - -```sql -SELECT - s.display_name, - s.role, - s.user_id as supabase_user_id_to_create -FROM staff s -ORDER BY s.role, s.display_name; -``` - -Copia los `user_id` de cada miembro del staff. - -### 4.2 Crear Usuarios en Supabase Auth - -**Opción A: Manual (recomendado para empezar)** - -1. Ve a **"Authentication" → "Users"** -2. Haz clic en **"Add user"** -3. Para cada miembro del staff, crea un usuario: - -**Admin Principal:** -- **Email:** `admin@salonos.com` -- **Password:** `Admin123!` (o una segura) -- **Auto Confirm User:** ON -- **User Metadata (opcional):** - ```json - { - "role": "admin", - "display_name": "Admin Principal" - } - ``` - -**Manager Centro:** -- **Email:** `manager.centro@salonos.com` -- **Password:** `Manager123!` -- **Auto Confirm User:** ON -- **User Metadata:** - ```json - { - "role": "manager", - "display_name": "Manager Centro" - } - ``` - -**Manager Polanco:** -- **Email:** `manager.polanco@salonos.com` -- **Password:** `Manager123!` -- **Auto Confirm User:** ON -- **User Metadata:** - ```json - { - "role": "manager", - "display_name": "Manager Polanco" - } - ``` - -**Staff Coordinadora:** -- **Email:** `staff.coordinadora@salonos.com` -- **Password:** `Staff123!` -- **Auto Confirm User:** ON -- **User Metadata:** - ```json - { - "role": "staff", - "display_name": "Staff Coordinadora" - } - ``` - -**Artist María García:** -- **Email:** `artist.maria@salonos.com` -- **Password:** `Artist123!` -- **Auto Confirm User:** ON -- **User Metadata:** - ```json - { - "role": "artist", - "display_name": "Artist María García" - } - ``` - -**Artist Ana Rodríguez:** -- **Email:** `artist.ana@salonos.com` -- **Password:** `Artist123!` -- **Auto Confirm User:** ON -- **User Metadata:** - ```json - { - "role": "artist", - "display_name": "Artist Ana Rodríguez" - } - ``` - -**Artist Carla López:** -- **Email:** `artist.carla@salonos.com` -- **Password:** `Artist123!` -- **Auto Confirm User:** ON -- **User Metadata:** - ```json - { - "role": "artist", - "display_name": "Artist Carla López" - } - ``` - -**Artist Laura Martínez:** -- **Email:** `artist.laura@salonos.com` -- **Password:** `Artist123!` -- **Auto Confirm User:** ON -- **User Metadata:** - ```json - { - "role": "artist", - "display_name": "Artist Laura Martínez" - } - ``` - -**Opción B: Automática con SQL (más avanzado)** - -Si prefieres crear usuarios automáticamente con SQL y luego actualizar sus IDs en la tabla staff: - -1. Crea una tabla temporal para mapear los usuarios: -```sql --- Primero, crea los usuarios en Supabase Auth manualmente (opción A) --- Luego ejecuta esta consulta para obtener sus IDs: -SELECT - id as auth_user_id, - email -FROM auth.users -ORDER BY created_at DESC -LIMIT 8; -``` - -2. Actualiza la tabla staff con los nuevos IDs: -```sql --- Ejemplo para actualizar un usuario -UPDATE staff -SET user_id = 'NUEVO_AUTH_USER_ID_DESDE_SUPABASE' -WHERE display_name = 'Artist María García'; -``` - ---- - -## 👩 Step 5: Crear Usuarios de Customers - -### 5.1 Obtener User IDs del Seed - -Ejecuta esta consulta en el SQL Editor: - -```sql -SELECT - c.email, - c.first_name || ' ' || c.last_name as full_name, - c.tier, - c.user_id as supabase_user_id_to_create -FROM customers c -ORDER BY c.last_name, c.first_name; -``` - -### 5.2 Crear Usuarios en Supabase Auth - -1. Ve a **"Authentication" → "Users"** -2. Haz clic en **"Add user"** -3. Para cada customer, crea un usuario: - -**Sofía Ramírez (Gold):** -- **Email:** `sofia.ramirez@example.com` -- **Password:** `Customer123!` -- **Auto Confirm User:** ON -- **User Metadata:** - ```json - { - "tier": "gold", - "display_name": "Sofía Ramírez" - } - ``` - -**Valentina Hernández (Gold):** -- **Email:** `valentina.hernandez@example.com` -- **Password:** `Customer123!` -- **Auto Confirm User:** ON -- **User Metadata:** - ```json - { - "tier": "gold", - "display_name": "Valentina Hernández" - } - ``` - -**Camila López (Free):** -- **Email:** `camila.lopez@example.com` -- **Password:** `Customer123!` -- **Auto Confirm User:** ON -- **User Metadata:** - ```json - { - "tier": "free", - "display_name": "Camila López" - } - ``` - -**Isabella García (Gold):** -- **Email:** `isabella.garcia@example.com` -- **Password:** `Customer123!` -- **Auto Confirm User:** ON -- **User Metadata:** - ```json - { - "tier": "gold", - "display_name": "Isabella García" - } - ``` - ---- - -## 🔗 Step 6: Actualizar Tablas con User IDs - -### 6.1 Actualizar Staff - -Después de crear los usuarios en Supabase Auth, necesitas actualizar la tabla `staff` con los nuevos `user_id`. - -1. Obten los nuevos `id` de `auth.users`: - -```sql -SELECT - id as auth_user_id, - email, - raw_user_meta_data->>'role' as role, - raw_user_meta_data->>'display_name' as display_name -FROM auth.users -WHERE raw_user_meta_data->>'role' IN ('admin', 'manager', 'staff', 'artist') -ORDER BY raw_user_meta_data->>'role', raw_user_meta_data->>'display_name'; -``` - -2. Actualiza la tabla `staff`: - -```sql --- Ejemplo para actualizar un usuario de staff -UPDATE staff -SET user_id = 'NUEVO_AUTH_USER_ID_DESDE_SUPABASE' -WHERE display_name = 'Artist María García'; - --- Repite para todos los usuarios de staff -``` - -### 6.2 Actualizar Customers - -1. Obten los nuevos `id` de `auth.users`: - -```sql -SELECT - id as auth_user_id, - email, - raw_user_meta_data->>'tier' as tier, - raw_user_meta_data->>'display_name' as display_name -FROM auth.users -WHERE email LIKE '%example.com' -ORDER BY raw_user_meta_data->>'display_name'; -``` - -2. Actualiza la tabla `customers`: - -```sql --- Ejemplo para actualizar un customer -UPDATE customers -SET user_id = 'NUEVO_AUTH_USER_ID_DESDE_SUPABASE' -WHERE email = 'sofia.ramirez@example.com'; - --- Repite para todos los customers -``` - ---- - -## 🧪 Step 7: Verificar Usuarios Creados - -### 7.1 Verificar en Supabase Auth - -1. Ve a **"Authentication" → "Users"** -2. Verifica que todos los usuarios estén listados -3. Debes ver: - - 8 usuarios de staff (admin, managers, staff, artists) - - 4 usuarios de customers - -### 7.2 Verificar en Base de Datos - -Ejecuta esta consulta en el SQL Editor: - -```sql --- Verificar staff con user_id actualizado -SELECT - 'STAFF' as type, - s.display_name, - s.role, - s.user_id is not null as user_id_set, - au.email as auth_user_email, - au.raw_user_meta_data->>'display_name' as auth_display_name -FROM staff s -LEFT JOIN auth.users au ON s.user_id = au.id -ORDER BY s.role, s.display_name; -``` - -**Resultado esperado:** -``` -type | display_name | role | user_id_set | auth_user_email -STAFF | Admin Principal | admin | true | admin@salonos.com -STAFF | Manager Centro | manager | true | manager.centro@salonos.com -STAFF | Manager Polanco | manager | true | manager.polanco@salonos.com -STAFF | Staff Coordinadora | staff | true | staff.coordinadora@salonos.com -STAFF | Artist María García | artist | true | artist.maria@salonos.com -STAFF | Artist Ana Rodríguez | artist | true | artist.ana@salonos.com -STAFF | Artist Carla López | artist | true | artist.carla@salonos.com -STAFF | Artist Laura Martínez | artist | true | artist.laura@salonos.com -``` - -```sql --- Verificar customers con user_id actualizado -SELECT - 'CUSTOMER' as type, - c.first_name || ' ' || c.last_name as name, - c.tier, - c.user_id is not null as user_id_set, - au.email as auth_user_email, - au.raw_user_meta_data->>'tier' as auth_tier -FROM customers c -LEFT JOIN auth.users au ON c.user_id = au.id -ORDER BY c.last_name, c.first_name; -``` - -**Resultado esperado:** -``` -type | name | tier | user_id_set | auth_user_email -CUSTOMER | Camila López | free | true | camila.lopez@example.com -CUSTOMER | Isabella García | gold | true | isabella.garcia@example.com -CUSTOMER | Sofía Ramírez | gold | true | sofia.ramirez@example.com -CUSTOMER | Valentina Hernández | gold | true | valentina.hernandez@example.com -``` - ---- - -## 🎨 Step 8: Configurar Email Templates (Opcional) - -### 8.1 Confirm Signup Template - -1. Ve a **"Authentication" → "Email Templates"** -2. Haz clic en **"Confirm signup"** -3. Personaliza el template: - -```html -

Bienvenida a SalonOS

- -

Hola {{ .Email }}

- -

Gracias por registrarte en SalonOS. Tu cuenta ha sido creada exitosamente.

- -

Si no creaste esta cuenta, por favor ignora este email.

- -

Saludos,
El equipo de SalonOS

-``` - -### 8.2 Reset Password Template - -1. Haz clic en **"Reset password"** -2. Personaliza el template: - -```html -

Restablecer Contraseña - SalonOS

- -

Hola {{ .Email }}

- -

Hemos recibido una solicitud para restablecer tu contraseña en SalonOS.

- -

Haz clic aquí para restablecer tu contraseña

- -

Este enlace expirará en 24 horas.

- -

Si no solicitaste restablecer tu contraseña, por favor ignora este email.

- -

Saludos,
El equipo de SalonOS

-``` - ---- - -## ✅ Step 9: Probar Autenticación - -### 9.1 Probar Login con Staff - -1. Ve a una página de login (aún no creada en el frontend) -2. Intenta iniciar sesión con: - - **Email:** `admin@salonos.com` - - **Password:** `Admin123!` - -### 9.2 Probar Login con Customer - -1. Intenta iniciar sesión con: - - **Email:** `sofia.ramirez@example.com` - - **Password:** `Customer123!` - -### 9.3 Verificar Token JWT - -Ejecuta esta consulta en el SQL Editor después de iniciar sesión: - -```sql --- Verificar sesión actual -SELECT - auth.uid() as current_user_id, - auth.email() as current_user_email, - auth.role() as current_user_role -FROM (SELECT 1) as dummy; -``` - ---- - -## 🔐 Step 10: Configurar Policies de RLS con Auth - -Las políticas de RLS ya están configuradas en la base de datos. Ahora que los usuarios están creados en Supabase Auth, las políticas deberían funcionar correctamente. - -### Verificar que las Políticas Funcionan - -Ejecuta esta consulta en el SQL Editor con diferentes usuarios: - -```sql --- Probar como Admin --- (Inicia sesión como admin en Supabase Dashboard primero) -SELECT - 'ADMIN TEST' as test_type, - s.display_name, - s.role, - s.phone as can_see_phone -FROM staff s -LIMIT 1; - --- Esta consulta debería mostrar los datos del staff porque admin tiene acceso total -``` - -```sql --- Probar como Artist --- (Inicia sesión como artist en Supabase Dashboard primero) -SELECT - 'ARTIST TEST' as test_type, - c.first_name, - c.last_name, - c.email as can_see_email, - c.phone as can_see_phone -FROM customers c -LIMIT 1; - --- Esta consulta debería mostrar solo first_name y last_name, email y phone deberían ser NULL --- debido a la política RLS que restringe el acceso de artist a datos PII -``` - ---- - -## 🚨 Troubleshooting - -### Error: "User already registered" - -**Causa:** El usuario ya existe en Supabase Auth. - -**Solución:** -1. Ve a **"Authentication" → "Users"** -2. Busca el usuario por email -3. Si existe, usa ese usuario -4. Si no, elige un email diferente - -### Error: "Invalid login credentials" - -**Causa:** El email o password es incorrecto. - -**Solución:** -1. Verifica el email y password -2. Si olvidaste el password, usa el link de **"Forgot password"** -3. O re crea el usuario en Supabase Auth - -### Error: "User ID mismatch" - -**Causa:** El `user_id` en la tabla staff/customers no coincide con el ID en `auth.users`. - -**Solución:** -1. Obtén el ID correcto de `auth.users` -2. Actualiza la tabla staff/customers con el ID correcto - ---- - -## 📚 Documentación Adicional - -- **Supabase Auth Docs:** https://supabase.com/docs/guides/auth -- **RLS Policies:** https://supabase.com/docs/guides/auth/row-level-security -- **Email Templates:** https://supabase.com/docs/guides/auth/auth-email - ---- - -## ✅ Checklist de Configuración - -- [ ] Email Provider habilitado y configurado -- [ ] Site URL configurado -- [ ] SMS Provider configurado (opcional) -- [ ] 8 usuarios de staff creados en Supabase Auth -- [ ] 4 usuarios de customers creados en Supabase Auth -- [ ] Tabla staff actualizada con user_ids correctos -- [ ] Tabla customers actualizada con user_ids correctos -- [ ] Email templates configurados (opcional) -- [ ] Login probado con admin -- [ ] Login probado con customer -- [ ] Políticas RLS verificadas - ---- - -## 🎯 Próximos Pasos - -Después de completar la configuración de Auth: - -1. **Implementar frontend de autenticación** en Next.js -2. **Crear API endpoints** para login/logout -3. **Implementar Tarea 1.3:** Short ID & Invitaciones (backend) -4. **Implementar Tarea 1.4:** CRM Base (endpoints CRUD) - ---- - -**¿Listo para continuar con el desarrollo de la aplicación?** diff --git a/docs/STEP_BY_STEP_VERIFICATION.md b/docs/STEP_BY_STEP_VERIFICATION.md deleted file mode 100644 index f9a6feb..0000000 --- a/docs/STEP_BY_STEP_VERIFICATION.md +++ /dev/null @@ -1,734 +0,0 @@ -# 📋 Guía Paso a Paso - Verificación y Seed en Supabase Dashboard - -## 🎯 Paso 1: Ejecutar Script de Verificación - -### 1.1 Abrir Supabase SQL Editor - -1. Ve a: **https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/sql** -2. Haz clic en **"New query"** para abrir un editor SQL vacío - -### 1.2 Copiar Script de Verificación - -Copia el contenido completo de: **`scripts/verify-migration.sql`** - -**O ejecuta estas consultas una por una:** - -#### Consulta 1: Verificar Tablas Creadas - -```sql -SELECT 'TABLAS' as verification_type, table_name as item -FROM information_schema.tables -WHERE table_schema = 'public' -AND table_name IN ('locations', 'resources', 'staff', 'services', 'customers', 'invitations', 'bookings', 'audit_logs') -ORDER BY table_name; -``` - -**Resultado esperado:** -``` -verification_type | item -TABLAS | locations -TABLAS | resources -TABLAS | staff -TABLAS | services -TABLAS | customers -TABLAS | invitations -TABLAS | bookings -TABLAS | audit_logs -``` - -#### Consulta 2: Verificar Funciones Creadas - -```sql -SELECT 'FUNCIONES' as verification_type, routine_name as item -FROM information_schema.routines -WHERE routine_schema = 'public' -ORDER BY routine_name; -``` - -**Resultado esperado (14 funciones):** -``` -verification_type | item -FUNCIONES | generate_booking_short_id -FUNCIONES | generate_invitation_code -FUNCIONES | generate_short_id -FUNCIONES | get_current_user_role -FUNCIONES | get_week_start -FUNCIONES | is_admin -FUNCIONES | is_artist -FUNCIONES | is_customer -FUNCIONES | is_staff_or_higher -FUNCIONES | log_audit -FUNCIONES | reset_all_weekly_invitations -FUNCIONES | reset_weekly_invitations_for_customer -FUNCIONES | update_updated_at -FUNCIONES | validate_secondary_artist_role -``` - -#### Consulta 3: Verificar Triggers Activos - -```sql -SELECT 'TRIGGERS' as verification_type, trigger_name as item -FROM information_schema.triggers -WHERE trigger_schema = 'public' -ORDER BY event_object_table, trigger_name; -``` - -**Resultado esperado (17+ triggers):** -``` -verification_type | item -TRIGGERS | audit_bookings -TRIGGERS | audit_customers -TRIGGERS | audit_invitations -TRIGGERS | audit_staff -TRIGGERS | audit_services -TRIGGERS | booking_generate_short_id -TRIGGERS | bookings_updated_at -TRIGGERS | customers_updated_at -TRIGGERS | invitations_updated_at -TRIGGERS | locations_updated_at -TRIGGERS | resources_updated_at -TRIGGERS | staff_updated_at -TRIGGERS | services_updated_at -TRIGGERS | validate_booking_secondary_artist -... -``` - -#### Consulta 4: Verificar Políticas RLS - -```sql -SELECT 'POLÍTICAS RLS' as verification_type, policyname as item -FROM pg_policies -WHERE schemaname = 'public' -ORDER BY tablename, policyname; -``` - -**Resultado esperado (20+ políticas):** -``` -verification_type | item -POLÍTICAS RLS | audit_logs_no_insert -POLÍTICAS RLS | audit_logs_select_admin_manager -POLÍTICAS RLS | audit_logs_select_staff_location -POLÍTICAS RLS | bookings_create_own -POLÍTICAS RLS | bookings_modify_admin_manager -POLÍTICAS RLS | bookings_modify_staff_location -POLÍTICAS RLS | bookings_no_modify_artist -POLÍTICAS RLS | bookings_select_admin_manager -POLÍTICAS RLS | bookings_select_artist_own -POLÍTICAS RLS | bookings_select_own -POLÍTICAS RLS | bookings_select_staff_location -POLÍTICAS RLS | bookings_update_own -POLÍTICAS RLS | customers_modify_admin_manager -POLÍTICAS RLS | customers_modify_staff -POLÍTICAS RLS | customers_select_admin_manager -POLÍTICAS RLS | customers_select_artist_restricted -POLÍTICAS RLS | customers_select_own -POLÍTICAS RLS | customers_select_staff -POLÍTICAS RLS | customers_update_own -... -``` - -#### Consulta 5: Verificar Tipos ENUM - -```sql -SELECT 'ENUM TYPES' as verification_type, typname as item -FROM pg_type -WHERE typtype = 'e' -AND typname IN ('user_role', 'customer_tier', 'booking_status', 'invitation_status', 'resource_type', 'audit_action') -ORDER BY typname; -``` - -**Resultado esperado (6 tipos):** -``` -verification_type | item -ENUM TYPES | audit_action -ENUM TYPES | booking_status -ENUM TYPES | customer_tier -ENUM TYPES | invitation_status -ENUM TYPES | resource_type -ENUM TYPES | user_role -``` - -#### Consulta 6: Probar Short ID Generation - -```sql -SELECT 'SHORT ID TEST' as verification_type, generate_short_id() as item; -``` - -**Resultado esperado:** -``` -verification_type | item -SHORT ID TEST | A3F7X2 -``` -*(El string será diferente cada vez)* - -#### Consulta 7: Probar Invitation Code Generation - -```sql -SELECT 'INVITATION CODE TEST' as verification_type, generate_invitation_code() as item; -``` - -**Resultado esperado:** -``` -verification_type | item -INVITATION CODE TEST | X9J4K2M5N8 -``` -*(El string será diferente cada vez)* - -#### Consulta 8: Verificar Trigger de Validación de Secondary Artist - -```sql -SELECT 'SECONDARY ARTIST TRIGGER' as verification_type, trigger_name as item -FROM information_schema.triggers -WHERE trigger_name = 'validate_booking_secondary_artist'; -``` - -**Resultado esperado:** -``` -verification_type | item -SECONDARY ARTIST TRIGGER | validate_booking_secondary_artist -``` - -#### Consulta 9: Verificar Función de Reset de Invitaciones - -```sql -SELECT 'RESET INVITATIONS FUNCTION' as verification_type, routine_name as item -FROM information_schema.routines -WHERE routine_name = 'reset_all_weekly_invitations'; -``` - -**Resultado esperado:** -``` -verification_type | item -RESET INVITATIONS FUNCTION | reset_all_weekly_invitations -``` - -#### Consulta 10: Verificar Función de Validación de Secondary Artist - -```sql -SELECT 'VALIDATE SECONDARY ARTIST' as verification_type, routine_name as item -FROM information_schema.routines -WHERE routine_name = 'validate_secondary_artist_role'; -``` - -**Resultado esperado:** -``` -verification_type | item -VALIDATE SECONDARY ARTIST | validate_secondary_artist_role -``` - -#### Consulta 11: Verificar Week Start Function - -```sql -SELECT 'WEEK START FUNCTION' as verification_type, get_week_start(CURRENT_DATE) as item; -``` - -**Resultado esperado:** -``` -verification_type | item -WEEK START FUNCTION | 2025-01-13 -``` -*(La fecha será el lunes de la semana actual)* - -#### Consulta 12: Contar Elementos por Tipo - -```sql -SELECT - 'RESUMEN' as verification_type, - 'Tablas: ' || (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN ('locations', 'resources', 'staff', 'services', 'customers', 'invitations', 'bookings', 'audit_logs')) as item - -UNION ALL - -SELECT - 'RESUMEN' as verification_type, - 'Funciones: ' || (SELECT COUNT(*) FROM information_schema.routines WHERE routine_schema = 'public') as item - -UNION ALL - -SELECT - 'RESUMEN' as verification_type, - 'Triggers: ' || (SELECT COUNT(*) FROM information_schema.triggers WHERE trigger_schema = 'public') as item - -UNION ALL - -SELECT - 'RESUMEN' as verification_type, - 'Políticas RLS: ' || (SELECT COUNT(*) FROM pg_policies WHERE schemaname = 'public') as item - -UNION ALL - -SELECT - 'RESUMEN' as verification_type, - 'Tipos ENUM: ' || (SELECT COUNT(*) FROM pg_type WHERE typtype = 'e' AND typname IN ('user_role', 'customer_tier', 'booking_status', 'invitation_status', 'resource_type', 'audit_action')) as item; -``` - -**Resultado esperado:** -``` -verification_type | item -RESUMEN | Tablas: 8 -RESUMEN | Funciones: 14 -RESUMEN | Triggers: 17 -RESUMEN | Políticas RLS: 24 -RESUMEN | Tipos ENUM: 6 -``` - ---- - -## 🌱 Paso 2: Ejecutar Script de Seed de Datos - -### 2.1 Abrir Nuevo Query en SQL Editor - -1. En el mismo SQL Editor, haz clic en **"New query"** -2. O pestaña para separar la verificación del seed - -### 2.2 Ejecutar Seed por Secciones - -**Opción A: Ejecutar el archivo completo** -- Copia TODO el contenido de **`scripts/seed-data.sql`** -- Pega en el SQL Editor -- Haz clic en **"Run"** - -**Opción B: Ejecutar por secciones** (recomendado para ver el progreso) - -#### Sección 1: Crear Locations - -```sql --- 1. Crear Locations -INSERT INTO locations (name, timezone, address, phone, is_active) -VALUES - ('Salón Principal - Centro', 'America/Mexico_City', 'Av. Reforma 222, Centro Histórico, Ciudad de México', '+52 55 1234 5678', true), - ('Salón Norte - Polanco', 'America/Mexico_City', 'Av. Masaryk 123, Polanco, Ciudad de México', '+52 55 2345 6789', true), - ('Salón Sur - Coyoacán', 'America/Mexico_City', 'Calle Hidalgo 456, Coyoacán, Ciudad de México', '+52 55 3456 7890', true); - --- Verificar -SELECT 'Locations creadas:', COUNT(*) FROM locations; -``` - -**Resultado esperado:** -``` -Locations creadas: | 3 -``` - -#### Sección 2: Crear Resources - -```sql --- 2. Crear Resources -INSERT INTO resources (location_id, name, type, capacity, is_active) -SELECT - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - 'Estación ' || generate_series(1, 3)::TEXT, - 'station', - 1, - true -UNION ALL -SELECT - (SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), - 'Estación ' || generate_series(1, 2)::TEXT, - 'station', - 1, - true -UNION ALL -SELECT - (SELECT id FROM locations WHERE name = 'Salón Sur - Coyoacán' LIMIT 1), - 'Estación 1', - 'station', - 1, - true; - --- Verificar -SELECT 'Resources creadas:', COUNT(*) FROM resources; -``` - -**Resultado esperado:** -``` -Resources creadas: | 6 -``` - -#### Sección 3: Crear Staff - -```sql --- 3. Crear Staff -INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) -VALUES - -- Admin Principal - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'admin', 'Admin Principal', '+52 55 1111 2222', true), - -- Managers - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'manager', 'Manager Centro', '+52 55 2222 3333', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), 'manager', 'Manager Polanco', '+52 55 6666 7777', true), - -- Staff - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'staff', 'Staff Coordinadora', '+52 55 3333 4444', true), - -- Artists - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'artist', 'Artist María García', '+52 55 4444 5555', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'artist', 'Artist Ana Rodríguez', '+52 55 5555 6666', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), 'artist', 'Artist Carla López', '+52 55 7777 8888', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Sur - Coyoacán' LIMIT 1), 'artist', 'Artist Laura Martínez', '+52 55 8888 9999', true); - --- Verificar -SELECT 'Staff creados:', COUNT(*) FROM staff; -``` - -**Resultado esperado:** -``` -Staff creados: | 8 -``` - -#### Sección 4: Crear Services - -```sql --- 4. Crear Services -INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) -VALUES - ('Corte y Estilizado', 'Corte de cabello profesional con lavado y estilizado', 60, 500.00, false, false, true), - ('Color Completo', 'Tinte completo con protección capilar', 120, 1200.00, false, true, true), - ('Balayage Premium', 'Técnica de balayage con productos premium', 180, 2000.00, true, true, true), - ('Tratamiento Kératina', 'Tratamiento de kératina para cabello dañado', 90, 1500.00, false, false, true), - ('Peinado Evento', 'Peinado para eventos especiales', 45, 800.00, false, true, true), - ('Servicio Express (Dual Artist)', 'Servicio rápido con dos artists simultáneas', 30, 600.00, true, true, true); - --- Verificar -SELECT 'Services creados:', COUNT(*) FROM services; -``` - -**Resultado esperado:** -``` -Services creados: | 6 -``` - -#### Sección 5: Crear Customers - -```sql --- 5. Crear Customers -INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, notes, total_spent, total_visits, last_visit_date, is_active) -VALUES - (uuid_generate_v4(), 'Sofía', 'Ramírez', 'sofia.ramirez@example.com', '+52 55 1111 1111', 'gold', 'Cliente VIP. Prefiere Artists María y Ana.', 15000.00, 25, '2025-12-20', true), - (uuid_generate_v4(), 'Valentina', 'Hernández', 'valentina.hernandez@example.com', '+52 55 2222 2222', 'gold', 'Cliente regular. Prefiere horarios de la mañana.', 8500.00, 15, '2025-12-15', true), - (uuid_generate_v4(), 'Camila', 'López', 'camila.lopez@example.com', '+52 55 3333 3333', 'free', 'Nueva cliente. Referida por Valentina.', 500.00, 1, '2025-12-10', true), - (uuid_generate_v4(), 'Isabella', 'García', 'isabella.garcia@example.com', '+52 55 4444 4444', 'gold', 'Cliente VIP. Requiere servicio de Balayage.', 22000.00, 30, '2025-12-18', true); - --- Verificar -SELECT 'Customers creados:', COUNT(*) FROM customers; -``` - -**Resultado esperado:** -``` -Customers creados: | 4 -``` - -#### Sección 6: Crear Invitaciones - -```sql --- 6. Crear Invitaciones (para clientes Gold) --- Resetear invitaciones para clientes Gold de la semana actual -SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com' LIMIT 1)); -SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'valentina.hernandez@example.com' LIMIT 1)); -SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'isabella.garcia@example.com' LIMIT 1)); - --- Verificar -SELECT 'Invitaciones creadas:', COUNT(*) FROM invitations WHERE status = 'pending'; -``` - -**Resultado esperado:** -``` -Invitaciones creadas: | 15 -``` -*(5 por cada cliente Gold)* - -#### Sección 7: Crear Bookings de Prueba - -```sql --- 7. Crear Bookings de Prueba -INSERT INTO bookings ( - customer_id, - staff_id, - location_id, - resource_id, - service_id, - start_time_utc, - end_time_utc, - status, - deposit_amount, - total_amount, - is_paid, - payment_reference, - notes -) -SELECT - (SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist María García' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Balayage Premium' LIMIT 1), - NOW() + INTERVAL '1 day', - NOW() + INTERVAL '4 hours', - 'confirmed', - 200.00, - 2000.00, - true, - 'pay_test_001', - 'Balayage Premium para Sofía' -UNION ALL -SELECT - (SELECT id FROM customers WHERE email = 'valentina.hernandez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist Ana Rodríguez' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Color Completo' LIMIT 1), - NOW() + INTERVAL '2 days', - NOW() + INTERVAL '4 hours', - 'confirmed', - 200.00, - 1200.00, - true, - 'pay_test_002', - 'Color Completo para Valentina' -UNION ALL -SELECT - (SELECT id FROM customers WHERE email = 'camila.lopez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist María García' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Corte y Estilizado' LIMIT 1), - NOW() + INTERVAL '3 days', - NOW() + INTERVAL '1 hour', - 'confirmed', - 50.00, - 500.00, - true, - 'pay_test_003', - 'Primer corte para Camila' -UNION ALL -SELECT - (SELECT id FROM customers WHERE email = 'isabella.garcia@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist María García' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Servicio Express (Dual Artist)' LIMIT 1), - NOW() + INTERVAL '4 days', - NOW() + INTERVAL '30 minutes', - 'confirmed', - 200.00, - 600.00, - true, - 'pay_test_004', - 'Servicio Express Dual Artist - Necesita secondary_artist' -UNION ALL -SELECT - (SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist Ana Rodríguez' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) OFFSET 1 LIMIT 1), - (SELECT id FROM services WHERE name = 'Peinado Evento' LIMIT 1), - NOW() + INTERVAL '5 days', - NOW() + INTERVAL '45 minutes', - 'pending', - 200.00, - 800.00, - false, - NULL, - 'Peinado para evento especial'; - --- Verificar -SELECT 'Bookings creados:', COUNT(*) FROM bookings; -``` - -**Resultado esperado:** -``` -Bookings creados: | 5 -``` - -#### Sección 8: Actualizar Booking con Secondary Artist - -```sql --- 8. Actualizar booking con secondary_artist (prueba de validación) -UPDATE bookings -SET secondary_artist_id = (SELECT id FROM staff WHERE display_name = 'Artist Carla López' LIMIT 1) -WHERE payment_reference = 'pay_test_004'; - --- Verificar -SELECT 'Bookings con secondary_artist:', COUNT(*) FROM bookings WHERE secondary_artist_id IS NOT NULL; -``` - -**Resultado esperado:** -``` -Bookings con secondary_artist: | 1 -``` - -#### Sección 9: Resumen Final - -```sql --- 9. Resumen de datos creados -DO $$ -BEGIN - RAISE NOTICE '=========================================='; - RAISE NOTICE 'SALONOS - SEED DE DATOS COMPLETADO'; - RAISE NOTICE '=========================================='; - RAISE NOTICE 'Locations: %', (SELECT COUNT(*) FROM locations); - RAISE NOTICE 'Resources: %', (SELECT COUNT(*) FROM resources); - RAISE NOTICE 'Staff: %', (SELECT COUNT(*) FROM staff); - RAISE NOTICE 'Services: %', (SELECT COUNT(*) FROM services); - RAISE NOTICE 'Customers: %', (SELECT COUNT(*) FROM customers); - RAISE NOTICE 'Invitations: %', (SELECT COUNT(*) FROM invitations WHERE status = 'pending'); - RAISE NOTICE 'Bookings: %', (SELECT COUNT(*) FROM bookings); - RAISE NOTICE '=========================================='; - RAISE NOTICE '✅ Base de datos lista para desarrollo'; - RAISE NOTICE '=========================================='; -END -$$; -``` - -**Resultado esperado:** -``` -NOTICE: ========================================== -NOTICE: SALONOS - SEED DE DATOS COMPLETADO -NOTICE: ========================================== -NOTICE: Locations: 3 -NOTICE: Resources: 6 -NOTICE: Staff: 8 -NOTICE: Services: 6 -NOTICE: Customers: 4 -NOTICE: Invitations: 15 -NOTICE: Bookings: 5 -NOTICE: ========================================== -NOTICE: ✅ Base de datos lista para desarrollo -NOTICE: ========================================== -``` - ---- - -## 🧪 Paso 3: Pruebas Adicionales - -### Test 1: Verificar Bookings con Detalles - -```sql -SELECT - b.short_id, - c.first_name || ' ' || c.last_name as customer, - s.display_name as artist, - sa.display_name as secondary_artist, - svc.name as service, - b.start_time_utc, - b.end_time_utc, - b.status, - b.total_amount -FROM bookings b -JOIN customers c ON b.customer_id = c.id -JOIN staff s ON b.staff_id = s.id -LEFT JOIN staff sa ON b.secondary_artist_id = sa.id -JOIN services svc ON b.service_id = svc.id -ORDER BY b.start_time_utc; -``` - -### Test 2: Verificar Invitaciones - -```sql -SELECT - i.code, - inv.first_name || ' ' || inv.last_name as inviter, - i.status, - i.week_start_date, - i.expiry_date -FROM invitations i -JOIN customers inv ON i.inviter_id = inv.id -WHERE i.status = 'pending' -ORDER BY inv.first_name, i.expiry_date; -``` - -### Test 3: Verificar Staff por Ubicación y Rol - -```sql -SELECT - l.name as location, - s.role, - s.display_name, - s.phone, - s.is_active -FROM staff s -JOIN locations l ON s.location_id = l.id -ORDER BY l.name, s.role, s.display_name; -``` - -### Test 4: Verificar Auditoría - -```sql -SELECT - entity_type, - action, - new_values->>'operation' as operation, - new_values->>'table_name' as table_name, - created_at -FROM audit_logs -WHERE new_values->>'table_name' = 'invitations' -ORDER BY created_at DESC -LIMIT 10; -``` - ---- - -## ✅ Checklist de Verificación - -Después de completar todos los pasos, asegúrate de: - -### Verificación de Migraciones -- [x] 8 tablas creadas (locations, resources, staff, services, customers, invitations, bookings, audit_logs) -- [x] 14 funciones creadas -- [x] 17+ triggers activos -- [x] 20+ políticas RLS configuradas -- [x] 6 tipos ENUM creados -- [x] Short ID generable -- [x] Código de invitación generable - -### Verificación de Seed de Datos -- [ ] 3 locations creadas -- [ ] 6 resources creadas -- [ ] 8 staff creados -- [ ] 6 services creados -- [ ] 4 customers creados -- [ ] 15 invitaciones creadas (5 por cliente Gold) -- [ ] 5 bookings creados -- [ ] 1 booking con secondary_artist - -### Pruebas Funcionales -- [ ] Short ID se genera correctamente -- [ ] Código de invitación se genera correctamente -- [ ] Bookings se crean con short_id automático -- [ ] Secondary artist validation funciona -- [ ] Auditoría se registra correctamente -- [ ] Reset de invitaciones funciona - ---- - -## 🚨 Troubleshooting - -### Error: "relation already exists" - -**Causa:** Ya ejecutaste esta sección anteriormente. - -**Solución:** Continúa con la siguiente sección. Los datos ya existen. - -### Error: "null value in column violates not-null constraint" - -**Causa:** Falta crear datos dependientes primero. - -**Solución:** Ejecuta las secciones en orden: Locations → Resources → Staff → Services → Customers → Invitations → Bookings - -### Error: "insert or update on table violates foreign key constraint" - -**Causa:** Estás intentando insertar un booking con un customer_id que no existe. - -**Solución:** Verifica que el customer exista antes de crear el booking: - -```sql -SELECT * FROM customers WHERE email = 'sofia.ramirez@example.com'; -``` - ---- - -## 📚 Documentación Adicional - -- **docs/POST_MIGRATION_SUCCESS.md** - Guía general post-migración -- **scripts/verify-migration.sql** - Script completo de verificación -- **scripts/seed-data.sql** - Script completo de seed -- **FASE_1_STATUS.md** - Estado actualizado de la Fase 1 - ---- - -**¿Listo para continuar con la configuración de Auth en Supabase Dashboard?** diff --git a/docs/SUPABASE_DASHBOARD_MIGRATION.md b/docs/SUPABASE_DASHBOARD_MIGRATION.md deleted file mode 100644 index 5ab99d3..0000000 --- a/docs/SUPABASE_DASHBOARD_MIGRATION.md +++ /dev/null @@ -1,322 +0,0 @@ -# 🚀 Guía de Ejecución de Migraciones - Supabase Dashboard - -## ⚠️ Situación Actual - -No es posible ejecutar las migraciones directamente desde la línea de comandos en este entorno debido a restricciones de red (el puerto 5432 no es accesible). - -## ✅ Solución: Ejecutar desde Supabase Dashboard - -Esta es la forma más segura y confiable de ejecutar las migraciones. - -**Nota:** Se ha corregido un error en la migración original. PostgreSQL no permite subqueries en constraints CHECK. Se ha reemplazado el constraint problemático con un trigger de validación. Usa el archivo `00_FULL_MIGRATION_CORRECTED.sql`. - ---- - -## 📋 PASOS A SEGUIR - -### Paso 1: Abrir Supabase SQL Editor - -1. Ve a: **https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/sql** - -2. Haz clic en **"New query"** para abrir un editor SQL vacío. - -### Paso 2: Copiar el contenido del archivo de migración - -El archivo consolidado corregido está en: -``` -db/migrations/00_FULL_MIGRATION_CORRECTED.sql -``` - -Copia **TODO** el contenido de este archivo. - -### Paso 3: Pegar y ejecutar en Supabase Dashboard - -1. Pega el contenido completo del archivo en el editor SQL. -2. Haz clic en el botón **"Run"** (o presiona `Ctrl+Enter` / `Cmd+Enter`). -3. Espera a que se complete la ejecución (puede tardar 10-30 segundos). - -### Paso 4: Verificar la ejecución - -Al finalizar, deberías ver: -- ✅ Un mensaje de éxito -- ✅ Notificaciones sobre la creación de tablas, funciones y triggers -- ✅ Un resumen de lo que se ha creado: - - 8 tablas - - 13 funciones - - 15+ triggers - - 20+ políticas RLS - - 6 tipos ENUM - ---- - -## 🔍 Verificación Manual - -Si deseas verificar que todo se creó correctamente, ejecuta estas consultas en el SQL Editor: - -### Verificar Tablas - -```sql -SELECT table_name -FROM information_schema.tables -WHERE table_schema = 'public' -ORDER BY table_name; -``` - -**Esperado:** 8 tablas (locations, resources, staff, services, customers, invitations, bookings, audit_logs) - -### Verificar Funciones - -```sql -SELECT routine_name -FROM information_schema.routines -WHERE routine_schema = 'public' -ORDER BY routine_name; -``` - -**Esperado:** 14 funciones incluyendo `generate_short_id`, `reset_weekly_invitations_for_customer`, `validate_secondary_artist_role`, etc. - -### Verificar Triggers - -```sql -SELECT trigger_name, event_object_table -FROM information_schema.triggers -WHERE trigger_schema = 'public' -ORDER BY event_object_table, trigger_name; -``` - -**Esperado:** Múltiples triggers para auditoría y timestamps - -### Verificar Políticas RLS - -```sql -SELECT schemaname, tablename, policyname, permissive, roles, cmd -FROM pg_policies -WHERE schemaname = 'public' -ORDER BY tablename, policyname; -``` - -**Esperado:** 20+ políticas por rol (admin, manager, staff, artist, customer) - -### Probar Generación de Short ID - -```sql -SELECT generate_short_id(); -``` - -**Esperado:** Un string de 6 caracteres alfanuméricos (ej: "A3F7X2") - -### Probar Generación de Código de Invitación - -```sql -SELECT generate_invitation_code(); -``` - -**Esperado:** Un string de 10 caracteres alfanuméricos (ej: "X9J4K2M5N8") - -### Verificar Tipos ENUM - -```sql -SELECT typname, enumlabel -FROM pg_enum e -JOIN pg_type t ON e.enumtypid = t.oid -WHERE t.typtype = 'e' -ORDER BY t.typname, e.enumsortorder; -``` - -**Esperado:** 6 tipos ENUM (user_role, customer_tier, booking_status, invitation_status, resource_type, audit_action) - ---- - -## 🎯 Próximos Pasos (Después de las Migraciones) - -### 1. Configurar Auth en Supabase Dashboard - -Ve a **Authentication → Providers** y configura: - -1. **Email Provider**: Habilitar email authentication -2. **SMS Provider**: Configurar Twilio para SMS (opcional) -3. **Email Templates**: Personalizar templates de Magic Link - -### 2. Crear Usuarios de Prueba - -Ve a **Authentication → Users** y crea: - -- **1 Admin**: Para acceso total -- **1 Manager**: Para gestión operacional -- **1 Staff**: Para coordinación -- **1 Artist**: Para ejecución de servicios -- **1 Customer Free**: Para clientes regulares -- **1 Customer Gold**: Para clientes VIP - -### 3. Asignar Roles a Staff - -Ejecuta este SQL en el editor para crear staff de prueba: - -```sql --- Insertar admin -INSERT INTO staff (user_id, location_id, role, display_name, is_active) -VALUES ('UUID_DEL_USUARIO_ADMIN', 'LOCATION_UUID', 'admin', 'Admin Principal', true); - --- Insertar manager -INSERT INTO staff (user_id, location_id, role, display_name, is_active) -VALUES ('UUID_DEL_USUARIO_MANAGER', 'LOCATION_UUID', 'manager', 'Manager Centro', true); - --- Insertar staff -INSERT INTO staff (user_id, location_id, role, display_name, is_active) -VALUES ('UUID_DEL_USUARIO_STAFF', 'LOCATION_UUID', 'staff', 'Staff Coordinadora', true); - --- Insertar artist -INSERT INTO staff (user_id, location_id, role, display_name, is_active) -VALUES ('UUID_DEL_USUARIO_ARTIST', 'LOCATION_UUID', 'artist', 'Artist María García', true); -``` - -### 4. Crear Datos de Prueba - -Opcionalmente, puedes ejecutar el script de seed desde la línea de comandos (si tienes acceso): - -```bash -npm run db:seed -``` - -O manualmente desde el SQL Editor: - -```sql --- Crear una location de prueba -INSERT INTO locations (name, timezone, address, phone, is_active) -VALUES ('Salón Principal - Centro', 'America/Mexico_City', 'Av. Reforma 222', '+52 55 1234 5678', true); - --- Crear un servicio de prueba -INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) -VALUES ('Corte y Estilizado', 'Corte de cabello profesional', 60, 500.00, false, false, true); - --- Crear un customer de prueba -INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, is_active) -VALUES ('UUID_DEL_USUARIO', 'Sofía', 'Ramírez', 'sofia@example.com', '+52 55 1111 2222', 'gold', true); -``` - -### 5. Probar el Sistema - -Una vez que tengas datos de prueba, puedes: - -1. **Probar Short ID**: - ```sql - SELECT generate_short_id(); - ``` - -2. **Probar Código de Invitación**: - ```sql - SELECT generate_invitation_code(); - ``` - -3. **Probar Reset de Invitaciones**: - ```sql - SELECT reset_weekly_invitations_for_customer('CUSTOMER_UUID'); - ``` - -4. **Crear un Booking**: - ```sql - INSERT INTO bookings (customer_id, staff_id, location_id, resource_id, service_id, start_time_utc, end_time_utc, status, deposit_amount, total_amount, is_paid) - VALUES ('CUSTOMER_UUID', 'STAFF_UUID', 'LOCATION_UUID', 'RESOURCE_UUID', 'SERVICE_UUID', NOW() + INTERVAL '1 day', NOW() + INTERVAL '2 days', 'confirmed', 200.00, 500.00, true); - ``` - -5. **Verificar Auditoría**: - ```sql - SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 10; - ``` - ---- - -## 🚨 Solución de Problemas - -### Error: "relation already exists" - -**Causa:** Las tablas ya existen. La migración se ejecutó parcialmente o anteriormente. - -**Solución:** Continúa con la ejecución o limpia la base de datos: - -```sql -DROP TABLE IF EXISTS audit_logs CASCADE; -DROP TABLE IF EXISTS bookings CASCADE; -DROP TABLE IF EXISTS invitations CASCADE; -DROP TABLE IF EXISTS customers CASCADE; -DROP TABLE IF EXISTS services CASCADE; -DROP TABLE IF EXISTS staff CASCADE; -DROP TABLE IF EXISTS resources CASCADE; -DROP TABLE IF EXISTS locations CASCADE; -``` - -Luego ejecuta la migración nuevamente. - -### Error: "function already exists" - -**Causa:** Las funciones ya existen. - -**Solución:** Esto es normal si estás reejecutando la migración. Los nuevos `CREATE OR REPLACE FUNCTION` no fallarán. - -### Error: RLS no funciona - -**Causa:** RLS no está habilitado o el usuario no tiene un rol asignado. - -**Solución:** - -1. Verifica que RLS está habilitado: - ```sql - SELECT tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public'; - ``` - -2. Verifica que el usuario tenga un registro en `staff` o `customers`: - ```sql - SELECT * FROM staff WHERE user_id = auth.uid(); - SELECT * FROM customers WHERE user_id = auth.uid(); - ``` - -3. Verifica las políticas RLS: - ```sql - SELECT * FROM pg_policies WHERE schemaname = 'public'; - ``` - ---- - -## 📚 Documentación Adicional - -- **PRD.md:** Reglas de negocio del sistema -- **TASKS.md:** Plan de ejecución por fases -- **AGENTS.md:** Roles y responsabilidades de IA -- **docs/MIGRATION_GUIDE.md:** Guía técnica de migraciones -- **db/migrations/README.md:** Documentación técnica de migraciones - ---- - -## ✅ Checklist de Verificación - -Antes de continuar con el desarrollo, asegúrate de: - -- [ ] Las 8 tablas están creadas -- [ ] Las 13 funciones están creadas -- [ ] Los 15+ triggers están activos -- [ ] Las 20+ políticas RLS están configuradas -- [ ] Los 6 tipos ENUM están creados -- [ ] `generate_short_id()` funciona -- [ ] `generate_invitation_code()` funciona -- [ ] `reset_weekly_invitations_for_customer()` funciona -- [ ] Auth está configurado -- [ ] Usuarios de prueba están creados -- [ ] Staff de prueba está creado con roles correctos -- [ ] Se puede crear un booking manualmente -- [ ] La auditoría se está registrando correctamente - ---- - -## 🎉 ¡Listo para el Desarrollo! - -Una vez que hayas completado todos estos pasos, tu base de datos de SalonOS estará lista para el desarrollo de: - -- **Tarea 1.3:** Short ID & Invitaciones (backend endpoints) -- **Tarea 1.4:** CRM Base (endpoints CRUD) -- **Fase 2:** Motor de Agendamiento -- **Fase 3:** Pagos y Protección -- **Fase 4:** HQ Dashboard - ---- - -**¿Necesitas ayuda con algún paso específico?** Puedo proporcionarte consultas SQL adicionales o ayuda para configurar los usuarios de prueba. diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index 3782df3..0000000 --- a/scripts/README.md +++ /dev/null @@ -1,342 +0,0 @@ -# 🚀 Scripts Simples - SalonOS - -Este directorio contiene scripts simplificados para facilitar el setup de SalonOS. - ---- - -## 📋 Scripts Disponibles - -### 1. check-connection.sh -**Qué hace:** Verifica la conexión a Supabase y si el puerto 5432 está abierto. - -**Cómo ejecutar:** -```bash -./scripts/check-connection.sh -``` - -**Output esperado:** -``` -✅ psql instalado -✅ Host alcanzable -✅ Puerto 5432 está abierto -✅ Conexión a base de datos exitosa -✅ Tablas encontradas: 8/8 -✅ Funciones encontradas: 14 -``` - -**Si falla:** -- Si el puerto está bloqueado, usa Supabase Dashboard -- Si falla la conexión, verifica las credenciales - ---- - -### 2. simple-verify.sh -**Qué hace:** Verifica que todas las migraciones están correctas. - -**Cómo ejecutar:** -```bash -./scripts/simple-verify.sh -``` - -**Output esperado:** -``` -📊 Verificando tablas... -✅ Tablas: 8/8 - -📊 Verificando funciones... -✅ Funciones: 14/14 - -📊 Verificando triggers... -✅ Triggers: 17+/17+ - -📊 Verificando políticas RLS... -✅ Políticas RLS: 24+/20+ - -📊 Probando generación de Short ID... -✅ Short ID: A3F7X2 (6 caracteres) - -📊 Probando generación de código de invitación... -✅ Código de invitación: X9J4K2M5N8 (10 caracteres) - -========================================== -RESUMEN -========================================== -🎉 TODAS LAS MIGRACIONES ESTÁN CORRECTAS -========================================== -``` - ---- - -### 3. simple-seed.sh -**Qué hace:** Crea todos los datos de prueba en la base de datos. - -**Cómo ejecutar:** -```bash -./scripts/simple-seed.sh -``` - -**Output esperado:** -``` -📍 Creando locations... -✅ Locations: 3/3 - -🪑 Creando resources... -✅ Resources: 6/6 - -👥 Creando staff... -✅ Staff: 8/8 - -💇 Creando services... -✅ Services: 6/6 - -👩 Creando customers... -✅ Customers: 4/4 - -💌 Creando invitations... -✅ Invitaciones: 15/15 - -📅 Creando bookings... -✅ Bookings: 5/5 - -========================================== -RESUMEN -========================================== -🎉 SEED DE DATOS COMPLETADO EXITOSAMENTE -========================================== -``` - ---- - -### 4. create-auth-users.js -**Qué hace:** Crea usuarios de staff y customers en Supabase Auth automáticamente. - -**Requiere:** `npm install @supabase/supabase-js` - -**Cómo ejecutar:** -```bash -node scripts/create-auth-users.js -``` - -**Output esperado:** -``` -👥 Creando usuarios de staff (8 usuarios)... - -✅ Admin Principal creado (ID: ...) -✅ Manager Centro creado (ID: ...) -✅ Manager Polanco creado (ID: ...) -✅ Staff Coordinadora creado (ID: ...) -✅ Artist María García creado (ID: ...) -✅ Artist Ana Rodríguez creado (ID: ...) -✅ Artist Carla López creado (ID: ...) -✅ Artist Laura Martínez creado (ID: ...) -✅ Usuarios de staff creados: 8/8 - -🔄 Actualizando tabla staff con user_ids... - -✅ Admin Principal actualizado con user_id -✅ Manager Centro actualizado con user_id -... -✅ Staff actualizados: 8/8 - -👩 Creando usuarios de customers (4 usuarios)... - -✅ Sofía Ramírez creado (ID: ...) -✅ Valentina Hernández creado (ID: ...) -✅ Camila López creado (ID: ...) -✅ Isabella García creado (ID: ...) -✅ Usuarios de customers creados: 4/4 - -🔄 Actualizando tabla customers con user_ids... - -✅ Sofía Ramírez actualizado con user_id -... -✅ Customers actualizados: 4/4 - -========================================== -RESUMEN FINAL -========================================== -Staff creados: 8/8 -Staff actualizados: 8/8 -Customers creados: 4/4 -Customers actualizados: 4/4 -========================================== - -🎉 TODOS LOS USUARIOS HAN SIDO CREADOS Y ACTUALIZADOS - -📝 Credenciales de prueba: - -ADMIN: - Email: admin@salonos.com - Password: Admin123! - -CUSTOMER (Gold): - Email: sofia.ramirez@example.com - Password: Customer123! -``` - ---- - -## 🚨 Si el Puerto 5432 Está Bloqueado - -Si ejecutas `check-connection.sh` y el puerto está bloqueado, tienes estas opciones: - -### Opción A: Usar Supabase Dashboard (Recomendado) -1. Ve a: https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/sql -2. Copia el contenido de: `db/migrations/00_FULL_MIGRATION_FINAL.sql` -3. Pega en el SQL Editor -4. Haz clic en "Run" - -### Opción B: Usar SQL desde Dashboard -Para el seed, ejecuta estas consultas una por una: - -**Crear locations:** -```sql -INSERT INTO locations (name, timezone, address, phone, is_active) -VALUES - ('Salón Principal - Centro', 'America/Mexico_City', 'Av. Reforma 222', '+52 55 1234 5678', true), - ('Salón Norte - Polanco', 'America/Mexico_City', 'Av. Masaryk 123', '+52 55 2345 6789', true), - ('Salón Sur - Coyoacán', 'America/Mexico_City', 'Calle Hidalgo 456', '+52 55 3456 7890', true); -``` - -**Crear staff:** -```sql -INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) -VALUES - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'admin', 'Admin Principal', '+52 55 1111 2222', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'manager', 'Manager Centro', '+52 55 2222 3333', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'artist', 'Artist María García', '+52 55 4444 5555', true); -``` - -**Crear usuarios de Auth manualmente:** -1. Ve a: https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/auth/users -2. Haz clic en "Add user" -3. Crea los usuarios con los emails de `scripts/create-auth-users.js` - ---- - -## 📝 Flujo de Ejecución Recomendado - -### Si el puerto 5432 está ABIERTO: - -```bash -# 1. Verificar conexión -./scripts/check-connection.sh - -# 2. Verificar migraciones -./scripts/simple-verify.sh - -# 3. Crear datos de prueba -./scripts/simple-seed.sh - -# 4. Crear usuarios de Auth -node scripts/create-auth-users.js -``` - -### Si el puerto 5432 está BLOQUEADO: - -```bash -# 1. Verificar conexión -./scripts/check-connection.sh - -# Esto te dirá que el puerto está bloqueado -# Entonces usa Supabase Dashboard -``` - -**En Supabase Dashboard:** -1. Ve a: https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/sql -2. Copia el contenido de: `db/migrations/00_FULL_MIGRATION_FINAL.sql` -3. Pega en el SQL Editor -4. Haz clic en "Run" -5. Para el seed, ejecuta las consultas de `scripts/simple-seed.sh` una por una -6. Para crear usuarios, usa el Dashboard manualmente - ---- - -## 🔧 Troubleshooting - -### Error: "psql: command not found" -**Solución:** Instala PostgreSQL client -- macOS: `brew install postgresql` -- Ubuntu/Debian: `sudo apt-get install postgresql-client` -- Windows: Descargar desde https://www.postgresql.org/download/windows/ - -### Error: "connection to server failed" -**Solución:** -1. Verifica que las variables de entorno estén en `.env.local` -2. Verifica que el puerto 5432 no esté bloqueado -3. Si está bloqueado, usa Supabase Dashboard - -### Error: "Password authentication failed" -**Solución:** -1. Verifica que `SUPABASE_SERVICE_ROLE_KEY` sea correcto -2. Verifica que no tenga espacios o caracteres especiales -3. Regenera el key en Supabase Dashboard si es necesario - -### Error: "relation already exists" -**Solución:** -- Los datos ya existen. Continúa con el siguiente script -- O elimina y recrea la base de datos - -### Error: "User already registered" -**Solución:** -- El usuario ya existe en Supabase Auth -- Borra el usuario en Supabase Dashboard y vuelve a ejecutar el script - ---- - -## 📚 Documentación Adicional - -- **`docs/STEP_BY_STEP_VERIFICATION.md`** - Guía detallada paso a paso -- **`docs/STEP_BY_STEP_AUTH_CONFIG.md`** - Guía de configuración de Auth -- **`docs/POST_MIGRATION_SUCCESS.md`** - Guía post-migración -- **`docs/QUICK_START_POST_MIGRATION.md`** - Guía rápida de referencia - ---- - -## ✅ Checklist - -### Verificar Conexión -- [ ] `check-connection.sh` ejecutado -- [ ] Puerto 5432 abierto (o usar Dashboard) -- [ ] Conexión a DB exitosa - -### Verificar Migraciones -- [ ] `simple-verify.sh` ejecutado -- [ ] Todas las tablas creadas (8/8) -- [ ] Todas las funciones creadas (14/14) -- [ ] Todos los triggers activos (17+) - -### Seed de Datos -- [ ] `simple-seed.sh` ejecutado -- [ ] Locations creadas (3/3) -- [ ] Resources creados (6/6) -- [ ] Staff creado (8/8) -- [ ] Services creados (6/6) -- [ ] Customers creados (4/4) -- [ ] Invitaciones creadas (15/15) -- [ ] Bookings creados (5/5) - -### Crear Usuarios Auth -- [ ] `create-auth-users.js` ejecutado -- [ ] Staff creados (8/8) -- [ ] Staff actualizados (8/8) -- [ ] Customers creados (4/4) -- [ ] Customers actualizados (4/4) - ---- - -## 🎯 Próximos Pasos - -Después de completar todos los scripts: - -1. **Probar login** con las credenciales: - - Admin: `admin@salonos.com` / `Admin123!` - - Customer: `sofia.ramirez@example.com` / `Customer123!` - -2. **Verificar políticas RLS** en Supabase Dashboard - -3. **Continuar con el desarrollo** de la aplicación - ---- - -**¿Necesitas ayuda con alguno de los scripts?** diff --git a/scripts/check-connection.sh b/scripts/check-connection.sh deleted file mode 100755 index 01a25ce..0000000 --- a/scripts/check-connection.sh +++ /dev/null @@ -1,157 +0,0 @@ -#!/bin/bash - -# Script para verificar conexión a Supabase y desbloquear puertos -# Ejecutar con: ./scripts/check-connection.sh - -echo "==========================================" -echo "SALONOS - VERIFICACIÓN DE CONEXIÓN" -echo "==========================================" -echo "" - -# Cargar variables de entorno -set -a -source .env.local -set +a - -if [ -z "$NEXT_PUBLIC_SUPABASE_URL" ] || [ -z "$SUPABASE_SERVICE_ROLE_KEY" ]; then - echo "❌ ERROR: Faltan variables de entorno" - echo "Asegúrate de tener NEXT_PUBLIC_SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY en .env.local" - exit 1 -fi - -# Extraer host de la URL -DB_HOST="${NEXT_PUBLIC_SUPABASE_URL#https://}" - -echo "📊 Información de conexión:" -echo " Host: $DB_HOST" -echo " Puerto: 5432" -echo "" - -# 1. Verificar si psql está instalado -echo "1️⃣ Verificando si psql está instalado..." -if command -v psql &> /dev/null; then - PSQL_VERSION=$(psql --version) - echo " ✅ psql instalado: $PSQL_VERSION" -else - echo " ❌ psql NO está instalado" - echo "" - echo " Para instalar psql:" - echo " - macOS: brew install postgresql" - echo " - Ubuntu/Debian: sudo apt-get install postgresql-client" - echo " - Windows: Descargar desde https://www.postgresql.org/download/windows/" - exit 1 -fi - -echo "" - -# 2. Verificar conectividad con ping -echo "2️⃣ Verificando conectividad con ping..." -if ping -c 2 -4 $DB_HOST &> /dev/null; then - echo " ✅ Host alcanzable" -else - echo " ❌ Host NO alcanzable" - echo " Verifica tu conexión a internet" - exit 1 -fi - -echo "" - -# 3. Verificar si el puerto 5432 está abierto -echo "3️⃣ Verificando si el puerto 5432 está abierto..." -if command -v nc &> /dev/null; then - if nc -z -w5 $DB_HOST 5432 2>/dev/null; then - echo " ✅ Puerto 5432 está abierto" - else - echo " ❌ Puerto 5432 está bloqueado" - echo "" - echo " 📋 SOLUCIÓN:" - echo " El puerto 5432 está bloqueado, posiblemente por:" - echo " 1. Firewall de tu empresa/ISP" - echo " 2. VPN corporativa" - echo " 3. Configuración de red local" - echo "" - echo " Opciones:" - echo " a. Usar Supabase Dashboard (recomendado)" - echo " b. Configurar VPN para permitir el puerto 5432" - echo " c. Usar túnel SSH para bypass del firewall" - echo " d. Contactar a tu administrador de red" - exit 1 - fi -else - echo " ⚠️ nc no está disponible, no se puede verificar el puerto" - echo " Continuando con la prueba de conexión..." -fi - -echo "" - -# 4. Configurar DATABASE_URL -DB_URL="postgresql://postgres:${SUPABASE_SERVICE_ROLE_KEY}@${DB_HOST}:5432/postgres" - -# 5. Probar conexión a la base de datos -echo "4️⃣ Probar conexión a la base de datos..." -if psql "$DB_URL" -c "SELECT 'Connection successful' as status;" &> /dev/null; then - echo " ✅ Conexión a base de datos exitosa" -else - echo " ❌ Conexión a base de datos fallida" - echo "" - echo " 📋 SOLUCIÓN:" - echo " 1. Verifica que las credenciales sean correctas" - echo " 2. Verifica que el proyecto de Supabase esté activo" - echo " 3. Verifica que el service_role_key sea correcto" - echo " 4. Si el puerto está bloqueado, usa Supabase Dashboard" - exit 1 -fi - -echo "" - -# 6. Verificar tablas -echo "5️⃣ Verificando tablas..." -TABLE_COUNT=$(psql "$DB_URL" -t -c " - SELECT COUNT(*) - FROM information_schema.tables - WHERE table_schema = 'public' - AND table_name IN ('locations', 'resources', 'staff', 'services', 'customers', 'invitations', 'bookings', 'audit_logs'); -") - -echo " ✅ Tablas encontradas: $TABLE_COUNT/8" - -# 7. Verificar funciones -echo "6️⃣ Verificando funciones..." -FUNC_COUNT=$(psql "$DB_URL" -t -c " - SELECT COUNT(*) - FROM information_schema.routines - WHERE routine_schema = 'public'; -") - -echo " ✅ Funciones encontradas: $FUNC_COUNT" - -echo "" - -# 8. Resumen -echo "==========================================" -echo "RESUMEN" -echo "==========================================" -echo "Host: $DB_HOST" -echo "Puerto: 5432" -echo "psql: ✅ Instalado" -echo "Conexión: ✅ Exitosa" -echo "Tablas: $TABLE_COUNT/8" -echo "Funciones: $FUNC_COUNT" -echo "==========================================" - -if [ "$TABLE_COUNT" -eq 8 ] && [ "$FUNC_COUNT" -ge 14 ]; then - echo "" - echo "🎉 CONEXIÓN VERIFICADA EXITOSAMENTE" - echo "" - echo "Próximos pasos:" - echo "1. Ejecutar: ./scripts/simple-verify.sh" - echo "2. Ejecutar: ./scripts/simple-seed.sh" - echo "3. Ejecutar: node scripts/create-auth-users.js" - echo "" - echo "O usar Supabase Dashboard:" - echo "https://supabase.com/dashboard/project/pvvwbnybkadhreuqijsl/sql" -else - echo "" - echo "⚠️ ALGUNOS ELEMENTOS FALTAN" - echo "Por favor, ejecuta las migraciones nuevamente" -fi diff --git a/scripts/create-auth-users.js b/scripts/create-auth-users.js deleted file mode 100644 index 57fec2d..0000000 --- a/scripts/create-auth-users.js +++ /dev/null @@ -1,330 +0,0 @@ -#!/usr/bin/env node - -/** - * Script simple para crear usuarios de Auth en Supabase - * Ejecutar con: node scripts/create-auth-users.js - * Requiere: npm install @supabase/supabase-js - */ - -require('dotenv').config({ path: '.env.local' }) -const { createClient } = require('@supabase/supabase-js') - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL -const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY - -if (!supabaseUrl || !supabaseServiceKey) { - console.error('❌ ERROR: Faltan variables de entorno') - console.error('Asegúrate de tener NEXT_PUBLIC_SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY en .env.local') - process.exit(1) -} - -const supabase = createClient(supabaseUrl, supabaseServiceKey) - -// Usuarios de staff -const staffUsers = [ - { - email: 'admin@salonos.com', - password: 'Admin123!', - role: 'admin', - display_name: 'Admin Principal', - phone: '+52 55 1111 2222', - location: 'Salón Principal - Centro' - }, - { - email: 'manager.centro@salonos.com', - password: 'Manager123!', - role: 'manager', - display_name: 'Manager Centro', - phone: '+52 55 2222 3333', - location: 'Salón Principal - Centro' - }, - { - email: 'manager.polanco@salonos.com', - password: 'Manager123!', - role: 'manager', - display_name: 'Manager Polanco', - phone: '+52 55 6666 7777', - location: 'Salón Norte - Polanco' - }, - { - email: 'staff.coordinadora@salonos.com', - password: 'Staff123!', - role: 'staff', - display_name: 'Staff Coordinadora', - phone: '+52 55 3333 4444', - location: 'Salón Principal - Centro' - }, - { - email: 'artist.maria@salonos.com', - password: 'Artist123!', - role: 'artist', - display_name: 'Artist María García', - phone: '+52 55 4444 5555', - location: 'Salón Principal - Centro' - }, - { - email: 'artist.ana@salonos.com', - password: 'Artist123!', - role: 'artist', - display_name: 'Artist Ana Rodríguez', - phone: '+52 55 5555 6666', - location: 'Salón Principal - Centro' - }, - { - email: 'artist.carla@salonos.com', - password: 'Artist123!', - role: 'artist', - display_name: 'Artist Carla López', - phone: '+52 55 7777 8888', - location: 'Salón Norte - Polanco' - }, - { - email: 'artist.laura@salonos.com', - password: 'Artist123!', - role: 'artist', - display_name: 'Artist Laura Martínez', - phone: '+52 55 8888 9999', - location: 'Salón Sur - Coyoacán' - } -] - -// Usuarios de customers -const customerUsers = [ - { - email: 'sofia.ramirez@example.com', - password: 'Customer123!', - tier: 'gold', - display_name: 'Sofía Ramírez' - }, - { - email: 'valentina.hernandez@example.com', - password: 'Customer123!', - tier: 'gold', - display_name: 'Valentina Hernández' - }, - { - email: 'camila.lopez@example.com', - password: 'Customer123!', - tier: 'free', - display_name: 'Camila López' - }, - { - email: 'isabella.garcia@example.com', - password: 'Customer123!', - tier: 'gold', - display_name: 'Isabella García' - } -] - -async function createStaffUser(user) { - try { - // Crear usuario en Supabase Auth - const { data, error } = await supabase.auth.admin.createUser({ - email: user.email, - password: user.password, - email_confirm: true, - user_metadata: { - role: user.role, - display_name: user.display_name, - location: user.location, - phone: user.phone - } - }) - - if (error) { - console.error(`❌ Error creando ${user.display_name}:`, error.message) - return null - } - - console.log(`✅ ${user.display_name} creado (ID: ${data.user.id})`) - return data.user - - } catch (error) { - console.error(`❌ Error inesperado creando ${user.display_name}:`, error.message) - return null - } -} - -async function createCustomerUser(user) { - try { - // Crear usuario en Supabase Auth - const { data, error } = await supabase.auth.admin.createUser({ - email: user.email, - password: user.password, - email_confirm: true, - user_metadata: { - tier: user.tier, - display_name: user.display_name - } - }) - - if (error) { - console.error(`❌ Error creando ${user.display_name}:`, error.message) - return null - } - - console.log(`✅ ${user.display_name} creado (ID: ${data.user.id})`) - return data.user - - } catch (error) { - console.error(`❌ Error inesperado creando ${user.display_name}:`, error.message) - return null - } -} - -async function updateStaffUserId(user) { - try { - const { error } = await supabase - .from('staff') - .update({ user_id: user.id }) - .eq('display_name', user.display_name) - - if (error) { - console.error(`❌ Error actualizando ${user.display_name}:`, error.message) - return false - } - - console.log(`✅ ${user.display_name} actualizado con user_id`) - return true - - } catch (error) { - console.error(`❌ Error inesperado actualizando ${user.display_name}:`, error.message) - return false - } -} - -async function updateCustomerUserId(user) { - try { - const { error } = await supabase - .from('customers') - .update({ user_id: user.id }) - .eq('email', user.email) - - if (error) { - console.error(`❌ Error actualizando ${user.display_name}:`, error.message) - return false - } - - console.log(`✅ ${user.display_name} actualizado con user_id`) - return true - - } catch (error) { - console.error(`❌ Error inesperado actualizando ${user.display_name}:`, error.message) - return false - } -} - -async function main() { - console.log('==========================================') - console.log('SALONOS - CREACIÓN DE USUARIOS AUTH') - console.log('==========================================') - console.log() - - // 1. Crear usuarios de staff - console.log('👥 Creando usuarios de staff (8 usuarios)...') - console.log() - - const createdStaff = [] - for (const user of staffUsers) { - const createdUser = await createStaffUser(user) - if (createdUser) { - createdStaff.push({ - ...user, - id: createdUser.id - }) - } - // Pequeña pausa para evitar rate limiting - await new Promise(resolve => setTimeout(resolve, 500)) - } - - console.log() - console.log(`✅ Usuarios de staff creados: ${createdStaff.length}/8`) - - // 2. Actualizar tabla staff con user_ids - console.log() - console.log('🔄 Actualizando tabla staff con user_ids...') - console.log() - - let updatedStaffCount = 0 - for (const user of createdStaff) { - const updated = await updateStaffUserId(user) - if (updated) { - updatedStaffCount++ - } - await new Promise(resolve => setTimeout(resolve, 200)) - } - - console.log() - console.log(`✅ Staff actualizados: ${updatedStaffCount}/8`) - - // 3. Crear usuarios de customers - console.log() - console.log('👩 Creando usuarios de customers (4 usuarios)...') - console.log() - - const createdCustomers = [] - for (const user of customerUsers) { - const createdUser = await createCustomerUser(user) - if (createdUser) { - createdCustomers.push({ - ...user, - id: createdUser.id - }) - } - await new Promise(resolve => setTimeout(resolve, 500)) - } - - console.log() - console.log(`✅ Usuarios de customers creados: ${createdCustomers.length}/4`) - - // 4. Actualizar tabla customers con user_ids - console.log() - console.log('🔄 Actualizando tabla customers con user_ids...') - console.log() - - let updatedCustomersCount = 0 - for (const user of createdCustomers) { - const updated = await updateCustomerUserId(user) - if (updated) { - updatedCustomersCount++ - } - await new Promise(resolve => setTimeout(resolve, 200)) - } - - console.log() - console.log(`✅ Customers actualizados: ${updatedCustomersCount}/4`) - - // 5. Resumen final - console.log() - console.log('==========================================') - console.log('RESUMEN FINAL') - console.log('==========================================') - console.log(`Staff creados: ${createdStaff.length}/8`) - console.log(`Staff actualizados: ${updatedStaffCount}/8`) - console.log(`Customers creados: ${createdCustomers.length}/4`) - console.log(`Customers actualizados: ${updatedCustomersCount}/4`) - console.log('==========================================') - - if (createdStaff.length === 8 && updatedStaffCount === 8 && createdCustomers.length === 4 && updatedCustomersCount === 4) { - console.log() - console.log('🎉 TODOS LOS USUARIOS HAN SIDO CREADOS Y ACTUALIZADOS') - console.log() - console.log('📝 Credenciales de prueba:') - console.log() - console.log('ADMIN:') - console.log(' Email: admin@salonos.com') - console.log(' Password: Admin123!') - console.log() - console.log('CUSTOMER (Gold):') - console.log(' Email: sofia.ramirez@example.com') - console.log(' Password: Customer123!') - console.log() - console.log('Puedes usar estas credenciales para probar el login.') - } else { - console.log() - console.log('⚠️ ALGUNOS USUARIOS NO FUERON CREADOS O ACTUALIZADOS') - console.log('Por favor, verifica los errores arriba.') - } -} - -main() diff --git a/scripts/seed-data.js b/scripts/seed-data.js deleted file mode 100644 index 9035ef3..0000000 --- a/scripts/seed-data.js +++ /dev/null @@ -1,439 +0,0 @@ -#!/usr/bin/env node - -/** - * Script de seed de datos - SalonOS - * Crea datos de prueba para development - */ - -const { createClient } = require('@supabase/supabase-js') - -// Cargar variables de entorno -require('dotenv').config({ path: '.env.local' }) - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL -const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY - -if (!supabaseUrl || !supabaseServiceKey) { - console.error('❌ ERROR: Faltan variables de entorno') - console.error('Asegúrate de tener NEXT_PUBLIC_SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY en .env.local') - process.exit(1) -} - -const supabase = createClient(supabaseUrl, supabaseServiceKey) - -console.log('==========================================') -console.log('SALONOS - SEED DE DATOS') -console.log('==========================================') -console.log() - -async function seedLocations() { - console.log('📍 Creando locations...') - - const { data, error } = await supabase.from('locations').insert([ - { - name: 'Salón Principal - Centro', - timezone: 'America/Mexico_City', - address: 'Av. Reforma 222, Centro Histórico, Ciudad de México', - phone: '+52 55 1234 5678', - is_active: true, - }, - { - name: 'Salón Norte - Polanco', - timezone: 'America/Mexico_City', - address: 'Av. Masaryk 123, Polanco, Ciudad de México', - phone: '+52 55 2345 6789', - is_active: true, - }, - { - name: 'Salón Sur - Coyoacán', - timezone: 'America/Mexico_City', - address: 'Calle Hidalgo 456, Coyoacán, Ciudad de México', - phone: '+52 55 3456 7890', - is_active: true, - }, - ]).select() - - if (error) { - console.error('❌ Error al crear locations:', error) - return [] - } - - console.log(`✅ ${data.length} locations creadas`) - return data -} - -async function seedResources(locations) { - console.log('🪑 Creando resources...') - - const resources = [] - - for (const location of locations) { - const { data, error } = await supabase.from('resources').insert([ - { - location_id: location.id, - name: `Estación ${Math.floor(Math.random() * 100)}`, - type: 'station', - capacity: 1, - is_active: true, - }, - { - location_id: location.id, - name: `Sala VIP ${Math.floor(Math.random() * 100)}`, - type: 'room', - capacity: 2, - is_active: true, - }, - ]).select() - - if (error) { - console.error('❌ Error al crear resources:', error) - continue - } - - resources.push(...data) - } - - console.log(`✅ ${resources.length} resources creadas`) - return resources -} - -async function seedStaff(locations) { - console.log('👥 Creando staff...') - - const { data, error } = await supabase.from('staff').insert([ - { - user_id: '00000000-0000-0000-0000-000000000001', - location_id: locations[0].id, - role: 'admin', - display_name: 'Admin Principal', - phone: '+52 55 1111 2222', - is_active: true, - }, - { - user_id: '00000000-0000-0000-0000-000000000002', - location_id: locations[0].id, - role: 'manager', - display_name: 'Manager Centro', - phone: '+52 55 2222 3333', - is_active: true, - }, - { - user_id: '00000000-0000-0000-0000-000000000003', - location_id: locations[0].id, - role: 'staff', - display_name: 'Staff Coordinadora', - phone: '+52 55 3333 4444', - is_active: true, - }, - { - user_id: '00000000-0000-0000-0000-000000000004', - location_id: locations[0].id, - role: 'artist', - display_name: 'Artist María García', - phone: '+52 55 4444 5555', - is_active: true, - }, - { - user_id: '00000000-0000-0000-0000-000000000005', - location_id: locations[0].id, - role: 'artist', - display_name: 'Artist Ana Rodríguez', - phone: '+52 55 5555 6666', - is_active: true, - }, - { - user_id: '00000000-0000-0000-0000-000000000006', - location_id: locations[1].id, - role: 'manager', - display_name: 'Manager Polanco', - phone: '+52 55 6666 7777', - is_active: true, - }, - { - user_id: '00000000-0000-0000-0000-000000000007', - location_id: locations[1].id, - role: 'artist', - display_name: 'Artist Carla López', - phone: '+52 55 7777 8888', - is_active: true, - }, - { - user_id: '00000000-0000-0000-0000-000000000008', - location_id: locations[2].id, - role: 'artist', - display_name: 'Artist Laura Martínez', - phone: '+52 55 8888 9999', - is_active: true, - }, - ]).select() - - if (error) { - console.error('❌ Error al crear staff:', error) - return [] - } - - console.log(`✅ ${data.length} staff creados`) - return data -} - -async function seedServices() { - console.log('💇 Creando services...') - - const { data, error } = await supabase.from('services').insert([ - { - name: 'Corte y Estilizado', - description: 'Corte de cabello profesional con lavado y estilizado', - duration_minutes: 60, - base_price: 500.00, - requires_dual_artist: false, - premium_fee_enabled: false, - is_active: true, - }, - { - name: 'Color Completo', - description: 'Tinte completo con protección capilar', - duration_minutes: 120, - base_price: 1200.00, - requires_dual_artist: false, - premium_fee_enabled: true, - is_active: true, - }, - { - name: 'Balayage Premium', - description: 'Técnica de balayage con productos premium', - duration_minutes: 180, - base_price: 2000.00, - requires_dual_artist: true, - premium_fee_enabled: true, - is_active: true, - }, - { - name: 'Tratamiento Kératina', - description: 'Tratamiento de kératina para cabello dañado', - duration_minutes: 90, - base_price: 1500.00, - requires_dual_artist: false, - premium_fee_enabled: false, - is_active: true, - }, - { - name: 'Peinado Evento', - description: 'Peinado para eventos especiales', - duration_minutes: 45, - base_price: 800.00, - requires_dual_artist: false, - premium_fee_enabled: true, - is_active: true, - }, - { - name: 'Servicio Express (Dual Artist)', - description: 'Servicio rápido con dos artists simultáneas', - duration_minutes: 30, - base_price: 600.00, - requires_dual_artist: true, - premium_fee_enabled: true, - is_active: true, - }, - ]).select() - - if (error) { - console.error('❌ Error al crear services:', error) - return [] - } - - console.log(`✅ ${data.length} services creados`) - return data -} - -async function seedCustomers() { - console.log('👩 Creando customers...') - - const { data, error } = await supabase.from('customers').insert([ - { - user_id: '10000000-0000-0000-0000-000000000001', - first_name: 'Sofía', - last_name: 'Ramírez', - email: 'sofia.ramirez@example.com', - phone: '+52 55 1111 1111', - tier: 'gold', - notes: 'Cliente VIP. Prefiere Artists María y Ana.', - total_spent: 15000.00, - total_visits: 25, - last_visit_date: '2025-12-20', - is_active: true, - }, - { - user_id: '10000000-0000-0000-0000-000000000002', - first_name: 'Valentina', - last_name: 'Hernández', - email: 'valentina.hernandez@example.com', - phone: '+52 55 2222 2222', - tier: 'gold', - notes: 'Cliente regular. Prefiere horarios de la mañana.', - total_spent: 8500.00, - total_visits: 15, - last_visit_date: '2025-12-15', - is_active: true, - }, - { - user_id: '10000000-0000-0000-0000-000000000003', - first_name: 'Camila', - last_name: 'López', - email: 'camila.lopez@example.com', - phone: '+52 55 3333 3333', - tier: 'free', - notes: 'Nueva cliente. Referida por Valentina.', - total_spent: 500.00, - total_visits: 1, - last_visit_date: '2025-12-10', - is_active: true, - }, - { - user_id: '10000000-0000-0000-0000-000000000004', - first_name: 'Isabella', - last_name: 'García', - email: 'isabella.garcia@example.com', - phone: '+52 55 4444 4444', - tier: 'gold', - notes: 'Cliente VIP. Requiere servicio de Balayage.', - total_spent: 22000.00, - total_visits: 30, - last_visit_date: '2025-12-18', - is_active: true, - }, - ]).select() - - if (error) { - console.error('❌ Error al crear customers:', error) - return [] - } - - console.log(`✅ ${data.length} customers creados`) - return data -} - -async function seedInvitations(customers) { - console.log('💌 Creando invitations...') - - const weekStart = new Date() - weekStart.setDate(weekStart.getDate() - weekStart.getDay() + 1) // Monday - weekStart.setHours(0, 0, 0, 0) - - const invitations = [] - - for (const customer of customers) { - if (customer.tier === 'gold') { - for (let i = 0; i < 5; i++) { - const { data, error } = await supabase.from('invitations').insert({ - inviter_id: customer.id, - code: await generateRandomCode(), - status: 'pending', - week_start_date: weekStart.toISOString().split('T')[0], - expiry_date: new Date(weekStart.getTime() + 6 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], - }).select() - - if (error) { - console.error('❌ Error al crear invitations:', error) - continue - } - - invitations.push(...data) - } - } - } - - console.log(`✅ ${invitations.length} invitations creadas`) - return invitations -} - -async function generateRandomCode() { - const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' - let code = '' - for (let i = 0; i < 10; i++) { - code += chars.charAt(Math.floor(Math.random() * chars.length)) - } - return code -} - -async function seedBookings(customers, staff, resources, services, locations) { - console.log('📅 Creando bookings de prueba...') - - const now = new Date() - const bookings = [] - - for (let i = 0; i < 5; i++) { - const startTime = new Date(now.getTime() + (i + 1) * 24 * 60 * 60 * 1000) - const endTime = new Date(startTime.getTime() + 60 * 60 * 1000) - - const { data, error } = await supabase.from('bookings').insert({ - customer_id: customers[i % customers.length].id, - staff_id: staff.filter(s => s.role === 'artist')[i % staff.filter(s => s.role === 'artist').length].id, - location_id: locations[0].id, - resource_id: resources[0].id, - service_id: services[i % services.length].id, - start_time_utc: startTime.toISOString(), - end_time_utc: endTime.toISOString(), - status: 'confirmed', - deposit_amount: 200.00, - total_amount: services[i % services.length].base_price, - is_paid: true, - payment_reference: `pay_${Math.random().toString(36).substring(7)}`, - }).select() - - if (error) { - console.error('❌ Error al crear bookings:', error) - continue - } - - bookings.push(...data) - } - - console.log(`✅ ${bookings.length} bookings creados`) - return bookings -} - -async function main() { - try { - const locations = await seedLocations() - if (locations.length === 0) throw new Error('No se crearon locations') - - const resources = await seedResources(locations) - const staff = await seedStaff(locations) - if (staff.length === 0) throw new Error('No se creó staff') - - const services = await seedServices() - if (services.length === 0) throw new Error('No se crearon services') - - const customers = await seedCustomers() - if (customers.length === 0) throw new Error('No se crearon customers') - - const invitations = await seedInvitations(customers) - const bookings = await seedBookings(customers, staff, resources, services, locations) - - console.log() - console.log('==========================================') - console.log('✅ SEED DE DATOS COMPLETADO') - console.log('==========================================') - console.log() - console.log('📊 Resumen:') - console.log(` Locations: ${locations.length}`) - console.log(` Resources: ${resources.length}`) - console.log(` Staff: ${staff.length}`) - console.log(` Services: ${services.length}`) - console.log(` Customers: ${customers.length}`) - console.log(` Invitations: ${invitations.length}`) - console.log(` Bookings: ${bookings.length}`) - console.log() - console.log('🎉 La base de datos está lista para desarrollo') - console.log() - console.log('📝 Próximos pasos:') - console.log(' 1. Configurar Auth en Supabase Dashboard') - console.log(' 2. Probar la API de bookings') - console.log(' 3. Implementar endpoints faltantes') - } catch (error) { - console.error('❌ Error inesperado:', error) - process.exit(1) - } -} - -main() diff --git a/scripts/seed-data.sql b/scripts/seed-data.sql deleted file mode 100644 index 5e4e6d5..0000000 --- a/scripts/seed-data.sql +++ /dev/null @@ -1,189 +0,0 @@ --- ============================================ --- SEED DE DATOS - SALONOS --- Ejecutar en Supabase SQL Editor después de las migraciones --- ============================================ - --- 1. Crear Locations -INSERT INTO locations (name, timezone, address, phone, is_active) -VALUES - ('Salón Principal - Centro', 'America/Mexico_City', 'Av. Reforma 222, Centro Histórico, Ciudad de México', '+52 55 1234 5678', true), - ('Salón Norte - Polanco', 'America/Mexico_City', 'Av. Masaryk 123, Polanco, Ciudad de México', '+52 55 2345 6789', true), - ('Salón Sur - Coyoacán', 'America/Mexico_City', 'Calle Hidalgo 456, Coyoacán, Ciudad de México', '+52 55 3456 7890', true); - --- 2. Crear Resources -INSERT INTO resources (location_id, name, type, capacity, is_active) -SELECT - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - 'Estación ' || generate_series(1, 3)::TEXT, - 'station', - 1, - true -UNION ALL -SELECT - (SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), - 'Estación ' || generate_series(1, 2)::TEXT, - 'station', - 1, - true -UNION ALL -SELECT - (SELECT id FROM locations WHERE name = 'Salón Sur - Coyoacán' LIMIT 1), - 'Estación 1', - 'station', - 1, - true; - --- 3. Crear Staff -INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) -VALUES - -- Admin Principal - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'admin', 'Admin Principal', '+52 55 1111 2222', true), - -- Managers - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'manager', 'Manager Centro', '+52 55 2222 3333', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), 'manager', 'Manager Polanco', '+52 55 6666 7777', true), - -- Staff - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'staff', 'Staff Coordinadora', '+52 55 3333 4444', true), - -- Artists - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'artist', 'Artist María García', '+52 55 4444 5555', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'artist', 'Artist Ana Rodríguez', '+52 55 5555 6666', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), 'artist', 'Artist Carla López', '+52 55 7777 8888', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Sur - Coyoacán' LIMIT 1), 'artist', 'Artist Laura Martínez', '+52 55 8888 9999', true); - --- 4. Crear Services -INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) -VALUES - ('Corte y Estilizado', 'Corte de cabello profesional con lavado y estilizado', 60, 500.00, false, false, true), - ('Color Completo', 'Tinte completo con protección capilar', 120, 1200.00, false, true, true), - ('Balayage Premium', 'Técnica de balayage con productos premium', 180, 2000.00, true, true, true), - ('Tratamiento Kératina', 'Tratamiento de kératina para cabello dañado', 90, 1500.00, false, false, true), - ('Peinado Evento', 'Peinado para eventos especiales', 45, 800.00, false, true, true), - ('Servicio Express (Dual Artist)', 'Servicio rápido con dos artists simultáneas', 30, 600.00, true, true, true); - --- 5. Crear Customers -INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, notes, total_spent, total_visits, last_visit_date, is_active) -VALUES - (uuid_generate_v4(), 'Sofía', 'Ramírez', 'sofia.ramirez@example.com', '+52 55 1111 1111', 'gold', 'Cliente VIP. Prefiere Artists María y Ana.', 15000.00, 25, '2025-12-20', true), - (uuid_generate_v4(), 'Valentina', 'Hernández', 'valentina.hernandez@example.com', '+52 55 2222 2222', 'gold', 'Cliente regular. Prefiere horarios de la mañana.', 8500.00, 15, '2025-12-15', true), - (uuid_generate_v4(), 'Camila', 'López', 'camila.lopez@example.com', '+52 55 3333 3333', 'free', 'Nueva cliente. Referida por Valentina.', 500.00, 1, '2025-12-10', true), - (uuid_generate_v4(), 'Isabella', 'García', 'isabella.garcia@example.com', '+52 55 4444 4444', 'gold', 'Cliente VIP. Requiere servicio de Balayage.', 22000.00, 30, '2025-12-18', true); - --- 6. Crear Invitaciones (para clientes Gold) --- Resetear invitaciones para clientes Gold de la semana actual -SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com' LIMIT 1)); -SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'valentina.hernandez@example.com' LIMIT 1)); -SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'isabella.garcia@example.com' LIMIT 1)); - --- 7. Crear Bookings de Prueba -INSERT INTO bookings ( - customer_id, - staff_id, - location_id, - resource_id, - service_id, - start_time_utc, - end_time_utc, - status, - deposit_amount, - total_amount, - is_paid, - payment_reference, - notes -) -SELECT - (SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist María García' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Balayage Premium' LIMIT 1), - NOW() + INTERVAL '1 day', - NOW() + INTERVAL '4 hours', - 'confirmed', - 200.00, - 2000.00, - true, - 'pay_test_001', - 'Balayage Premium para Sofía' -UNION ALL -SELECT - (SELECT id FROM customers WHERE email = 'valentina.hernandez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist Ana Rodríguez' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Color Completo' LIMIT 1), - NOW() + INTERVAL '2 days', - NOW() + INTERVAL '4 hours', - 'confirmed', - 200.00, - 1200.00, - true, - 'pay_test_002', - 'Color Completo para Valentina' -UNION ALL -SELECT - (SELECT id FROM customers WHERE email = 'camila.lopez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist María García' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Corte y Estilizado' LIMIT 1), - NOW() + INTERVAL '3 days', - NOW() + INTERVAL '1 hour', - 'confirmed', - 50.00, - 500.00, - true, - 'pay_test_003', - 'Primer corte para Camila' -UNION ALL -SELECT - (SELECT id FROM customers WHERE email = 'isabella.garcia@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist María García' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Servicio Express (Dual Artist)' LIMIT 1), - NOW() + INTERVAL '4 days', - NOW() + INTERVAL '30 minutes', - 'confirmed', - 200.00, - 600.00, - true, - 'pay_test_004', - 'Servicio Express Dual Artist - Necesita secondary_artist' -UNION ALL -SELECT - (SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist Ana Rodríguez' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) OFFSET 1 LIMIT 1), - (SELECT id FROM services WHERE name = 'Peinado Evento' LIMIT 1), - NOW() + INTERVAL '5 days', - NOW() + INTERVAL '45 minutes', - 'pending', - 200.00, - 800.00, - false, - NULL, - 'Peinado para evento especial'; - --- 8. Actualizar booking con secondary_artist (prueba de validación) -UPDATE bookings -SET secondary_artist_id = (SELECT id FROM staff WHERE display_name = 'Artist Carla López' LIMIT 1) -WHERE payment_reference = 'pay_test_004'; - --- 9. Resumen de datos creados -DO $$ -BEGIN - RAISE NOTICE '=========================================='; - RAISE NOTICE 'SALONOS - SEED DE DATOS COMPLETADO'; - RAISE NOTICE '=========================================='; - RAISE NOTICE 'Locations: %', (SELECT COUNT(*) FROM locations); - RAISE NOTICE 'Resources: %', (SELECT COUNT(*) FROM resources); - RAISE NOTICE 'Staff: %', (SELECT COUNT(*) FROM staff); - RAISE NOTICE 'Services: %', (SELECT COUNT(*) FROM services); - RAISE NOTICE 'Customers: %', (SELECT COUNT(*) FROM customers); - RAISE NOTICE 'Invitations: %', (SELECT COUNT(*) FROM invitations WHERE status = 'pending'); - RAISE NOTICE 'Bookings: %', (SELECT COUNT(*) FROM bookings); - RAISE NOTICE '=========================================='; - RAISE NOTICE '✅ Base de datos lista para desarrollo'; - RAISE NOTICE '=========================================='; -END -$$; diff --git a/scripts/simple-seed.sh b/scripts/simple-seed.sh deleted file mode 100755 index cd65d76..0000000 --- a/scripts/simple-seed.sh +++ /dev/null @@ -1,276 +0,0 @@ -#!/bin/bash - -# Script simple para seed de datos de SalonOS -# Ejecutar con: ./scripts/simple-seed.sh -# Requiere: psql instalado y variables de entorno en .env.local - -# Cargar variables de entorno -set -a -source .env.local -set +a - -if [ -z "$NEXT_PUBLIC_SUPABASE_URL" ] || [ -z "$SUPABASE_SERVICE_ROLE_KEY" ]; then - echo "❌ ERROR: Faltan variables de entorno" - echo "Asegúrate de tener NEXT_PUBLIC_SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY en .env.local" - exit 1 -fi - -# Configurar DATABASE_URL -DB_HOST="${NEXT_PUBLIC_SUPABASE_URL#https://}" -DB_URL="postgresql://postgres:${SUPABASE_SERVICE_ROLE_KEY}@${DB_HOST}:5432/postgres" - -echo "==========================================" -echo "SALONOS - SEED DE DATOS" -echo "==========================================" -echo "" - -# 1. Crear Locations -echo "📍 Creando locations..." -psql "$DB_URL" -c " - INSERT INTO locations (name, timezone, address, phone, is_active) - VALUES - ('Salón Principal - Centro', 'America/Mexico_City', 'Av. Reforma 222, Centro Histórico, Ciudad de México', '+52 55 1234 5678', true), - ('Salón Norte - Polanco', 'America/Mexico_City', 'Av. Masaryk 123, Polanco, Ciudad de México', '+52 55 2345 6789', true), - ('Salón Sur - Coyoacán', 'America/Mexico_City', 'Calle Hidalgo 456, Coyoacán, Ciudad de México', '+52 55 3456 7890', true) - ON CONFLICT DO NOTHING; -" 2>&1 | grep -v "NOTICE" - -LOCATIONS_COUNT=$(psql "$DB_URL" -t -c "SELECT COUNT(*) FROM locations;") -echo "✅ Locations: $LOCATIONS_COUNT/3" - -# 2. Crear Resources -echo "" -echo "🪑 Creando resources..." -psql "$DB_URL" -c " - INSERT INTO resources (location_id, name, type, capacity, is_active) - SELECT - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - 'Estación ' || generate_series(1, 3)::TEXT, - 'station', - 1, - true - UNION ALL - SELECT - (SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), - 'Estación ' || generate_series(1, 2)::TEXT, - 'station', - 1, - true - UNION ALL - SELECT - (SELECT id FROM locations WHERE name = 'Salón Sur - Coyoacán' LIMIT 1), - 'Estación 1', - 'station', - 1, - true - ON CONFLICT DO NOTHING; -" 2>&1 | grep -v "NOTICE" - -RESOURCES_COUNT=$(psql "$DB_URL" -t -c "SELECT COUNT(*) FROM resources;") -echo "✅ Resources: $RESOURCES_COUNT/6" - -# 3. Crear Staff -echo "" -echo "👥 Creando staff..." -psql "$DB_URL" -c " - INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) - VALUES - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'admin', 'Admin Principal', '+52 55 1111 2222', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'manager', 'Manager Centro', '+52 55 2222 3333', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), 'manager', 'Manager Polanco', '+52 55 6666 7777', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'staff', 'Staff Coordinadora', '+52 55 3333 4444', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'artist', 'Artist María García', '+52 55 4444 5555', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'artist', 'Artist Ana Rodríguez', '+52 55 5555 6666', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), 'artist', 'Artist Carla López', '+52 55 7777 8888', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Sur - Coyoacán' LIMIT 1), 'artist', 'Artist Laura Martínez', '+52 55 8888 9999', true) - ON CONFLICT DO NOTHING; -" 2>&1 | grep -v "NOTICE" - -STAFF_COUNT=$(psql "$DB_URL" -t -c "SELECT COUNT(*) FROM staff;") -echo "✅ Staff: $STAFF_COUNT/8" - -# 4. Crear Services -echo "" -echo "💇 Creando services..." -psql "$DB_URL" -c " - INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) - VALUES - ('Corte y Estilizado', 'Corte de cabello profesional con lavado y estilizado', 60, 500.00, false, false, true), - ('Color Completo', 'Tinte completo con protección capilar', 120, 1200.00, false, true, true), - ('Balayage Premium', 'Técnica de balayage con productos premium', 180, 2000.00, true, true, true), - ('Tratamiento Kératina', 'Tratamiento de kératina para cabello dañado', 90, 1500.00, false, false, true), - ('Peinado Evento', 'Peinado para eventos especiales', 45, 800.00, false, true, true), - ('Servicio Express (Dual Artist)', 'Servicio rápido con dos artists simultáneas', 30, 600.00, true, true, true) - ON CONFLICT DO NOTHING; -" 2>&1 | grep -v "NOTICE" - -SERVICES_COUNT=$(psql "$DB_URL" -t -c "SELECT COUNT(*) FROM services;") -echo "✅ Services: $SERVICES_COUNT/6" - -# 5. Crear Customers -echo "" -echo "👩 Creando customers..." -psql "$DB_URL" -c " - INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, notes, total_spent, total_visits, last_visit_date, is_active) - VALUES - (uuid_generate_v4(), 'Sofía', 'Ramírez', 'sofia.ramirez@example.com', '+52 55 1111 1111', 'gold', 'Cliente VIP. Prefiere Artists María y Ana.', 15000.00, 25, '2025-12-20', true), - (uuid_generate_v4(), 'Valentina', 'Hernández', 'valentina.hernandez@example.com', '+52 55 2222 2222', 'gold', 'Cliente regular. Prefiere horarios de la mañana.', 8500.00, 15, '2025-12-15', true), - (uuid_generate_v4(), 'Camila', 'López', 'camila.lopez@example.com', '+52 55 3333 3333', 'free', 'Nueva cliente. Referida por Valentina.', 500.00, 1, '2025-12-10', true), - (uuid_generate_v4(), 'Isabella', 'García', 'isabella.garcia@example.com', '+52 55 4444 4444', 'gold', 'Cliente VIP. Requiere servicio de Balayage.', 22000.00, 30, '2025-12-18', true) - ON CONFLICT (email) DO NOTHING; -" 2>&1 | grep -v "NOTICE" - -CUSTOMERS_COUNT=$(psql "$DB_URL" -t -c "SELECT COUNT(*) FROM customers;") -echo "✅ Customers: $CUSTOMERS_COUNT/4" - -# 6. Crear Invitaciones (para clientes Gold) -echo "" -echo "💌 Creando invitations..." -psql "$DB_URL" -c " - SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com' LIMIT 1)); - SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'valentina.hernandez@example.com' LIMIT 1)); - SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'isabella.garcia@example.com' LIMIT 1)); -" 2>&1 | grep -v "NOTICE" - -INVITATIONS_COUNT=$(psql "$DB_URL" -t -c "SELECT COUNT(*) FROM invitations WHERE status = 'pending';") -echo "✅ Invitaciones: $INVITATIONS_COUNT/15" - -# 7. Crear Bookings de Prueba -echo "" -echo "📅 Creando bookings..." -psql "$DB_URL" -c " - INSERT INTO bookings ( - customer_id, - staff_id, - location_id, - resource_id, - service_id, - start_time_utc, - end_time_utc, - status, - deposit_amount, - total_amount, - is_paid, - payment_reference, - notes - ) - SELECT - (SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist María García' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Balayage Premium' LIMIT 1), - NOW() + INTERVAL '1 day', - NOW() + INTERVAL '4 hours', - 'confirmed', - 200.00, - 2000.00, - true, - 'pay_test_001', - 'Balayage Premium para Sofía' - UNION ALL - SELECT - (SELECT id FROM customers WHERE email = 'valentina.hernandez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist Ana Rodríguez' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Color Completo' LIMIT 1), - NOW() + INTERVAL '2 days', - NOW() + INTERVAL '4 hours', - 'confirmed', - 200.00, - 1200.00, - true, - 'pay_test_002', - 'Color Completo para Valentina' - UNION ALL - SELECT - (SELECT id FROM customers WHERE email = 'camila.lopez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist María García' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Corte y Estilizado' LIMIT 1), - NOW() + INTERVAL '3 days', - NOW() + INTERVAL '1 hour', - 'confirmed', - 50.00, - 500.00, - true, - 'pay_test_003', - 'Primer corte para Camila' - UNION ALL - SELECT - (SELECT id FROM customers WHERE email = 'isabella.garcia@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist María García' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Servicio Express (Dual Artist)' LIMIT 1), - NOW() + INTERVAL '4 days', - NOW() + INTERVAL '30 minutes', - 'confirmed', - 200.00, - 600.00, - true, - 'pay_test_004', - 'Servicio Express Dual Artist - Necesita secondary_artist' - UNION ALL - SELECT - (SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist Ana Rodríguez' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) OFFSET 1 LIMIT 1), - (SELECT id FROM services WHERE name = 'Peinado Evento' LIMIT 1), - NOW() + INTERVAL '5 days', - NOW() + INTERVAL '45 minutes', - 'pending', - 200.00, - 800.00, - false, - NULL, - 'Peinado para evento especial' - ON CONFLICT DO NOTHING; -" 2>&1 | grep -v "NOTICE" - -BOOKINGS_COUNT=$(psql "$DB_URL" -t -c "SELECT COUNT(*) FROM bookings;") -echo "✅ Bookings: $BOOKINGS_COUNT/5" - -# 8. Actualizar booking con secondary_artist -echo "" -echo "🔄 Actualizando booking con secondary_artist..." -psql "$DB_URL" -c " - UPDATE bookings - SET secondary_artist_id = (SELECT id FROM staff WHERE display_name = 'Artist Carla López' LIMIT 1) - WHERE payment_reference = 'pay_test_004'; -" 2>&1 | grep -v "NOTICE" - -SECONDARY_ARTIST_COUNT=$(psql "$DB_URL" -t -c "SELECT COUNT(*) FROM bookings WHERE secondary_artist_id IS NOT NULL;") -echo "✅ Bookings con secondary_artist: $SECONDARY_ARTIST_COUNT/1" - -# Resumen -echo "" -echo "==========================================" -echo "RESUMEN" -echo "==========================================" -echo "Locations: $LOCATIONS_COUNT/3" -echo "Resources: $RESOURCES_COUNT/6" -echo "Staff: $STAFF_COUNT/8" -echo "Services: $SERVICES_COUNT/6" -echo "Customers: $CUSTOMERS_COUNT/4" -echo "Invitations: $INVITATIONS_COUNT/15" -echo "Bookings: $BOOKINGS_COUNT/5" -echo "Sec. Artist: $SECONDARY_ARTIST_COUNT/1" -echo "==========================================" - -if [ "$LOCATIONS_COUNT" -eq 3 ] && [ "$RESOURCES_COUNT" -eq 6 ] && [ "$STAFF_COUNT" -eq 8 ] && [ "$SERVICES_COUNT" -eq 6 ] && [ "$CUSTOMERS_COUNT" -eq 4 ] && [ "$INVITATIONS_COUNT" -eq 15 ] && [ "$BOOKINGS_COUNT" -eq 5 ]; then - echo "" - echo "🎉 SEED DE DATOS COMPLETADO EXITOSAMENTE" - echo "" - echo "Próximos pasos:" - echo "1. Configurar Auth en Supabase Dashboard" - echo "2. Crear usuarios de staff y customers" - echo "3. Actualizar tablas staff y customers con user_ids" -else - echo "" - echo "⚠️ ALGUNOS DATOS NO SE CREARON CORRECTAMENTE" - echo "Por favor, verifica los errores arriba." -fi diff --git a/scripts/simple-verify.sh b/scripts/simple-verify.sh deleted file mode 100755 index 3a5e3d7..0000000 --- a/scripts/simple-verify.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/bin/bash - -# Script simple para verificar migraciones de SalonOS -# Ejecutar con: ./scripts/simple-verify.sh -# Requiere: psql instalado y variables de entorno en .env.local - -# Cargar variables de entorno -set -a -source .env.local -set +a - -if [ -z "$NEXT_PUBLIC_SUPABASE_URL" ] || [ -z "$SUPABASE_SERVICE_ROLE_KEY" ]; then - echo "❌ ERROR: Faltan variables de entorno" - echo "Asegúrate de tener NEXT_PUBLIC_SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY en .env.local" - exit 1 -fi - -# Configurar DATABASE_URL -DB_HOST="${NEXT_PUBLIC_SUPABASE_URL#https://}" -DB_URL="postgresql://postgres:${SUPABASE_SERVICE_ROLE_KEY}@${DB_HOST}:5432/postgres" - -echo "==========================================" -echo "SALONOS - VERIFICACIÓN DE MIGRACIONES" -echo "==========================================" -echo "" - -# 1. Verificar Tablas -echo "📊 Verificando tablas..." -TABLE_COUNT=$(psql "$DB_URL" -t -c " - SELECT COUNT(*) - FROM information_schema.tables - WHERE table_schema = 'public' - AND table_name IN ('locations', 'resources', 'staff', 'services', 'customers', 'invitations', 'bookings', 'audit_logs'); -") - -echo "✅ Tablas: ${TABLE_COUNT}/8" -if [ "$TABLE_COUNT" -lt 8 ]; then - echo "⚠️ Faltan tablas por crear" -fi - -# 2. Verificar Funciones -echo "" -echo "📊 Verificando funciones..." -FUNC_COUNT=$(psql "$DB_URL" -t -c " - SELECT COUNT(*) - FROM information_schema.routines - WHERE routine_schema = 'public'; -") - -echo "✅ Funciones: ${FUNC_COUNT}/14" -if [ "$FUNC_COUNT" -lt 14 ]; then - echo "⚠️ Faltan funciones por crear" -fi - -# 3. Verificar Triggers -echo "" -echo "📊 Verificando triggers..." -TRIGGER_COUNT=$(psql "$DB_URL" -t -c " - SELECT COUNT(*) - FROM information_schema.triggers - WHERE trigger_schema = 'public'; -") - -echo "✅ Triggers: ${TRIGGER_COUNT}/17+" -if [ "$TRIGGER_COUNT" -lt 17 ]; then - echo "⚠️ Faltan triggers por crear" -fi - -# 4. Verificar Políticas RLS -echo "" -echo "📊 Verificando políticas RLS..." -POLICY_COUNT=$(psql "$DB_URL" -t -c " - SELECT COUNT(*) - FROM pg_policies - WHERE schemaname = 'public'; -") - -echo "✅ Políticas RLS: ${POLICY_COUNT}/20+" -if [ "$POLICY_COUNT" -lt 20 ]; then - echo "⚠️ Faltan políticas RLS por crear" -fi - -# 5. Probar Short ID -echo "" -echo "📊 Probando generación de Short ID..." -SHORT_ID=$(psql "$DB_URL" -t -c "SELECT generate_short_id();") - -echo "✅ Short ID: ${SHORT_ID} (${#SHORT_ID} caracteres)" - -# 6. Probar Código de Invitación -echo "" -echo "📊 Probando generación de código de invitación..." -INV_CODE=$(psql "$DB_URL" -t -c "SELECT generate_invitation_code();") - -echo "✅ Código de invitación: ${INV_CODE} (${#INV_CODE} caracteres)" - -# Resumen -echo "" -echo "==========================================" -echo "RESUMEN" -echo "==========================================" -if [ "$TABLE_COUNT" -ge 8 ] && [ "$FUNC_COUNT" -ge 14 ] && [ "$TRIGGER_COUNT" -ge 17 ] && [ "$POLICY_COUNT" -ge 20 ]; then - echo "Tablas: ✅ ${TABLE_COUNT}/8" - echo "Funciones: ✅ ${FUNC_COUNT}/14" - echo "Triggers: ✅ ${TRIGGER_COUNT}/17+" - echo "Políticas RLS: ✅ ${POLICY_COUNT}/20+" - echo "Short ID: ✅ Generable" - echo "Cód. Invit.: ✅ Generable" - echo "" - echo "==========================================" - echo "🎉 TODAS LAS MIGRACIONES ESTÁN CORRECTAS" - echo "Puedes continuar con el seed de datos." - echo "==========================================" -else - echo "Tablas: ❌ ${TABLE_COUNT}/8" - echo "Funciones: ❌ ${FUNC_COUNT}/14" - echo "Triggers: ❌ ${TRIGGER_COUNT}/17+" - echo "Políticas RLS: ❌ ${POLICY_COUNT}/20+" - echo "" - echo "==========================================" - echo "⚠️ ALGUNAS MIGRACIONES FALTAN" - echo "Por favor, verifica los errores arriba." - echo "==========================================" -fi diff --git a/scripts/verify-migration.js b/scripts/verify-migration.js deleted file mode 100644 index b51f7fc..0000000 --- a/scripts/verify-migration.js +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/env node - -/** - * Script de verificación de migraciones - SalonOS - * Verifica que todas las tablas, funciones, triggers y políticas RLS estén creados - */ - -const { createClient } = require('@supabase/supabase-js') - -// Cargar variables de entorno -require('dotenv').config({ path: '.env.local' }) - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL -const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY - -if (!supabaseUrl || !supabaseServiceKey) { - console.error('❌ ERROR: Faltan variables de entorno') - console.error('Asegúrate de tener NEXT_PUBLIC_SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY en .env.local') - process.exit(1) -} - -const supabase = createClient(supabaseUrl, supabaseServiceKey) - -console.log('==========================================') -console('SALONOS - VERIFICACIÓN DE MIGRACIONES') -console.log('==========================================') -console.log() - -const expectedTables = [ - 'locations', - 'resources', - 'staff', - 'services', - 'customers', - 'invitations', - 'bookings', - 'audit_logs', -] - -const expectedFunctions = [ - 'generate_short_id', - 'generate_invitation_code', - 'reset_weekly_invitations_for_customer', - 'reset_all_weekly_invitations', - 'log_audit', - 'get_current_user_role', - 'is_staff_or_higher', - 'is_artist', - 'is_customer', - 'is_admin', - 'update_updated_at', - 'generate_booking_short_id', - 'get_week_start', -] - -const expectedEnums = [ - 'user_role', - 'customer_tier', - 'booking_status', - 'invitation_status', - 'resource_type', - 'audit_action', -] - -async function verifyTables() { - console.log('📊 Verificando tablas...') - - const { data: tables, error } = await supabase.rpc('verify_tables_exist', { - table_names: expectedTables, - }) - - if (error) { - console.error('❌ Error al verificar tablas:', error) - return false - } - - console.log(`✅ Tablas creadas: ${tables.length}/${expectedTables.length}`) - - if (tables.length !== expectedTables.length) { - console.log('⚠️ Tablas faltantes:') - expectedTables.forEach(table => { - if (!tables.includes(table)) { - console.log(` - ${table}`) - } - }) - return false - } - - return true -} - -async function verifyFunctions() { - console.log('📊 Verificando funciones...') - - const { data: functions, error } = await supabase.rpc('verify_functions_exist', { - function_names: expectedFunctions, - }) - - if (error) { - console.error('❌ Error al verificar funciones:', error) - return false - } - - console.log(`✅ Funciones creadas: ${functions.length}/${expectedFunctions.length}`) - - if (functions.length !== expectedFunctions.length) { - console.log('⚠️ Funciones faltantes:') - expectedFunctions.forEach(func => { - if (!functions.includes(func)) { - console.log(` - ${func}`) - } - }) - return false - } - - return true -} - -async function verifyEnums() { - console.log('📊 Verificando tipos ENUM...') - - const { data: enums, error } = await supabase.rpc('verify_enums_exist', { - enum_names: expectedEnums, - }) - - if (error) { - console.error('❌ Error al verificar tipos ENUM:', error) - return false - } - - console.log(`✅ Tipos ENUM creados: ${enums.length}/${expectedEnums.length}`) - - if (enums.length !== expectedEnums.length) { - console.log('⚠️ Tipos ENUM faltantes:') - expectedEnums.forEach(enumName => { - if (!enums.includes(enumName)) { - console.log(` - ${enumName}`) - } - }) - return false - } - - return true -} - -async function testShortID() { - console.log('🧪 Probando generación de Short ID...') - - const { data, error } = await supabase.rpc('generate_short_id') - - if (error) { - console.error('❌ Error al generar Short ID:', error) - return false - } - - console.log(`✅ Short ID generado: ${data}`) - console.log(` Longitud: ${data.length} caracteres`) - - if (data.length !== 6) { - console.error('❌ ERROR: El Short ID debe tener 6 caracteres') - return false - } - - return true -} - -async function testInvitationCode() { - console.log('🧪 Probando generación de código de invitación...') - - const { data, error } = await supabase.rpc('generate_invitation_code') - - if (error) { - console.error('❌ Error al generar código de invitación:', error) - return false - } - - console.log(`✅ Código de invitación generado: ${data}`) - console.log(` Longitud: ${data.length} caracteres`) - - if (data.length !== 10) { - console.error('❌ ERROR: El código de invitación debe tener 10 caracteres') - return false - } - - return true -} - -async function main() { - try { - const tablesOk = await verifyTables() - const functionsOk = await verifyFunctions() - const enumsOk = await verifyEnums() - const shortIdOk = await testShortID() - const invitationCodeOk = await testInvitationCode() - - console.log() - console.log('==========================================') - - if (tablesOk && functionsOk && enumsOk && shortIdOk && invitationCodeOk) { - console.log('✅ TODAS LAS VERIFICACIONES PASARON') - console.log('==========================================') - console.log() - console.log('🎉 La base de datos está lista para usar') - console.log() - console.log('📝 Próximos pasos:') - console.log(' 1. Configurar Auth en Supabase Dashboard') - console.log(' 2. Crear usuarios de prueba con roles específicos') - console.log(' 3. Ejecutar seeds de datos de prueba') - console.log(' 4. Probar la API de bookings') - process.exit(0) - } else { - console.log('❌ ALGUNAS VERIFICACIONES FALLARON') - console.log('==========================================') - console.log() - console.log('Por favor, revisa los errores arriba y ejecuta nuevamente:') - console.log(' npm run db:migrate') - process.exit(1) - } - } catch (error) { - console.error('❌ Error inesperado:', error) - process.exit(1) - } -} - -main() diff --git a/scripts/verify-migration.sql b/scripts/verify-migration.sql deleted file mode 100644 index 8067e7d..0000000 --- a/scripts/verify-migration.sql +++ /dev/null @@ -1,89 +0,0 @@ --- ============================================ --- VERIFICACIÓN POST-MIGRACIÓN - SALONOS --- Ejecutar en Supabase SQL Editor después de las migraciones --- ============================================ - --- 1. Verificar Tablas Creadas -SELECT 'TABLAS' as verification_type, table_name as item -FROM information_schema.tables -WHERE table_schema = 'public' -AND table_name IN ('locations', 'resources', 'staff', 'services', 'customers', 'invitations', 'bookings', 'audit_logs') -ORDER BY table_name; - --- 2. Verificar Funciones Creadas -SELECT 'FUNCIONES' as verification_type, routine_name as item -FROM information_schema.routines -WHERE routine_schema = 'public' -ORDER BY routine_name; - --- 3. Verificar Triggers Activos -SELECT 'TRIGGERS' as verification_type, trigger_name as item -FROM information_schema.triggers -WHERE trigger_schema = 'public' -ORDER BY event_object_table, trigger_name; - --- 4. Verificar Políticas RLS -SELECT 'POLÍTICAS RLS' as verification_type, policyname as item -FROM pg_policies -WHERE schemaname = 'public' -ORDER BY tablename, policyname; - --- 5. Verificar Tipos ENUM -SELECT 'ENUM TYPES' as verification_type, typname as item -FROM pg_type -WHERE typtype = 'e' -AND typname IN ('user_role', 'customer_tier', 'booking_status', 'invitation_status', 'resource_type', 'audit_action') -ORDER BY typname; - --- 6. Probar Short ID Generation -SELECT 'SHORT ID TEST' as verification_type, generate_short_id() as item; - --- 7. Probar Invitation Code Generation -SELECT 'INVITATION CODE TEST' as verification_type, generate_invitation_code() as item; - --- 8. Verificar Trigger de Validación de Secondary Artist -SELECT 'SECONDARY ARTIST TRIGGER' as verification_type, trigger_name as item -FROM information_schema.triggers -WHERE trigger_name = 'validate_booking_secondary_artist'; - --- 9. Verificar Función de Reset de Invitaciones -SELECT 'RESET INVITATIONS FUNCTION' as verification_type, routine_name as item -FROM information_schema.routines -WHERE routine_name = 'reset_all_weekly_invitations'; - --- 10. Verificar Función de Validación de Secondary Artist -SELECT 'VALIDATE SECONDARY ARTIST' as verification_type, routine_name as item -FROM information_schema.routines -WHERE routine_name = 'validate_secondary_artist_role'; - --- 11. Verificar Week Start Function -SELECT 'WEEK START FUNCTION' as verification_type, get_week_start(CURRENT_DATE) as item; - --- 12. Contar elementos por tipo -SELECT - 'RESUMEN' as verification_type, - 'Tablas: ' || (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name IN ('locations', 'resources', 'staff', 'services', 'customers', 'invitations', 'bookings', 'audit_logs')) as item - -UNION ALL - -SELECT - 'RESUMEN' as verification_type, - 'Funciones: ' || (SELECT COUNT(*) FROM information_schema.routines WHERE routine_schema = 'public') as item - -UNION ALL - -SELECT - 'RESUMEN' as verification_type, - 'Triggers: ' || (SELECT COUNT(*) FROM information_schema.triggers WHERE trigger_schema = 'public') as item - -UNION ALL - -SELECT - 'RESUMEN' as verification_type, - 'Políticas RLS: ' || (SELECT COUNT(*) FROM pg_policies WHERE schemaname = 'public') as item - -UNION ALL - -SELECT - 'RESUMEN' as verification_type, - 'Tipos ENUM: ' || (SELECT COUNT(*) FROM pg_type WHERE typtype = 'e' AND typname IN ('user_role', 'customer_tier', 'booking_status', 'invitation_status', 'resource_type', 'audit_action')) as item; diff --git a/supabase/.gitignore b/supabase/.gitignore new file mode 100644 index 0000000..ad9264f --- /dev/null +++ b/supabase/.gitignore @@ -0,0 +1,8 @@ +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 0000000..a593eef --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1,384 @@ +# For detailed configuration reference documentation, visit: +# https://supabase.com/docs/guides/local-development/cli/config +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "salonOS" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` and `graphql_public` schemas are included by default. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[api.tls] +# Enable HTTPS endpoints locally using a self-signed certificate. +enabled = false +# Paths to self-signed certificate pair. +# cert_path = "../certs/my-cert.pem" +# key_path = "../certs/my-key.pem" + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# Maximum amount of time to wait for health check when starting the local database. +health_timeout = "2m" +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 17 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +# [db.vault] +# secret_key = "env(SECRET_VALUE)" + +[db.migrations] +# If disabled, migrations will be skipped during a db push or reset. +enabled = true +# Specifies an ordered list of schema files that describe your database. +# Supports glob patterns relative to supabase directory: "./schemas/*.sql" +schema_paths = [] + +[db.seed] +# If enabled, seeds the database after migrations during a db reset. +enabled = true +# Specifies an ordered list of seed files to load during db reset. +# Supports glob patterns relative to supabase directory: "./seeds/*.sql" +sql_paths = ["./seed.sql"] + +[db.network_restrictions] +# Enable management of network restrictions. +enabled = false +# List of IPv4 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv4 connections. Set empty array to block all IPs. +allowed_cidrs = ["0.0.0.0/0"] +# List of IPv6 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv6 connections. Set empty array to block all IPs. +allowed_cidrs_v6 = ["::/0"] + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 +# admin_email = "admin@email.com" +# sender_name = "Admin" + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +# Uncomment to configure local storage buckets +# [storage.buckets.images] +# public = false +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# objects_path = "./images" + +# Allow connections via S3 compatible clients +[storage.s3_protocol] +enabled = true + +# Image transformation API is available to Supabase Pro plan. +# [storage.image_transformation] +# enabled = true + +# Store analytical data in S3 for running ETL jobs over Iceberg Catalog +# This feature is only available on the hosted platform. +[storage.analytics] +enabled = false +max_namespaces = 5 +max_tables = 10 +max_catalogs = 2 + +# Analytics Buckets is available to Supabase Pro plan. +# [storage.analytics.buckets.my-warehouse] + +# Store vector embeddings in S3 for large and durable datasets +# This feature is only available on the hosted platform. +[storage.vector] +enabled = false +max_buckets = 10 +max_indexes = 5 + +# Vector Buckets is available to Supabase Pro plan. +# [storage.vector.buckets.documents-openai] + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:/auth/v1). +# jwt_issuer = "" +# Path to JWT signing key. DO NOT commit your signing keys file to git. +# signing_keys_path = "./signing_keys.json" +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false +# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. +minimum_password_length = 6 +# Passwords that do not meet the following requirements will be rejected as weak. Supported values +# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +password_requirements = "" + +[auth.rate_limit] +# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. +email_sent = 2 +# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. +sms_sent = 30 +# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. +anonymous_users = 30 +# Number of sessions that can be refreshed in a 5 minute interval per IP address. +token_refresh = 150 +# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +sign_in_sign_ups = 30 +# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +token_verifications = 30 +# Number of Web3 logins that can be made in a 5 minute interval per IP address. +web3 = 30 + +# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. +# [auth.captcha] +# enabled = true +# provider = "hcaptcha" +# secret = "" + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# If enabled, users will need to reauthenticate or have logged in recently to change their password. +secure_password_change = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" +# Number of characters used in the email OTP. +otp_length = 6 +# Number of seconds before the email OTP expires (defaults to 1 hour). +otp_expiry = 3600 + +# Use a production-ready SMTP server +# [auth.email.smtp] +# enabled = true +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +# Uncomment to customize notification email template +# [auth.email.notification.password_changed] +# enabled = true +# subject = "Your password has been changed" +# content_path = "./templates/password_changed_notification.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = false +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }}" +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Multi-factor-authentication is available to Supabase Pro plan. +[auth.mfa] +# Control how many MFA factors can be enrolled at once per user. +max_enrolled_factors = 10 + +# Control MFA via App Authenticator (TOTP) +[auth.mfa.totp] +enroll_enabled = false +verify_enabled = false + +# Configure MFA via Phone Messaging +[auth.mfa.phone] +enroll_enabled = false +verify_enabled = false +otp_length = 6 +template = "Your code is {{ .Code }}" +max_frequency = "5s" + +# Configure MFA via WebAuthn +# [auth.mfa.web_authn] +# enroll_enabled = true +# verify_enabled = true + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `x`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth redirectUrl. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false +# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. +email_optional = false + +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. +[auth.web3.solana] +enabled = false + +# Use Firebase Auth as a third-party provider alongside Supabase Auth. +[auth.third_party.firebase] +enabled = false +# project_id = "my-firebase-project" + +# Use Auth0 as a third-party provider alongside Supabase Auth. +[auth.third_party.auth0] +enabled = false +# tenant = "my-auth0-tenant" +# tenant_region = "us" + +# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. +[auth.third_party.aws_cognito] +enabled = false +# user_pool_id = "my-user-pool-id" +# user_pool_region = "us-east-1" + +# Use Clerk as a third-party provider alongside Supabase Auth. +[auth.third_party.clerk] +enabled = false +# Obtain from https://clerk.com/setup/supabase +# domain = "example.clerk.accounts.dev" + +# OAuth server configuration +[auth.oauth_server] +# Enable OAuth server functionality +enabled = false +# Path for OAuth consent flow UI +authorization_url_path = "/oauth/consent" +# Allow dynamic client registration +allow_dynamic_registration = false + +[edge_runtime] +enabled = true +# Supported request policies: `oneshot`, `per_worker`. +# `per_worker` (default) — enables hot reload during local development. +# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). +policy = "per_worker" +# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 +# The Deno major version to use. +deno_version = 2 + +# [edge_runtime.secrets] +# secret_key = "env(SECRET_VALUE)" + +[analytics] +enabled = true +port = 54327 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/db/migrations/00_FULL_MIGRATION_CORRECTED.sql b/supabase/migrations/20260115235737_initial_schema.sql similarity index 77% rename from db/migrations/00_FULL_MIGRATION_CORRECTED.sql rename to supabase/migrations/20260115235737_initial_schema.sql index eab1d5f..9b8bd25 100644 --- a/db/migrations/00_FULL_MIGRATION_CORRECTED.sql +++ b/supabase/migrations/20260115235737_initial_schema.sql @@ -12,15 +12,30 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- ENUMS -CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer'); -CREATE TYPE customer_tier AS ENUM ('free', 'gold'); -CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'cancelled', 'completed', 'no_show'); -CREATE TYPE invitation_status AS ENUM ('pending', 'used', 'expired'); -CREATE TYPE resource_type AS ENUM ('station', 'room', 'equipment'); -CREATE TYPE audit_action AS ENUM ('create', 'update', 'delete', 'reset_invitations', 'payment', 'status_change'); +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN + CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'customer_tier') THEN + CREATE TYPE customer_tier AS ENUM ('free', 'gold'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'booking_status') THEN + CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'cancelled', 'completed', 'no_show'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'invitation_status') THEN + CREATE TYPE invitation_status AS ENUM ('pending', 'used', 'expired'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'resource_type') THEN + CREATE TYPE resource_type AS ENUM ('station', 'room', 'equipment'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'audit_action') THEN + CREATE TYPE audit_action AS ENUM ('create', 'update', 'delete', 'reset_invitations', 'payment', 'status_change'); + END IF; +END $$; -- LOCATIONS -CREATE TABLE locations ( +CREATE TABLE IF NOT EXISTS locations ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), name VARCHAR(100) NOT NULL, timezone VARCHAR(50) NOT NULL DEFAULT 'UTC', @@ -32,7 +47,7 @@ CREATE TABLE locations ( ); -- RESOURCES -CREATE TABLE resources ( +CREATE TABLE IF NOT EXISTS resources ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, @@ -44,7 +59,7 @@ CREATE TABLE resources ( ); -- STAFF -CREATE TABLE staff ( +CREATE TABLE IF NOT EXISTS staff ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL, location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE, @@ -58,7 +73,7 @@ CREATE TABLE staff ( ); -- SERVICES -CREATE TABLE services ( +CREATE TABLE IF NOT EXISTS services ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), name VARCHAR(100) NOT NULL, description TEXT, @@ -72,7 +87,7 @@ CREATE TABLE services ( ); -- CUSTOMERS -CREATE TABLE customers ( +CREATE TABLE IF NOT EXISTS customers ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID UNIQUE, first_name VARCHAR(100) NOT NULL, @@ -90,7 +105,7 @@ CREATE TABLE customers ( ); -- INVITATIONS -CREATE TABLE invitations ( +CREATE TABLE IF NOT EXISTS invitations ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), inviter_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, code VARCHAR(10) UNIQUE NOT NULL, @@ -104,7 +119,7 @@ CREATE TABLE invitations ( ); -- BOOKINGS -CREATE TABLE bookings ( +CREATE TABLE IF NOT EXISTS bookings ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), short_id VARCHAR(6) UNIQUE NOT NULL, customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, @@ -126,7 +141,7 @@ CREATE TABLE bookings ( ); -- AUDIT LOGS -CREATE TABLE audit_logs ( +CREATE TABLE IF NOT EXISTS audit_logs ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), entity_type VARCHAR(50) NOT NULL, entity_id UUID NOT NULL, @@ -142,30 +157,30 @@ CREATE TABLE audit_logs ( ); -- INDEXES -CREATE INDEX idx_locations_active ON locations(is_active); -CREATE INDEX idx_resources_location ON resources(location_id); -CREATE INDEX idx_resources_active ON resources(location_id, is_active); -CREATE INDEX idx_staff_user ON staff(user_id); -CREATE INDEX idx_staff_location ON staff(location_id); -CREATE INDEX idx_staff_role ON staff(location_id, role, is_active); -CREATE INDEX idx_services_active ON services(is_active); -CREATE INDEX idx_customers_tier ON customers(tier); -CREATE INDEX idx_customers_email ON customers(email); -CREATE INDEX idx_customers_active ON customers(is_active); -CREATE INDEX idx_invitations_inviter ON invitations(inviter_id); -CREATE INDEX idx_invitations_code ON invitations(code); -CREATE INDEX idx_invitations_week ON invitations(week_start_date, status); -CREATE INDEX idx_bookings_customer ON bookings(customer_id); -CREATE INDEX idx_bookings_staff ON bookings(staff_id); -CREATE INDEX idx_bookings_secondary_artist ON bookings(secondary_artist_id); -CREATE INDEX idx_bookings_location ON bookings(location_id); -CREATE INDEX idx_bookings_resource ON bookings(resource_id); -CREATE INDEX idx_bookings_time ON bookings(start_time_utc, end_time_utc); -CREATE INDEX idx_bookings_status ON bookings(status); -CREATE INDEX idx_bookings_short_id ON bookings(short_id); -CREATE INDEX idx_audit_entity ON audit_logs(entity_type, entity_id); -CREATE INDEX idx_audit_action ON audit_logs(action, created_at); -CREATE INDEX idx_audit_performed ON audit_logs(performed_by); +CREATE INDEX IF NOT EXISTS idx_locations_active ON locations(is_active); +CREATE INDEX IF NOT EXISTS idx_resources_location ON resources(location_id); +CREATE INDEX IF NOT EXISTS idx_resources_active ON resources(location_id, is_active); +CREATE INDEX IF NOT EXISTS idx_staff_user ON staff(user_id); +CREATE INDEX IF NOT EXISTS idx_staff_location ON staff(location_id); +CREATE INDEX IF NOT EXISTS idx_staff_role ON staff(location_id, role, is_active); +CREATE INDEX IF NOT EXISTS idx_services_active ON services(is_active); +CREATE INDEX IF NOT EXISTS idx_customers_tier ON customers(tier); +CREATE INDEX IF NOT EXISTS idx_customers_email ON customers(email); +CREATE INDEX IF NOT EXISTS idx_customers_active ON customers(is_active); +CREATE INDEX IF NOT EXISTS idx_invitations_inviter ON invitations(inviter_id); +CREATE INDEX IF NOT EXISTS idx_invitations_code ON invitations(code); +CREATE INDEX IF NOT EXISTS idx_invitations_week ON invitations(week_start_date, status); +CREATE INDEX IF NOT EXISTS idx_bookings_customer ON bookings(customer_id); +CREATE INDEX IF NOT EXISTS idx_bookings_staff ON bookings(staff_id); +CREATE INDEX IF NOT EXISTS idx_bookings_secondary_artist ON bookings(secondary_artist_id); +CREATE INDEX IF NOT EXISTS idx_bookings_location ON bookings(location_id); +CREATE INDEX IF NOT EXISTS idx_bookings_resource ON bookings(resource_id); +CREATE INDEX IF NOT EXISTS idx_bookings_time ON bookings(start_time_utc, end_time_utc); +CREATE INDEX IF NOT EXISTS idx_bookings_status ON bookings(status); +CREATE INDEX IF NOT EXISTS idx_bookings_short_id ON bookings(short_id); +CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_logs(entity_type, entity_id); +CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action, created_at); +CREATE INDEX IF NOT EXISTS idx_audit_performed ON audit_logs(performed_by); -- UPDATED_AT TRIGGER FUNCTION CREATE OR REPLACE FUNCTION update_updated_at() @@ -177,32 +192,39 @@ END; $$ LANGUAGE plpgsql; -- UPDATED_AT TRIGGERS +DROP TRIGGER IF EXISTS locations_updated_at ON locations; CREATE TRIGGER locations_updated_at BEFORE UPDATE ON locations FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +DROP TRIGGER IF EXISTS resources_updated_at ON resources; CREATE TRIGGER resources_updated_at BEFORE UPDATE ON resources FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +DROP TRIGGER IF EXISTS staff_updated_at ON staff; CREATE TRIGGER staff_updated_at BEFORE UPDATE ON staff FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +DROP TRIGGER IF EXISTS services_updated_at ON services; CREATE TRIGGER services_updated_at BEFORE UPDATE ON services FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +DROP TRIGGER IF EXISTS customers_updated_at ON customers; CREATE TRIGGER customers_updated_at BEFORE UPDATE ON customers FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +DROP TRIGGER IF EXISTS invitations_updated_at ON invitations; CREATE TRIGGER invitations_updated_at BEFORE UPDATE ON invitations FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +DROP TRIGGER IF EXISTS bookings_updated_at ON bookings; CREATE TRIGGER bookings_updated_at BEFORE UPDATE ON bookings FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -- CONSTRAINTS (Simple ones only - no subqueries) -ALTER TABLE bookings ADD CONSTRAINT check_booking_time +ALTER TABLE bookings ADD CONSTRAINT IF NOT EXISTS check_booking_time CHECK (end_time_utc > start_time_utc); -ALTER TABLE invitations ADD CONSTRAINT check_week_start_is_monday +ALTER TABLE invitations ADD CONSTRAINT IF NOT EXISTS check_week_start_is_monday CHECK (EXTRACT(ISODOW FROM week_start_date) = 1); -- Trigger for secondary_artist validation (instead of CHECK constraint with subquery) @@ -221,6 +243,7 @@ BEGIN END; $$ LANGUAGE plpgsql; +DROP TRIGGER IF EXISTS validate_booking_secondary_artist ON bookings; CREATE TRIGGER validate_booking_secondary_artist BEFORE INSERT OR UPDATE ON bookings FOR EACH ROW EXECUTE FUNCTION validate_secondary_artist_role(); @@ -299,32 +322,39 @@ ALTER TABLE bookings ENABLE ROW LEVEL SECURITY; ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY; -- LOCATIONS POLICIES +DROP POLICY IF EXISTS "locations_select_staff_higher" ON locations; CREATE POLICY "locations_select_staff_higher" ON locations FOR SELECT USING (is_staff_or_higher() OR is_admin()); +DROP POLICY IF EXISTS "locations_modify_admin_manager" ON locations; CREATE POLICY "locations_modify_admin_manager" ON locations FOR ALL USING (get_current_user_role() IN ('admin', 'manager')); -- RESOURCES POLICIES +DROP POLICY IF EXISTS "resources_select_staff_higher" ON resources; CREATE POLICY "resources_select_staff_higher" ON resources FOR SELECT USING (is_staff_or_higher() OR is_admin()); +DROP POLICY IF EXISTS "resources_select_artist" ON resources; CREATE POLICY "resources_select_artist" ON resources FOR SELECT USING (is_artist()); +DROP POLICY IF EXISTS "resources_modify_admin_manager" ON resources; CREATE POLICY "resources_modify_admin_manager" ON resources FOR ALL USING (get_current_user_role() IN ('admin', 'manager')); -- STAFF POLICIES +DROP POLICY IF EXISTS "staff_select_admin_manager" ON staff; CREATE POLICY "staff_select_admin_manager" ON staff FOR SELECT USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "staff_select_same_location" ON staff; CREATE POLICY "staff_select_same_location" ON staff FOR SELECT USING ( @@ -334,6 +364,7 @@ CREATE POLICY "staff_select_same_location" ON staff ) ); +DROP POLICY IF EXISTS "staff_select_artist_view_artists" ON staff; CREATE POLICY "staff_select_artist_view_artists" ON staff FOR SELECT USING ( @@ -344,74 +375,91 @@ CREATE POLICY "staff_select_artist_view_artists" ON staff staff.role = 'artist' ); +DROP POLICY IF EXISTS "staff_modify_admin_manager" ON staff; CREATE POLICY "staff_modify_admin_manager" ON staff FOR ALL USING (get_current_user_role() IN ('admin', 'manager')); -- SERVICES POLICIES +DROP POLICY IF EXISTS "services_select_all" ON services; CREATE POLICY "services_select_all" ON services FOR SELECT USING (is_active = true); +DROP POLICY IF EXISTS "services_all_admin_manager" ON services; CREATE POLICY "services_all_admin_manager" ON services FOR ALL USING (get_current_user_role() IN ('admin', 'manager')); -- CUSTOMERS POLICIES (RESTRICTED FOR ARTISTS) +DROP POLICY IF EXISTS "customers_select_admin_manager" ON customers; CREATE POLICY "customers_select_admin_manager" ON customers FOR SELECT USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "customers_select_staff" ON customers; CREATE POLICY "customers_select_staff" ON customers FOR SELECT USING (is_staff_or_higher()); +DROP POLICY IF EXISTS "customers_select_artist_restricted" ON customers; CREATE POLICY "customers_select_artist_restricted" ON customers FOR SELECT USING (is_artist()); +DROP POLICY IF EXISTS "customers_select_own" ON customers; CREATE POLICY "customers_select_own" ON customers FOR SELECT USING (is_customer() AND user_id = auth.uid()); +DROP POLICY IF EXISTS "customers_modify_admin_manager" ON customers; CREATE POLICY "customers_modify_admin_manager" ON customers FOR ALL USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "customers_modify_staff" ON customers; CREATE POLICY "customers_modify_staff" ON customers FOR ALL USING (is_staff_or_higher()); +DROP POLICY IF EXISTS "customers_update_own" ON customers; CREATE POLICY "customers_update_own" ON customers FOR UPDATE USING (is_customer() AND user_id = auth.uid()); -- INVITATIONS POLICIES +DROP POLICY IF EXISTS "invitations_select_admin_manager" ON invitations; CREATE POLICY "invitations_select_admin_manager" ON invitations FOR SELECT USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "invitations_select_staff" ON invitations; CREATE POLICY "invitations_select_staff" ON invitations FOR SELECT USING (is_staff_or_higher()); +DROP POLICY IF EXISTS "invitations_select_own" ON invitations; CREATE POLICY "invitations_select_own" ON invitations FOR SELECT USING (is_customer() AND inviter_id = (SELECT id FROM customers WHERE user_id = auth.uid())); +DROP POLICY IF EXISTS "invitations_modify_admin_manager" ON invitations; CREATE POLICY "invitations_modify_admin_manager" ON invitations FOR ALL USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "invitations_modify_staff" ON invitations; CREATE POLICY "invitations_modify_staff" ON invitations FOR ALL USING (is_staff_or_higher()); -- BOOKINGS POLICIES +DROP POLICY IF EXISTS "bookings_select_admin_manager" ON bookings; CREATE POLICY "bookings_select_admin_manager" ON bookings FOR SELECT USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "bookings_select_staff_location" ON bookings; CREATE POLICY "bookings_select_staff_location" ON bookings FOR SELECT USING ( @@ -421,6 +469,7 @@ CREATE POLICY "bookings_select_staff_location" ON bookings ) ); +DROP POLICY IF EXISTS "bookings_select_artist_own" ON bookings; CREATE POLICY "bookings_select_artist_own" ON bookings FOR SELECT USING ( @@ -429,14 +478,17 @@ CREATE POLICY "bookings_select_artist_own" ON bookings secondary_artist_id = (SELECT id FROM staff WHERE user_id = auth.uid())) ); +DROP POLICY IF EXISTS "bookings_select_own" ON bookings; CREATE POLICY "bookings_select_own" ON bookings FOR SELECT USING (is_customer() AND customer_id = (SELECT id FROM customers WHERE user_id = auth.uid())); +DROP POLICY IF EXISTS "bookings_modify_admin_manager" ON bookings; CREATE POLICY "bookings_modify_admin_manager" ON bookings FOR ALL USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "bookings_modify_staff_location" ON bookings; CREATE POLICY "bookings_modify_staff_location" ON bookings FOR ALL USING ( @@ -446,10 +498,12 @@ CREATE POLICY "bookings_modify_staff_location" ON bookings ) ); +DROP POLICY IF EXISTS "bookings_no_modify_artist" ON bookings; CREATE POLICY "bookings_no_modify_artist" ON bookings FOR ALL USING (NOT is_artist()); +DROP POLICY IF EXISTS "bookings_create_own" ON bookings; CREATE POLICY "bookings_create_own" ON bookings FOR INSERT WITH CHECK ( @@ -457,6 +511,7 @@ CREATE POLICY "bookings_create_own" ON bookings customer_id = (SELECT id FROM customers WHERE user_id = auth.uid()) ); +DROP POLICY IF EXISTS "bookings_update_own" ON bookings; CREATE POLICY "bookings_update_own" ON bookings FOR UPDATE USING ( @@ -465,10 +520,12 @@ CREATE POLICY "bookings_update_own" ON bookings ); -- AUDIT LOGS POLICIES +DROP POLICY IF EXISTS "audit_logs_select_admin_manager" ON audit_logs; CREATE POLICY "audit_logs_select_admin_manager" ON audit_logs FOR SELECT USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "audit_logs_select_staff_location" ON audit_logs; CREATE POLICY "audit_logs_select_staff_location" ON audit_logs FOR SELECT USING ( @@ -481,6 +538,7 @@ CREATE POLICY "audit_logs_select_staff_location" ON audit_logs ) ); +DROP POLICY IF EXISTS "audit_logs_no_insert" ON audit_logs; CREATE POLICY "audit_logs_no_insert" ON audit_logs FOR INSERT WITH CHECK (false); @@ -736,18 +794,23 @@ END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- APPLY AUDIT LOG TRIGGERS +DROP TRIGGER IF EXISTS audit_bookings ON bookings; CREATE TRIGGER audit_bookings AFTER INSERT OR UPDATE OR DELETE ON bookings FOR EACH ROW EXECUTE FUNCTION log_audit(); +DROP TRIGGER IF EXISTS audit_customers ON customers; CREATE TRIGGER audit_customers AFTER INSERT OR UPDATE OR DELETE ON customers FOR EACH ROW EXECUTE FUNCTION log_audit(); +DROP TRIGGER IF EXISTS audit_invitations ON invitations; CREATE TRIGGER audit_invitations AFTER INSERT OR UPDATE OR DELETE ON invitations FOR EACH ROW EXECUTE FUNCTION log_audit(); +DROP TRIGGER IF EXISTS audit_staff ON staff; CREATE TRIGGER audit_staff AFTER INSERT OR UPDATE OR DELETE ON staff FOR EACH ROW EXECUTE FUNCTION log_audit(); +DROP TRIGGER IF EXISTS audit_services ON services; CREATE TRIGGER audit_services AFTER INSERT OR UPDATE OR DELETE ON services FOR EACH ROW EXECUTE FUNCTION log_audit(); @@ -762,6 +825,7 @@ BEGIN END; $$ LANGUAGE plpgsql; +DROP TRIGGER IF EXISTS booking_generate_short_id ON bookings; CREATE TRIGGER booking_generate_short_id BEFORE INSERT ON bookings FOR EACH ROW EXECUTE FUNCTION generate_booking_short_id(); @@ -774,22 +838,11 @@ BEGIN RAISE NOTICE '==========================================='; RAISE NOTICE 'SALONOS - DATABASE MIGRATION COMPLETED'; RAISE NOTICE '==========================================='; - RAISE NOTICE '✅ Tables created: 8'; - RAISE NOTICE '✅ Functions created: 14'; - RAISE NOTICE '✅ Triggers active: 17+'; - RAISE NOTICE '✅ RLS policies configured: 20+'; - RAISE NOTICE '✅ ENUM types created: 6'; - RAISE NOTICE '==========================================='; - RAISE NOTICE 'NEXT STEPS:'; - RAISE NOTICE '1. Configure Auth in Supabase Dashboard'; - RAISE NOTICE '2. Create test users with specific roles'; - RAISE NOTICE '3. Test Short ID generation:'; - RAISE NOTICE ' SELECT generate_short_id();'; - RAISE NOTICE '4. Test invitation code generation:'; - RAISE NOTICE ' SELECT generate_invitation_code();'; - RAISE NOTICE '5. Verify tables:'; - RAISE NOTICE ' SELECT table_name FROM information_schema.tables'; - RAISE NOTICE ' WHERE table_schema = ''public'' ORDER BY table_name;'; + RAISE NOTICE 'Tables created: 8'; + RAISE NOTICE 'Functions created: 14'; + RAISE NOTICE 'Triggers active: 17+'; + RAISE NOTICE 'RLS policies configured: 20+'; + RAISE NOTICE 'ENUM types created: 6'; RAISE NOTICE '==========================================='; END $$; diff --git a/db/migrations/00_FULL_MIGRATION_FINAL.sql b/supabase/migrations/20260115235800_add_kiosk_tables.sql similarity index 75% rename from db/migrations/00_FULL_MIGRATION_FINAL.sql rename to supabase/migrations/20260115235800_add_kiosk_tables.sql index eab1d5f..40a8c0b 100644 --- a/db/migrations/00_FULL_MIGRATION_FINAL.sql +++ b/supabase/migrations/20260115235800_add_kiosk_tables.sql @@ -8,20 +8,34 @@ -- BEGIN MIGRATION 001: INITIAL SCHEMA -- ============================================ --- Habilitar UUID extension -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +-- UUID extension not needed in Supabase (uses gen_random_uuid) -- ENUMS -CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer'); -CREATE TYPE customer_tier AS ENUM ('free', 'gold'); -CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'cancelled', 'completed', 'no_show'); -CREATE TYPE invitation_status AS ENUM ('pending', 'used', 'expired'); -CREATE TYPE resource_type AS ENUM ('station', 'room', 'equipment'); -CREATE TYPE audit_action AS ENUM ('create', 'update', 'delete', 'reset_invitations', 'payment', 'status_change'); +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN + CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'customer_tier') THEN + CREATE TYPE customer_tier AS ENUM ('free', 'gold', 'black', 'VIP'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'booking_status') THEN + CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'cancelled', 'completed', 'no_show'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'invitation_status') THEN + CREATE TYPE invitation_status AS ENUM ('pending', 'used', 'expired'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'resource_type') THEN + CREATE TYPE resource_type AS ENUM ('station', 'room', 'equipment'); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'audit_action') THEN + CREATE TYPE audit_action AS ENUM ('create', 'update', 'delete', 'reset_invitations', 'payment', 'status_change'); + END IF; +END $$; -- LOCATIONS -CREATE TABLE locations ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), +CREATE TABLE IF NOT EXISTS locations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL, timezone VARCHAR(50) NOT NULL DEFAULT 'UTC', address TEXT, @@ -32,8 +46,8 @@ CREATE TABLE locations ( ); -- RESOURCES -CREATE TABLE resources ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), +CREATE TABLE IF NOT EXISTS resources ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, type resource_type NOT NULL, @@ -44,8 +58,8 @@ CREATE TABLE resources ( ); -- STAFF -CREATE TABLE staff ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), +CREATE TABLE IF NOT EXISTS staff ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE, role user_role NOT NULL CHECK (role IN ('admin', 'manager', 'staff', 'artist')), @@ -58,8 +72,8 @@ CREATE TABLE staff ( ); -- SERVICES -CREATE TABLE services ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), +CREATE TABLE IF NOT EXISTS services ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL, description TEXT, duration_minutes INTEGER NOT NULL CHECK (duration_minutes > 0), @@ -72,8 +86,8 @@ CREATE TABLE services ( ); -- CUSTOMERS -CREATE TABLE customers ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), +CREATE TABLE IF NOT EXISTS customers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID UNIQUE, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, @@ -90,8 +104,8 @@ CREATE TABLE customers ( ); -- INVITATIONS -CREATE TABLE invitations ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), +CREATE TABLE IF NOT EXISTS invitations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), inviter_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, code VARCHAR(10) UNIQUE NOT NULL, email VARCHAR(255), @@ -104,8 +118,8 @@ CREATE TABLE invitations ( ); -- BOOKINGS -CREATE TABLE bookings ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), +CREATE TABLE IF NOT EXISTS bookings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), short_id VARCHAR(6) UNIQUE NOT NULL, customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT, @@ -126,8 +140,8 @@ CREATE TABLE bookings ( ); -- AUDIT LOGS -CREATE TABLE audit_logs ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), +CREATE TABLE IF NOT EXISTS audit_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), entity_type VARCHAR(50) NOT NULL, entity_id UUID NOT NULL, action audit_action NOT NULL, @@ -142,30 +156,30 @@ CREATE TABLE audit_logs ( ); -- INDEXES -CREATE INDEX idx_locations_active ON locations(is_active); -CREATE INDEX idx_resources_location ON resources(location_id); -CREATE INDEX idx_resources_active ON resources(location_id, is_active); -CREATE INDEX idx_staff_user ON staff(user_id); -CREATE INDEX idx_staff_location ON staff(location_id); -CREATE INDEX idx_staff_role ON staff(location_id, role, is_active); -CREATE INDEX idx_services_active ON services(is_active); -CREATE INDEX idx_customers_tier ON customers(tier); -CREATE INDEX idx_customers_email ON customers(email); -CREATE INDEX idx_customers_active ON customers(is_active); -CREATE INDEX idx_invitations_inviter ON invitations(inviter_id); -CREATE INDEX idx_invitations_code ON invitations(code); -CREATE INDEX idx_invitations_week ON invitations(week_start_date, status); -CREATE INDEX idx_bookings_customer ON bookings(customer_id); -CREATE INDEX idx_bookings_staff ON bookings(staff_id); -CREATE INDEX idx_bookings_secondary_artist ON bookings(secondary_artist_id); -CREATE INDEX idx_bookings_location ON bookings(location_id); -CREATE INDEX idx_bookings_resource ON bookings(resource_id); -CREATE INDEX idx_bookings_time ON bookings(start_time_utc, end_time_utc); -CREATE INDEX idx_bookings_status ON bookings(status); -CREATE INDEX idx_bookings_short_id ON bookings(short_id); -CREATE INDEX idx_audit_entity ON audit_logs(entity_type, entity_id); -CREATE INDEX idx_audit_action ON audit_logs(action, created_at); -CREATE INDEX idx_audit_performed ON audit_logs(performed_by); +CREATE INDEX IF NOT EXISTS idx_locations_active ON locations(is_active); +CREATE INDEX IF NOT EXISTS idx_resources_location ON resources(location_id); +CREATE INDEX IF NOT EXISTS idx_resources_active ON resources(location_id, is_active); +CREATE INDEX IF NOT EXISTS idx_staff_user ON staff(user_id); +CREATE INDEX IF NOT EXISTS idx_staff_location ON staff(location_id); +CREATE INDEX IF NOT EXISTS idx_staff_role ON staff(location_id, role, is_active); +CREATE INDEX IF NOT EXISTS idx_services_active ON services(is_active); +CREATE INDEX IF NOT EXISTS idx_customers_tier ON customers(tier); +CREATE INDEX IF NOT EXISTS idx_customers_email ON customers(email); +CREATE INDEX IF NOT EXISTS idx_customers_active ON customers(is_active); +CREATE INDEX IF NOT EXISTS idx_invitations_inviter ON invitations(inviter_id); +CREATE INDEX IF NOT EXISTS idx_invitations_code ON invitations(code); +CREATE INDEX IF NOT EXISTS idx_invitations_week ON invitations(week_start_date, status); +CREATE INDEX IF NOT EXISTS idx_bookings_customer ON bookings(customer_id); +CREATE INDEX IF NOT EXISTS idx_bookings_staff ON bookings(staff_id); +CREATE INDEX IF NOT EXISTS idx_bookings_secondary_artist ON bookings(secondary_artist_id); +CREATE INDEX IF NOT EXISTS idx_bookings_location ON bookings(location_id); +CREATE INDEX IF NOT EXISTS idx_bookings_resource ON bookings(resource_id); +CREATE INDEX IF NOT EXISTS idx_bookings_time ON bookings(start_time_utc, end_time_utc); +CREATE INDEX IF NOT EXISTS idx_bookings_status ON bookings(status); +CREATE INDEX IF NOT EXISTS idx_bookings_short_id ON bookings(short_id); +CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_logs(entity_type, entity_id); +CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action, created_at); +CREATE INDEX IF NOT EXISTS idx_audit_performed ON audit_logs(performed_by); -- UPDATED_AT TRIGGER FUNCTION CREATE OR REPLACE FUNCTION update_updated_at() @@ -177,31 +191,40 @@ END; $$ LANGUAGE plpgsql; -- UPDATED_AT TRIGGERS +DROP TRIGGER IF EXISTS locations_updated_at ON locations; CREATE TRIGGER locations_updated_at BEFORE UPDATE ON locations FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +DROP TRIGGER IF EXISTS resources_updated_at ON resources; CREATE TRIGGER resources_updated_at BEFORE UPDATE ON resources FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +DROP TRIGGER IF EXISTS staff_updated_at ON staff; CREATE TRIGGER staff_updated_at BEFORE UPDATE ON staff FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +DROP TRIGGER IF EXISTS services_updated_at ON services; CREATE TRIGGER services_updated_at BEFORE UPDATE ON services FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +DROP TRIGGER IF EXISTS customers_updated_at ON customers; CREATE TRIGGER customers_updated_at BEFORE UPDATE ON customers FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +DROP TRIGGER IF EXISTS invitations_updated_at ON invitations; CREATE TRIGGER invitations_updated_at BEFORE UPDATE ON invitations FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +DROP TRIGGER IF EXISTS bookings_updated_at ON bookings; CREATE TRIGGER bookings_updated_at BEFORE UPDATE ON bookings FOR EACH ROW EXECUTE FUNCTION update_updated_at(); -- CONSTRAINTS (Simple ones only - no subqueries) +ALTER TABLE bookings DROP CONSTRAINT IF EXISTS check_booking_time; ALTER TABLE bookings ADD CONSTRAINT check_booking_time CHECK (end_time_utc > start_time_utc); +ALTER TABLE invitations DROP CONSTRAINT IF EXISTS check_week_start_is_monday; ALTER TABLE invitations ADD CONSTRAINT check_week_start_is_monday CHECK (EXTRACT(ISODOW FROM week_start_date) = 1); @@ -221,6 +244,7 @@ BEGIN END; $$ LANGUAGE plpgsql; +DROP TRIGGER IF EXISTS validate_booking_secondary_artist ON bookings; CREATE TRIGGER validate_booking_secondary_artist BEFORE INSERT OR UPDATE ON bookings FOR EACH ROW EXECUTE FUNCTION validate_secondary_artist_role(); @@ -299,32 +323,39 @@ ALTER TABLE bookings ENABLE ROW LEVEL SECURITY; ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY; -- LOCATIONS POLICIES +DROP POLICY IF EXISTS "locations_select_staff_higher" ON locations; CREATE POLICY "locations_select_staff_higher" ON locations FOR SELECT USING (is_staff_or_higher() OR is_admin()); +DROP POLICY IF EXISTS "locations_modify_admin_manager" ON locations; CREATE POLICY "locations_modify_admin_manager" ON locations FOR ALL USING (get_current_user_role() IN ('admin', 'manager')); -- RESOURCES POLICIES +DROP POLICY IF EXISTS "resources_select_staff_higher" ON resources; CREATE POLICY "resources_select_staff_higher" ON resources FOR SELECT USING (is_staff_or_higher() OR is_admin()); +DROP POLICY IF EXISTS "resources_select_artist" ON resources; CREATE POLICY "resources_select_artist" ON resources FOR SELECT USING (is_artist()); +DROP POLICY IF EXISTS "resources_modify_admin_manager" ON resources; CREATE POLICY "resources_modify_admin_manager" ON resources FOR ALL USING (get_current_user_role() IN ('admin', 'manager')); -- STAFF POLICIES +DROP POLICY IF EXISTS "staff_select_admin_manager" ON staff; CREATE POLICY "staff_select_admin_manager" ON staff FOR SELECT USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "staff_select_same_location" ON staff; CREATE POLICY "staff_select_same_location" ON staff FOR SELECT USING ( @@ -334,6 +365,7 @@ CREATE POLICY "staff_select_same_location" ON staff ) ); +DROP POLICY IF EXISTS "staff_select_artist_view_artists" ON staff; CREATE POLICY "staff_select_artist_view_artists" ON staff FOR SELECT USING ( @@ -344,74 +376,91 @@ CREATE POLICY "staff_select_artist_view_artists" ON staff staff.role = 'artist' ); +DROP POLICY IF EXISTS "staff_modify_admin_manager" ON staff; CREATE POLICY "staff_modify_admin_manager" ON staff FOR ALL USING (get_current_user_role() IN ('admin', 'manager')); -- SERVICES POLICIES +DROP POLICY IF EXISTS "services_select_all" ON services; CREATE POLICY "services_select_all" ON services FOR SELECT USING (is_active = true); +DROP POLICY IF EXISTS "services_all_admin_manager" ON services; CREATE POLICY "services_all_admin_manager" ON services FOR ALL USING (get_current_user_role() IN ('admin', 'manager')); -- CUSTOMERS POLICIES (RESTRICTED FOR ARTISTS) +DROP POLICY IF EXISTS "customers_select_admin_manager" ON customers; CREATE POLICY "customers_select_admin_manager" ON customers FOR SELECT USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "customers_select_staff" ON customers; CREATE POLICY "customers_select_staff" ON customers FOR SELECT USING (is_staff_or_higher()); +DROP POLICY IF EXISTS "customers_select_artist_restricted" ON customers; CREATE POLICY "customers_select_artist_restricted" ON customers FOR SELECT USING (is_artist()); +DROP POLICY IF EXISTS "customers_select_own" ON customers; CREATE POLICY "customers_select_own" ON customers FOR SELECT USING (is_customer() AND user_id = auth.uid()); +DROP POLICY IF EXISTS "customers_modify_admin_manager" ON customers; CREATE POLICY "customers_modify_admin_manager" ON customers FOR ALL USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "customers_modify_staff" ON customers; CREATE POLICY "customers_modify_staff" ON customers FOR ALL USING (is_staff_or_higher()); +DROP POLICY IF EXISTS "customers_update_own" ON customers; CREATE POLICY "customers_update_own" ON customers FOR UPDATE USING (is_customer() AND user_id = auth.uid()); -- INVITATIONS POLICIES +DROP POLICY IF EXISTS "invitations_select_admin_manager" ON invitations; CREATE POLICY "invitations_select_admin_manager" ON invitations FOR SELECT USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "invitations_select_staff" ON invitations; CREATE POLICY "invitations_select_staff" ON invitations FOR SELECT USING (is_staff_or_higher()); +DROP POLICY IF EXISTS "invitations_select_own" ON invitations; CREATE POLICY "invitations_select_own" ON invitations FOR SELECT USING (is_customer() AND inviter_id = (SELECT id FROM customers WHERE user_id = auth.uid())); +DROP POLICY IF EXISTS "invitations_modify_admin_manager" ON invitations; CREATE POLICY "invitations_modify_admin_manager" ON invitations FOR ALL USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "invitations_modify_staff" ON invitations; CREATE POLICY "invitations_modify_staff" ON invitations FOR ALL USING (is_staff_or_higher()); -- BOOKINGS POLICIES +DROP POLICY IF EXISTS "bookings_select_admin_manager" ON bookings; CREATE POLICY "bookings_select_admin_manager" ON bookings FOR SELECT USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "bookings_select_staff_location" ON bookings; CREATE POLICY "bookings_select_staff_location" ON bookings FOR SELECT USING ( @@ -421,6 +470,7 @@ CREATE POLICY "bookings_select_staff_location" ON bookings ) ); +DROP POLICY IF EXISTS "bookings_select_artist_own" ON bookings; CREATE POLICY "bookings_select_artist_own" ON bookings FOR SELECT USING ( @@ -429,14 +479,17 @@ CREATE POLICY "bookings_select_artist_own" ON bookings secondary_artist_id = (SELECT id FROM staff WHERE user_id = auth.uid())) ); +DROP POLICY IF EXISTS "bookings_select_own" ON bookings; CREATE POLICY "bookings_select_own" ON bookings FOR SELECT USING (is_customer() AND customer_id = (SELECT id FROM customers WHERE user_id = auth.uid())); +DROP POLICY IF EXISTS "bookings_modify_admin_manager" ON bookings; CREATE POLICY "bookings_modify_admin_manager" ON bookings FOR ALL USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "bookings_modify_staff_location" ON bookings; CREATE POLICY "bookings_modify_staff_location" ON bookings FOR ALL USING ( @@ -446,10 +499,12 @@ CREATE POLICY "bookings_modify_staff_location" ON bookings ) ); +DROP POLICY IF EXISTS "bookings_no_modify_artist" ON bookings; CREATE POLICY "bookings_no_modify_artist" ON bookings FOR ALL USING (NOT is_artist()); +DROP POLICY IF EXISTS "bookings_create_own" ON bookings; CREATE POLICY "bookings_create_own" ON bookings FOR INSERT WITH CHECK ( @@ -457,6 +512,7 @@ CREATE POLICY "bookings_create_own" ON bookings customer_id = (SELECT id FROM customers WHERE user_id = auth.uid()) ); +DROP POLICY IF EXISTS "bookings_update_own" ON bookings; CREATE POLICY "bookings_update_own" ON bookings FOR UPDATE USING ( @@ -465,10 +521,12 @@ CREATE POLICY "bookings_update_own" ON bookings ); -- AUDIT LOGS POLICIES +DROP POLICY IF EXISTS "audit_logs_select_admin_manager" ON audit_logs; CREATE POLICY "audit_logs_select_admin_manager" ON audit_logs FOR SELECT USING (get_current_user_role() IN ('admin', 'manager')); +DROP POLICY IF EXISTS "audit_logs_select_staff_location" ON audit_logs; CREATE POLICY "audit_logs_select_staff_location" ON audit_logs FOR SELECT USING ( @@ -481,6 +539,7 @@ CREATE POLICY "audit_logs_select_staff_location" ON audit_logs ) ); +DROP POLICY IF EXISTS "audit_logs_no_insert" ON audit_logs; CREATE POLICY "audit_logs_no_insert" ON audit_logs FOR INSERT WITH CHECK (false); @@ -521,18 +580,18 @@ CREATE OR REPLACE FUNCTION generate_invitation_code() RETURNS VARCHAR(10) AS $$ DECLARE chars VARCHAR(36) := '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - code VARCHAR(10); + new_code VARCHAR(10); attempts INT := 0; max_attempts INT := 10; BEGIN LOOP - code := ''; + new_code := ''; FOR i IN 1..10 LOOP - code := code || substr(chars, floor(random() * 36 + 1)::INT, 1); + new_code := new_code || substr(chars, floor(random() * 36 + 1)::INT, 1); END LOOP; - IF NOT EXISTS (SELECT 1 FROM invitations WHERE code = code) THEN - RETURN code; + IF NOT EXISTS (SELECT 1 FROM invitations WHERE code = new_code) THEN + RETURN new_code; END IF; attempts := attempts + 1; @@ -593,7 +652,7 @@ BEGIN customer_uuid, 'reset_invitations', '{"week_start": null}'::JSONB, - '{"week_start": "' || week_start || '", "count": 5}'::JSONB, + jsonb_build_object('week_start', week_start, 'count', 5), NULL, 'system', '{"reset_type": "weekly", "invitations_created": 5}'::JSONB @@ -637,7 +696,7 @@ BEGIN ) VALUES ( 'invitations', - uuid_generate_v4(), + gen_random_uuid(), 'reset_invitations', '{}'::JSONB, result, @@ -736,18 +795,23 @@ END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- APPLY AUDIT LOG TRIGGERS +DROP TRIGGER IF EXISTS audit_bookings ON bookings; CREATE TRIGGER audit_bookings AFTER INSERT OR UPDATE OR DELETE ON bookings FOR EACH ROW EXECUTE FUNCTION log_audit(); +DROP TRIGGER IF EXISTS audit_customers ON customers; CREATE TRIGGER audit_customers AFTER INSERT OR UPDATE OR DELETE ON customers FOR EACH ROW EXECUTE FUNCTION log_audit(); +DROP TRIGGER IF EXISTS audit_invitations ON invitations; CREATE TRIGGER audit_invitations AFTER INSERT OR UPDATE OR DELETE ON invitations FOR EACH ROW EXECUTE FUNCTION log_audit(); +DROP TRIGGER IF EXISTS audit_staff ON staff; CREATE TRIGGER audit_staff AFTER INSERT OR UPDATE OR DELETE ON staff FOR EACH ROW EXECUTE FUNCTION log_audit(); +DROP TRIGGER IF EXISTS audit_services ON services; CREATE TRIGGER audit_services AFTER INSERT OR UPDATE OR DELETE ON services FOR EACH ROW EXECUTE FUNCTION log_audit(); @@ -762,6 +826,7 @@ BEGIN END; $$ LANGUAGE plpgsql; +DROP TRIGGER IF EXISTS booking_generate_short_id ON bookings; CREATE TRIGGER booking_generate_short_id BEFORE INSERT ON bookings FOR EACH ROW EXECUTE FUNCTION generate_booking_short_id(); diff --git a/supabase/migrations/20260115235900_seed_data.sql b/supabase/migrations/20260115235900_seed_data.sql new file mode 100644 index 0000000..b60cbcd --- /dev/null +++ b/supabase/migrations/20260115235900_seed_data.sql @@ -0,0 +1,393 @@ +-- ============================================ +-- SEED DE DATOS - SALONOS (IDEMPOTENTE) +-- Ejecutar múltiples veces sin errores +-- ============================================ + +-- 1. Crear Locations (solo si no existen) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA') THEN + INSERT INTO locations (name, timezone, address, phone, is_active) + VALUES + ('ANCHOR:23 - Via KLAVA', 'America/Monterrey', 'Blvd. Moctezuma 2370, Los Pinos 2do y 3er Sector, 25204 Saltillo, Coah.', '+52 81 1234 5678', true); + END IF; + + IF NOT EXISTS (SELECT 1 FROM locations WHERE name = 'TEST - Salón Principal') THEN + INSERT INTO locations (name, timezone, address, phone, is_active) + VALUES + ('TEST - Salón Principal', 'America/Monterrey', 'Av. Masaryk 123, Polanco, Ciudad de México', '+52 55 2345 6789', true); + END IF; +END $$; + +-- 2. Crear Resources (solo si no existen) +DO $$ +BEGIN + -- Para ANCHOR:23 - Via KLAVA + FOR i IN 1..3 LOOP + IF NOT EXISTS ( + SELECT 1 FROM resources r + JOIN locations l ON l.id = r.location_id + WHERE l.name = 'ANCHOR:23 - Via KLAVA' AND r.name = 'Sillón Pedicure ' || i + ) THEN + INSERT INTO resources (location_id, name, type, capacity, is_active) + SELECT id, 'Sillón Pedicure ' || i, 'station', 1, true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + END LOOP; + + FOR i IN 1..4 LOOP + IF NOT EXISTS ( + SELECT 1 FROM resources r + JOIN locations l ON l.id = r.location_id + WHERE l.name = 'ANCHOR:23 - Via KLAVA' AND r.name = 'Estación Manicure ' || i + ) THEN + INSERT INTO resources (location_id, name, type, capacity, is_active) + SELECT id, 'Estación Manicure ' || i, 'station', 1, true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + END LOOP; + + IF NOT EXISTS ( + SELECT 1 FROM resources r + JOIN locations l ON l.id = r.location_id + WHERE l.name = 'ANCHOR:23 - Via KLAVA' AND r.name = 'Estación Maquillaje' + ) THEN + INSERT INTO resources (location_id, name, type, capacity, is_active) + SELECT id, 'Estación Maquillaje', 'station', 1, true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM resources r + JOIN locations l ON l.id = r.location_id + WHERE l.name = 'ANCHOR:23 - Via KLAVA' AND r.name = 'Cama Pestañas' + ) THEN + INSERT INTO resources (location_id, name, type, capacity, is_active) + SELECT id, 'Cama Pestañas', 'station', 1, true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + + -- Para TEST - Salón Principal + FOR i IN 1..3 LOOP + IF NOT EXISTS ( + SELECT 1 FROM resources r + JOIN locations l ON l.id = r.location_id + WHERE l.name = 'TEST - Salón Principal' AND r.name = 'Sillón Pedicure ' || i + ) THEN + INSERT INTO resources (location_id, name, type, capacity, is_active) + SELECT id, 'Sillón Pedicure ' || i, 'station', 1, true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; + END LOOP; + + FOR i IN 1..4 LOOP + IF NOT EXISTS ( + SELECT 1 FROM resources r + JOIN locations l ON l.id = r.location_id + WHERE l.name = 'TEST - Salón Principal' AND r.name = 'Estación Manicure ' || i + ) THEN + INSERT INTO resources (location_id, name, type, capacity, is_active) + SELECT id, 'Estación Manicure ' || i, 'station', 1, true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; + END LOOP; + + IF NOT EXISTS ( + SELECT 1 FROM resources r + JOIN locations l ON l.id = r.location_id + WHERE l.name = 'TEST - Salón Principal' AND r.name = 'Estación Maquillaje' + ) THEN + INSERT INTO resources (location_id, name, type, capacity, is_active) + SELECT id, 'Estación Maquillaje', 'station', 1, true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM resources r + JOIN locations l ON l.id = r.location_id + WHERE l.name = 'TEST - Salón Principal' AND r.name = 'Cama Pestañas' + ) THEN + INSERT INTO resources (location_id, name, type, capacity, is_active) + SELECT id, 'Cama Pestañas', 'station', 1, true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; +END $$; + +-- 3. Crear Staff (solo si no existen por display_name) +DO $$ +BEGIN + -- Admin Principal + IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Admin Principal') THEN + INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) + SELECT gen_random_uuid(), id, 'admin', 'Admin Principal', '+52 55 1111 2222', true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + + -- ANCHOR:23 - Via KLAVA: 1 Staff + 4 Artists + 1 Kiosk + IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Staff KLAVA Coordinadora') THEN + INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) + SELECT gen_random_uuid(), id, 'staff', 'Staff KLAVA Coordinadora', '+52 55 3333 4444', true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Kiosk KLAVA Principal') THEN + INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) + SELECT gen_random_uuid(), id, 'kiosk', 'Kiosk KLAVA Principal', '+52 55 3333 0000', true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist KLAVA María García') THEN + INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) + SELECT gen_random_uuid(), id, 'artist', 'Artist KLAVA María García', '+52 55 4444 5555', true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist KLAVA Ana Rodríguez') THEN + INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) + SELECT gen_random_uuid(), id, 'artist', 'Artist KLAVA Ana Rodríguez', '+52 55 5555 6666', true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist KLAVA Sofía López') THEN + INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) + SELECT gen_random_uuid(), id, 'artist', 'Artist KLAVA Sofía López', '+52 55 5555 7777', true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist KLAVA Valentina Ruiz') THEN + INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) + SELECT gen_random_uuid(), id, 'artist', 'Artist KLAVA Valentina Ruiz', '+52 55 5555 8888', true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + + -- TEST - Salón Principal: 1 Staff + 4 Artists + 1 Kiosk + IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Staff Test Coordinador') THEN + INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) + SELECT gen_random_uuid(), id, 'staff', 'Staff Test Coordinador', '+52 55 6666 1111', true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Kiosk Test Principal') THEN + INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) + SELECT gen_random_uuid(), id, 'kiosk', 'Kiosk Test Principal', '+52 55 6666 0000', true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist Test Carla López') THEN + INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) + SELECT gen_random_uuid(), id, 'artist', 'Artist Test Carla López', '+52 55 7777 8888', true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist Test Daniela García') THEN + INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) + SELECT gen_random_uuid(), id, 'artist', 'Artist Test Daniela García', '+52 55 7777 9999', true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist Test Andrea Martínez') THEN + INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) + SELECT gen_random_uuid(), id, 'artist', 'Artist Test Andrea Martínez', '+52 55 7777 0000', true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM staff WHERE display_name = 'Artist Test Fernanda Torres') THEN + INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) + SELECT gen_random_uuid(), id, 'artist', 'Artist Test Fernanda Torres', '+52 55 7777 1111', true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; +END $$; + +-- 4. Crear Services (solo si no existen) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Corte y Estilizado') THEN + INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) + VALUES ('Corte y Estilizado', 'Corte de cabello profesional con lavado y estilizado', 60, 500.00, false, false, true); + END IF; + + IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Color Completo') THEN + INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) + VALUES ('Color Completo', 'Tinte completo con protección capilar', 120, 1200.00, false, true, true); + END IF; + + IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Balayage Premium') THEN + INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) + VALUES ('Balayage Premium', 'Técnica de balayage con productos premium', 180, 2000.00, true, true, true); + END IF; + + IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Tratamiento Kératina') THEN + INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) + VALUES ('Tratamiento Kératina', 'Tratamiento de kératina para cabello dañado', 90, 1500.00, false, false, true); + END IF; + + IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Peinado Evento') THEN + INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) + VALUES ('Peinado Evento', 'Peinado para eventos especiales', 45, 800.00, false, true, true); + END IF; + + IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Pedicure Spa') THEN + INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) + VALUES ('Pedicure Spa', 'Pedicure completo con exfoliación y mascarilla', 60, 450.00, false, false, true); + END IF; + + IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Manicure Gel') THEN + INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) + VALUES ('Manicure Gel', 'Manicure con esmalte de gel duradero', 45, 350.00, false, true, true); + END IF; + + IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Uñas Acrílicas') THEN + INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) + VALUES ('Uñas Acrílicas', 'Aplicación de uñas acrílicas con diseño', 120, 800.00, false, true, true); + END IF; + + IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Maquillaje Profesional') THEN + INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) + VALUES ('Maquillaje Profesional', 'Maquillaje para eventos especiales', 60, 1200.00, false, true, true); + END IF; + + IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Extensión de Pestañas') THEN + INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) + VALUES ('Extensión de Pestañas', 'Aplicación de extensiones pestañas volumen 3D', 90, 1500.00, false, true, true); + END IF; + + IF NOT EXISTS (SELECT 1 FROM services WHERE name = 'Servicio Express (Dual Artist)') THEN + INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) + VALUES ('Servicio Express (Dual Artist)', 'Servicio rápido con dos artists simultáneas', 30, 600.00, true, true, true); + END IF; +END $$; + +-- 5. Crear Customers (solo si no existen por email) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM customers WHERE email = 'sofia.ramirez@example.com') THEN + INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, notes, total_spent, total_visits, last_visit_date, is_active) + VALUES (gen_random_uuid(), 'Sofía', 'Ramírez', 'sofia.ramirez@example.com', '+52 55 1111 1111', 'gold', 'Cliente VIP. Prefiere Artists María y Ana.', 15000.00, 25, '2025-12-20', true); + END IF; + + IF NOT EXISTS (SELECT 1 FROM customers WHERE email = 'valentina.hernandez@example.com') THEN + INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, notes, total_spent, total_visits, last_visit_date, is_active) + VALUES (gen_random_uuid(), 'Valentina', 'Hernández', 'valentina.hernandez@example.com', '+52 55 2222 2222', 'gold', 'Cliente regular. Prefiere horarios de la mañana.', 8500.00, 15, '2025-12-15', true); + END IF; + + IF NOT EXISTS (SELECT 1 FROM customers WHERE email = 'camila.lopez@example.com') THEN + INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, notes, total_spent, total_visits, last_visit_date, is_active) + VALUES (gen_random_uuid(), 'Camila', 'López', 'camila.lopez@example.com', '+52 55 3333 3333', 'free', 'Nueva cliente. Referida por Valentina.', 500.00, 1, '2025-12-10', true); + END IF; + + IF NOT EXISTS (SELECT 1 FROM customers WHERE email = 'isabella.garcia@example.com') THEN + INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, notes, total_spent, total_visits, last_visit_date, is_active) + VALUES (gen_random_uuid(), 'Isabella', 'García', 'isabella.garcia@example.com', '+52 55 4444 4444', 'gold', 'Cliente VIP. Requiere servicio de Balayage.', 22000.00, 30, '2025-12-18', true); + END IF; +END $$; + +-- 6. Crear Amenities (cortesías para kiosks) +DO $$ +BEGIN + -- ANCHOR:23 - Via KLAVA Amenities + IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Café Americano' AND location_id = (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1)) THEN + INSERT INTO amenities (location_id, name, description, category, is_active) + SELECT id, 'Café Americano', 'Café negro tradicional', 'coffee', true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Café Latte' AND location_id = (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1)) THEN + INSERT INTO amenities (location_id, name, description, category, is_active) + SELECT id, 'Café Latte', 'Café con leche vaporizada', 'coffee', true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Té Verde' AND location_id = (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1)) THEN + INSERT INTO amenities (location_id, name, description, category, is_active) + SELECT id, 'Té Verde', 'Té verde orgánico', 'coffee', true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Cocktail Mojito' AND location_id = (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1)) THEN + INSERT INTO amenities (location_id, name, description, category, is_active) + SELECT id, 'Cocktail Mojito', 'Refresco de menta y limón', 'cocktail', true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Mocktail Piña Colada' AND location_id = (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1)) THEN + INSERT INTO amenities (location_id, name, description, category, is_active) + SELECT id, 'Mocktail Piña Colada', 'Bebida tropical sin alcohol', 'mocktail', true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Agua Mineral' AND location_id = (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1)) THEN + INSERT INTO amenities (location_id, name, description, category, is_active) + SELECT id, 'Agua Mineral', 'Agua mineral fresca', 'other', true + FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA'; + END IF; + + -- TEST - Salón Principal Amenities + IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Café Espresso' AND location_id = (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1)) THEN + INSERT INTO amenities (location_id, name, description, category, is_active) + SELECT id, 'Café Espresso', 'Café espresso intenso', 'coffee', true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Café Cappuccino' AND location_id = (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1)) THEN + INSERT INTO amenities (location_id, name, description, category, is_active) + SELECT id, 'Café Cappuccino', 'Café con espuma de leche', 'coffee', true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Té Chai' AND location_id = (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1)) THEN + INSERT INTO amenities (location_id, name, description, category, is_active) + SELECT id, 'Té Chai', 'Té chai con especias', 'coffee', true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Cocktail Margarita' AND location_id = (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1)) THEN + INSERT INTO amenities (location_id, name, description, category, is_active) + SELECT id, 'Cocktail Margarita', 'Cóctel clásico de tequila', 'cocktail', true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Mocktail Limonada' AND location_id = (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1)) THEN + INSERT INTO amenities (location_id, name, description, category, is_active) + SELECT id, 'Mocktail Limonada', 'Limonada fresca natural', 'mocktail', true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM amenities WHERE name = 'Revista de Moda' AND location_id = (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1)) THEN + INSERT INTO amenities (location_id, name, description, category, is_active) + SELECT id, 'Revista de Moda', 'Revistas de moda actual', 'other', true + FROM locations WHERE name = 'TEST - Salón Principal'; + END IF; +END $$; + +-- 7. Crear Invitaciones (para clientes Gold) +DO $$ +DECLARE + week_start DATE; + customer_record RECORD; +BEGIN + week_start := get_week_start(CURRENT_DATE); + + FOR customer_record IN + SELECT id FROM customers WHERE tier = 'gold' AND is_active = true + LOOP + PERFORM reset_weekly_invitations_for_customer(customer_record.id); + END LOOP; +END $$; + +-- 8. Resumen de datos creados +DO $$ +BEGIN + RAISE NOTICE '=========================================='; + RAISE NOTICE 'SALONOS - SEED DE DATOS COMPLETADO'; + RAISE NOTICE '=========================================='; + RAISE NOTICE 'Locations: %', (SELECT COUNT(*) FROM locations); + RAISE NOTICE 'Resources: %', (SELECT COUNT(*) FROM resources); + RAISE NOTICE 'Staff: %', (SELECT COUNT(*) FROM staff); + RAISE NOTICE 'Services: %', (SELECT COUNT(*) FROM services); + RAISE NOTICE 'Customers: %', (SELECT COUNT(*) FROM customers); + RAISE NOTICE 'Amenities: %', (SELECT COUNT(*) FROM amenities); + RAISE NOTICE '=========================================='; + RAISE NOTICE 'Base de datos lista para desarrollo'; + RAISE NOTICE '=========================================='; +END +$$; diff --git a/tasks_mg.md b/tasks_mg.md index a63ef9e..a89261b 100644 --- a/tasks_mg.md +++ b/tasks_mg.md @@ -57,56 +57,69 @@ SALONOS - DATABASE MIGRATION COMPLETED ```sql INSERT INTO locations (name, timezone, address, phone, is_active) VALUES - ('Salón Principal - Centro', 'America/Mexico_City', 'Av. Reforma 222, Centro Histórico, Ciudad de México', '+52 55 1234 5678', true), - ('Salón Norte - Polanco', 'America/Mexico_City', 'Av. Masaryk 123, Polanco, Ciudad de México', '+52 55 2345 6789', true), - ('Salón Sur - Coyoacán', 'America/Mexico_City', 'Calle Hidalgo 456, Coyoacán, Ciudad de México', '+52 55 3456 7890', true); + ('ANCHOR:23 - Via KLAVA', 'America/Monterrey', 'Blvd. Moctezuma 2370, Los Pinos 2do y 3er Sector, 25204 Saltillo, Coah.', '+52 844 123 4567', true), + ('TEST - Salón Principal', 'America/Monterrey', 'Av. Universidad 456, Zona Centro, 25000 Saltillo, Coah.', '+52 844 234 5678', true); ``` 3. Deberías ver: `Success, no rows returned` -### 2.2 Crear Resources +### 2.2 Crear Resources (4 estaciones por salón) 1. Nuevo query, copiar y ejecutar: ```sql +-- ANCHOR:23 - Via KLAVA (4 estaciones: 1 maquillaje + 3 pedicure) INSERT INTO resources (location_id, name, type, capacity, is_active) SELECT - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - 'Estación ' || generate_series(1, 3)::TEXT, - 'station', + (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1), + 'Estación de Maquillaje', + 'station'::resource_type, 1, true UNION ALL SELECT - (SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), - 'Estación ' || generate_series(1, 2)::TEXT, - 'station', + (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1), + 'Sillón Pedicure ' || generate_series(1, 3)::TEXT, + 'station'::resource_type, + 1, + true +UNION ALL +-- TEST - Salón Principal (4 estaciones: 1 maquillaje + 3 pedicure) +SELECT + (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1), + 'Estación de Maquillaje', + 'station'::resource_type, 1, true UNION ALL SELECT - (SELECT id FROM locations WHERE name = 'Salón Sur - Coyoacán' LIMIT 1), - 'Estación 1', - 'station', + (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1), + 'Sillón Pedicure ' || generate_series(1, 3)::TEXT, + 'station'::resource_type, 1, true; ``` 2. Deberías ver: `Success, no rows returned` -### 2.3 Crear Staff +### 2.3 Crear Staff (5 empleadas por salón) 1. Nuevo query, copiar y ejecutar: ```sql INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) VALUES - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'admin', 'Admin Principal', '+52 55 1111 2222', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'manager', 'Manager Centro', '+52 55 2222 3333', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), 'manager', 'Manager Polanco', '+52 55 6666 7777', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'staff', 'Staff Coordinadora', '+52 55 3333 4444', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'artist', 'Artist María García', '+52 55 4444 5555', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'artist', 'Artist Ana Rodríguez', '+52 55 5555 6666', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), 'artist', 'Artist Carla López', '+52 55 7777 8888', true), - (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Sur - Coyoacán' LIMIT 1), 'artist', 'Artist Laura Martínez', '+52 55 8888 9999', true); + -- ANCHOR:23 - Via KLAVA (5 empleadas) + (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1), 'admin', 'Mariana González', '+52 844 111 2222', true), + (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1), 'manager', 'Daniela Sánchez', '+52 844 222 3333', true), + (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1), 'artist', 'Karla Rodríguez', '+52 844 333 4444', true), + (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1), 'artist', 'Fernanda Martínez', '+52 844 444 5555', true), + (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1), 'artist', 'Paola Hernández', '+52 844 555 6666', true), + + -- TEST - Salón Principal (5 empleadas) + (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1), 'manager', 'Sofía Ramírez', '+52 844 666 7777', true), + (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1), 'artist', 'Valeria López', '+52 844 777 8888', true), + (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1), 'artist', 'Andrea García', '+52 844 888 9999', true), + (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1), 'artist', 'Camila Torres', '+52 844 999 0000', true), + (uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1), 'artist', 'Regina Flores', '+52 844 000 1111', true); ``` 2. Deberías ver: `Success, no rows returned` @@ -117,26 +130,61 @@ VALUES ```sql INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) VALUES - ('Corte y Estilizado', 'Corte de cabello profesional con lavado y estilizado', 60, 500.00, false, false, true), - ('Color Completo', 'Tinte completo con protección capilar', 120, 1200.00, false, true, true), - ('Balayage Premium', 'Técnica de balayage con productos premium', 180, 2000.00, true, true, true), - ('Tratamiento Kératina', 'Tratamiento de kératina para cabello dañado', 90, 1500.00, false, false, true), - ('Peinado Evento', 'Peinado para eventos especiales', 45, 800.00, false, true, true), - ('Servicio Express (Dual Artist)', 'Servicio rápido con dos artists simultáneas', 30, 600.00, true, true, true); + ('Maquillaje Social', 'Maquillaje para eventos sociales', 45, 450.00, false, false, true), + ('Maquillaje de Novia', 'Maquillaje profesional para novias', 90, 1200.00, false, true, true), + ('Pedicure Básico', 'Pedicure con esmaltado tradicional', 45, 300.00, false, false, true), + ('Pedicure Spa', 'Pedicure con exfoliación y masaje', 60, 450.00, false, false, true), + ('Pedicure Premium', 'Pedicure completo con tratamiento de parafina', 75, 600.00, false, true, true), + ('Uñas Acrilicas', 'Aplicación de uñas acrílicas con diseño', 90, 550.00, false, false, true), + ('Diseño de Uñas', 'Diseño artístico en uñas naturales o acrílicas', 30, 200.00, false, true, true); ``` 2. Deberías ver: `Success, no rows returned` +### 2.4.1 Agregar Nuevos Tiers de Clientes (IMPORTANTE) +**⚠️ Ejecutar ANTES de crear customers** + +1. Nuevo query, copiar y ejecutar: + +```sql +-- Agregar 'black' tier +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum + WHERE enumlabel = 'black' + AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'customer_tier') + ) THEN + ALTER TYPE customer_tier ADD VALUE 'black'; + END IF; +END $$; + +-- Agregar 'VIP' tier +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_enum + WHERE enumlabel = 'VIP' + AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'customer_tier') + ) THEN + ALTER TYPE customer_tier ADD VALUE 'VIP'; + END IF; +END $$; +``` + +2. Deberías ver: `Success, no rows returned` (dos veces) + ### 2.5 Crear Customers 1. Nuevo query, copiar y ejecutar: ```sql INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, notes, total_spent, total_visits, last_visit_date, is_active) VALUES - (uuid_generate_v4(), 'Sofía', 'Ramírez', 'sofia.ramirez@example.com', '+52 55 1111 1111', 'gold', 'Cliente VIP. Prefiere Artists María y Ana.', 15000.00, 25, '2025-12-20', true), - (uuid_generate_v4(), 'Valentina', 'Hernández', 'valentina.hernandez@example.com', '+52 55 2222 2222', 'gold', 'Cliente regular. Prefiere horarios de la mañana.', 8500.00, 15, '2025-12-15', true), - (uuid_generate_v4(), 'Camila', 'López', 'camila.lopez@example.com', '+52 55 3333 3333', 'free', 'Nueva cliente. Referida por Valentina.', 500.00, 1, '2025-12-10', true), - (uuid_generate_v4(), 'Isabella', 'García', 'isabella.garcia@example.com', '+52 55 4444 4444', 'gold', 'Cliente VIP. Requiere servicio de Balayage.', 22000.00, 30, '2025-12-18', true); + (uuid_generate_v4(), 'María José', 'Villarreal', 'mariajose.villarreal@example.com', '+52 844 111 1111', 'gold', 'Cliente VIP. Prefiere maquillaje de novia.', 12000.00, 20, '2026-01-10', true), + (uuid_generate_v4(), 'Ana Paula', 'Garza', 'anapaula.garza@example.com', '+52 844 222 2222', 'gold', 'Cliente regular. Prefiere pedicure premium.', 8500.00, 15, '2026-01-08', true), + (uuid_generate_v4(), 'Lucía', 'Treviño', 'lucia.trevino@example.com', '+52 844 333 3333', 'free', 'Nueva cliente. Referida por Ana Paula.', 450.00, 2, '2026-01-05', true), + (uuid_generate_v4(), 'Gabriela', 'Saldaña', 'gabriela.saldana@example.com', '+52 844 444 4444', 'black', 'Cliente Black. Eventos corporativos.', 25000.00, 35, '2026-01-12', true), + (uuid_generate_v4(), 'Fernanda', 'Cárdenas', 'fernanda.cardenas@example.com', '+52 844 555 5555', 'VIP', 'Cliente VIP. Requiere atención personalizada.', 45000.00, 50, '2026-01-14', true); ``` 2. Deberías ver: `Success, no rows returned` @@ -145,9 +193,10 @@ VALUES 1. Nuevo query, copiar y ejecutar: ```sql -SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com' LIMIT 1)); -SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'valentina.hernandez@example.com' LIMIT 1)); -SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'isabella.garcia@example.com' LIMIT 1)); +SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'mariajose.villarreal@example.com' LIMIT 1)); +SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'anapaula.garza@example.com' LIMIT 1)); +SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'gabriela.saldana@example.com' LIMIT 1)); +SELECT reset_weekly_invitations_for_customer((SELECT id FROM customers WHERE email = 'fernanda.cardenas@example.com' LIMIT 1)); ``` 2. Deberías ver: @@ -178,49 +227,64 @@ INSERT INTO bookings ( notes ) SELECT - (SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist María García' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Balayage Premium' LIMIT 1), + (SELECT id FROM customers WHERE email = 'mariajose.villarreal@example.com' LIMIT 1), + (SELECT id FROM staff WHERE display_name = 'Karla Rodríguez' LIMIT 1), + (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1), + (SELECT id FROM resources WHERE name = 'Estación de Maquillaje' AND location_id = (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1) LIMIT 1), + (SELECT id FROM services WHERE name = 'Maquillaje de Novia' LIMIT 1), NOW() + INTERVAL '1 day', - NOW() + INTERVAL '4 hours', - 'confirmed', - 200.00, - 2000.00, - true, - 'pay_test_001', - 'Balayage Premium para Sofía' -UNION ALL -SELECT - (SELECT id FROM customers WHERE email = 'valentina.hernandez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist Ana Rodríguez' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Color Completo' LIMIT 1), - NOW() + INTERVAL '2 days', - NOW() + INTERVAL '4 hours', + NOW() + INTERVAL '1 day 1 hour 30 minutes', 'confirmed', 200.00, 1200.00, true, - 'pay_test_002', - 'Color Completo para Valentina' + 'pay_test_001', + 'Maquillaje de novia para María José' UNION ALL SELECT - (SELECT id FROM customers WHERE email = 'camila.lopez@example.com' LIMIT 1), - (SELECT id FROM staff WHERE display_name = 'Artist María García' LIMIT 1), - (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), - (SELECT id FROM resources WHERE location_id = (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1) LIMIT 1), - (SELECT id FROM services WHERE name = 'Corte y Estilizado' LIMIT 1), + (SELECT id FROM customers WHERE email = 'anapaula.garza@example.com' LIMIT 1), + (SELECT id FROM staff WHERE display_name = 'Fernanda Martínez' LIMIT 1), + (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1), + (SELECT id FROM resources WHERE name = 'Sillón Pedicure 1' AND location_id = (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1) LIMIT 1), + (SELECT id FROM services WHERE name = 'Pedicure Premium' LIMIT 1), + NOW() + INTERVAL '2 days', + NOW() + INTERVAL '2 days 1 hour 15 minutes', + 'confirmed', + 100.00, + 600.00, + true, + 'pay_test_002', + 'Pedicure Premium para Ana Paula' +UNION ALL +SELECT + (SELECT id FROM customers WHERE email = 'lucia.trevino@example.com' LIMIT 1), + (SELECT id FROM staff WHERE display_name = 'Valeria López' LIMIT 1), + (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1), + (SELECT id FROM resources WHERE name = 'Sillón Pedicure 2' AND location_id = (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1) LIMIT 1), + (SELECT id FROM services WHERE name = 'Pedicure Básico' LIMIT 1), NOW() + INTERVAL '3 days', - NOW() + INTERVAL '1 hour', + NOW() + INTERVAL '3 days 45 minutes', 'confirmed', 50.00, - 500.00, + 300.00, true, 'pay_test_003', - 'Primer corte para Camila'; + 'Primer pedicure para Lucía' +UNION ALL +SELECT + (SELECT id FROM customers WHERE email = 'gabriela.saldana@example.com' LIMIT 1), + (SELECT id FROM staff WHERE display_name = 'Paola Hernández' LIMIT 1), + (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1), + (SELECT id FROM resources WHERE name = 'Estación de Maquillaje' AND location_id = (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1) LIMIT 1), + (SELECT id FROM services WHERE name = 'Maquillaje Social' LIMIT 1), + NOW() + INTERVAL '4 days', + NOW() + INTERVAL '4 days 45 minutes', + 'confirmed', + 0.00, + 450.00, + true, + 'pay_test_004', + 'Maquillaje para evento corporativo'; ``` 2. Deberías ver: `Success, no rows returned` @@ -247,19 +311,21 @@ SELECT 'Bookings: ' || COUNT(*) FROM bookings; 2. Deberías ver: ``` resumen -Locations: 3 -Resources: 6 -Staff: 8 -Services: 6 -Customers: 4 -Invitaciones: 15 -Bookings: 3 +Locations: 2 +Resources: 8 +Staff: 10 +Services: 7 +Customers: 5 +Invitaciones: 20 +Bookings: 4 ``` **Si ves números diferentes:** - Verifica que todos los queries anteriores se ejecutaron correctamente - Revisa los errores (si hubo) +**Nota:** Las invitaciones solo se crean para clientes Gold, Black y VIP (4 clientes × 5 invitaciones = 20) + --- ## 📋 PASO 3: CREAR USUARIOS AUTH (10 min) @@ -277,7 +343,7 @@ Bookings: 3 ### 3.3 Crear Usuario Customer (para probar) 1. Clic en botón "Add user" -2. Email: `sofia.ramirez@example.com` +2. Email: `mariajose.villarreal@example.com` 3. Password: `Customer123!` 4. **Auto Confirm User:** ON (marcar la casilla) 5. Clic en "Create user" @@ -286,7 +352,7 @@ Bookings: 3 ### 3.4 Verificar Usuarios 1. En la página de Auth Users, deberías ver 2 usuarios: - admin@salonos.com - - sofia.ramirez@example.com + - mariajose.villarreal@example.com --- @@ -300,14 +366,14 @@ Bookings: 3 -- REEMPLAZA [USER_ID_DEL_CUSTOMER] con el ID que copiaste del paso 3.3 UPDATE customers SET user_id = '[USER_ID_DEL_CUSTOMER]' -WHERE email = 'sofia.ramirez@example.com'; +WHERE email = 'mariajose.villarreal@example.com'; ``` 3. Ejemplo de cómo debe verse (NO ejecutar esto, es solo ejemplo): ```sql UPDATE customers SET user_id = '01234567-89ab-cdef-0123-456789abcdef' -WHERE email = 'sofia.ramirez@example.com'; +WHERE email = 'mariajose.villarreal@example.com'; ``` ### 4.2 Verificar Actualización @@ -321,7 +387,7 @@ SELECT au.email as auth_user_email FROM customers c LEFT JOIN auth.users au ON c.user_id = au.id -WHERE c.email = 'sofia.ramirez@example.com'; +WHERE c.email = 'mariajose.villarreal@example.com'; ``` 2. Deberías ver `true` en la columna `user_id_set` @@ -373,14 +439,15 @@ LIMIT 1; **Antes de ir a la junta, marca cada paso:** - [ ] PASO 1: Migraciones ejecutadas (verificar mensaje "MIGRATION COMPLETED") -- [ ] PASO 2.1: Locations creadas (3) -- [ ] PASO 2.2: Resources creados (6) -- [ ] PASO 2.3: Staff creado (8) -- [ ] PASO 2.4: Services creados (6) -- [ ] PASO 2.5: Customers creados (4) -- [ ] PASO 2.6: Invitaciones creadas (15) -- [ ] PASO 2.7: Bookings creados (3) -- [ ] PASO 2.8: Verificación correcta (3-6-8-6-4-15-3) +- [ ] PASO 2.1: Locations creadas (2 - Saltillo) +- [ ] PASO 2.2: Resources creados (8 - 4 por salón) +- [ ] PASO 2.3: Staff creado (10 - 5 por salón) +- [ ] PASO 2.4: Services creados (7 - maquillaje y pedicure) +- [ ] PASO 2.4.1: Nuevos tiers agregados (black, VIP) +- [ ] PASO 2.5: Customers creados (5 - varios tiers) +- [ ] PASO 2.6: Invitaciones creadas (20 - 4 clientes premium) +- [ ] PASO 2.7: Bookings creados (4) +- [ ] PASO 2.8: Verificación correcta (2-8-10-7-5-20-4) - [ ] PASO 3.2: Usuario Admin creado - [ ] PASO 3.3: Usuario Customer creado - [ ] PASO 3.4: Verificación Auth Users correcta @@ -421,7 +488,7 @@ Guarda esto en un lugar seguro: - Password: `Admin123!` **Customer (para probar):** -- Email: `sofia.ramirez@example.com` +- Email: `mariajose.villarreal@example.com` - Password: `Customer123!` ---