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!`
---