refactor: migrate from old db/ to supabase CLI structure

- Migrated database schema from db/migrations to supabase/migrations
- Added Supabase CLI configuration (config.toml, .gitignore)
- Added kiosk role and amenities tables for touch kiosks
- Added notification system for artist alerts
- Added seed data with test locations and staff
- Removed old migration scripts and documentation
- Updated tasks_mg.md with current setup

Features:
- 2 locations: ANCHOR:23 - Via KLAVA and TEST
- Kiosk role for touch screen check-in/booking
- Amenities: coffee, cocktails, mocktails for clients
- Notifications when client arrives
- 1 staff + 4 artists + 1 kiosk per location
- Services: hair, nails, makeup, lashes
This commit is contained in:
Marco Gallegos
2026-01-16 00:01:32 -06:00
parent a2054ba403
commit 18071cfb63
34 changed files with 1168 additions and 8626 deletions

118
AGENTS.md
View File

@@ -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.

View File

@@ -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! 🎉

View File

@@ -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)

View File

@@ -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!** 🚀

View File

@@ -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 ""

View File

@@ -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
-- ============================================

View File

@@ -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
-- ============================================

View File

@@ -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
-- ============================================

View File

@@ -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
$$;

View File

@@ -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

View File

@@ -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 '===========================================';

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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?**

View File

@@ -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!** 🚀

View File

@@ -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
<h2>Bienvenida a SalonOS</h2>
<p>Hola {{ .Email }}</p>
<p>Gracias por registrarte en SalonOS. Tu cuenta ha sido creada exitosamente.</p>
<p>Si no creaste esta cuenta, por favor ignora este email.</p>
<p>Saludos,<br>El equipo de SalonOS</p>
```
### 8.2 Reset Password Template
1. Haz clic en **"Reset password"**
2. Personaliza el template:
```html
<h2>Restablecer Contraseña - SalonOS</h2>
<p>Hola {{ .Email }}</p>
<p>Hemos recibido una solicitud para restablecer tu contraseña en SalonOS.</p>
<p><a href="{{ .ConfirmationURL }}">Haz clic aquí para restablecer tu contraseña</a></p>
<p>Este enlace expirará en 24 horas.</p>
<p>Si no solicitaste restablecer tu contraseña, por favor ignora este email.</p>
<p>Saludos,<br>El equipo de SalonOS</p>
```
---
## ✅ 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?**

View File

@@ -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?**

View File

@@ -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.

View File

@@ -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?**

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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
$$;

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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;

8
supabase/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
# Supabase
.branches
.temp
# dotenvx
.env.keys
.env.local
.env.*.local

384
supabase/config.toml Normal file
View File

@@ -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:<port>/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://<database>/<schema>/<hook_name>"
# 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. <bucket_name>.s3-<region>.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)"

View File

@@ -12,15 +12,30 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ENUMS -- ENUMS
CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer'); DO $$
CREATE TYPE customer_tier AS ENUM ('free', 'gold'); BEGIN
CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'cancelled', 'completed', 'no_show'); IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN
CREATE TYPE invitation_status AS ENUM ('pending', 'used', 'expired'); CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer');
CREATE TYPE resource_type AS ENUM ('station', 'room', 'equipment'); END IF;
CREATE TYPE audit_action AS ENUM ('create', 'update', 'delete', 'reset_invitations', 'payment', 'status_change'); 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 -- LOCATIONS
CREATE TABLE locations ( CREATE TABLE IF NOT EXISTS locations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
timezone VARCHAR(50) NOT NULL DEFAULT 'UTC', timezone VARCHAR(50) NOT NULL DEFAULT 'UTC',
@@ -32,7 +47,7 @@ CREATE TABLE locations (
); );
-- RESOURCES -- RESOURCES
CREATE TABLE resources ( CREATE TABLE IF NOT EXISTS resources (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE, location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
@@ -44,7 +59,7 @@ CREATE TABLE resources (
); );
-- STAFF -- STAFF
CREATE TABLE staff ( CREATE TABLE IF NOT EXISTS staff (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL, user_id UUID NOT NULL,
location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE, location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
@@ -58,7 +73,7 @@ CREATE TABLE staff (
); );
-- SERVICES -- SERVICES
CREATE TABLE services ( CREATE TABLE IF NOT EXISTS services (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
description TEXT, description TEXT,
@@ -72,7 +87,7 @@ CREATE TABLE services (
); );
-- CUSTOMERS -- CUSTOMERS
CREATE TABLE customers ( CREATE TABLE IF NOT EXISTS customers (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID UNIQUE, user_id UUID UNIQUE,
first_name VARCHAR(100) NOT NULL, first_name VARCHAR(100) NOT NULL,
@@ -90,7 +105,7 @@ CREATE TABLE customers (
); );
-- INVITATIONS -- INVITATIONS
CREATE TABLE invitations ( CREATE TABLE IF NOT EXISTS invitations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
inviter_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, inviter_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
code VARCHAR(10) UNIQUE NOT NULL, code VARCHAR(10) UNIQUE NOT NULL,
@@ -104,7 +119,7 @@ CREATE TABLE invitations (
); );
-- BOOKINGS -- BOOKINGS
CREATE TABLE bookings ( CREATE TABLE IF NOT EXISTS bookings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
short_id VARCHAR(6) UNIQUE NOT NULL, short_id VARCHAR(6) UNIQUE NOT NULL,
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
@@ -126,7 +141,7 @@ CREATE TABLE bookings (
); );
-- AUDIT LOGS -- AUDIT LOGS
CREATE TABLE audit_logs ( CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
entity_type VARCHAR(50) NOT NULL, entity_type VARCHAR(50) NOT NULL,
entity_id UUID NOT NULL, entity_id UUID NOT NULL,
@@ -142,30 +157,30 @@ CREATE TABLE audit_logs (
); );
-- INDEXES -- INDEXES
CREATE INDEX idx_locations_active ON locations(is_active); CREATE INDEX IF NOT EXISTS idx_locations_active ON locations(is_active);
CREATE INDEX idx_resources_location ON resources(location_id); CREATE INDEX IF NOT EXISTS idx_resources_location ON resources(location_id);
CREATE INDEX idx_resources_active ON resources(location_id, is_active); CREATE INDEX IF NOT EXISTS idx_resources_active ON resources(location_id, is_active);
CREATE INDEX idx_staff_user ON staff(user_id); CREATE INDEX IF NOT EXISTS idx_staff_user ON staff(user_id);
CREATE INDEX idx_staff_location ON staff(location_id); CREATE INDEX IF NOT EXISTS idx_staff_location ON staff(location_id);
CREATE INDEX idx_staff_role ON staff(location_id, role, is_active); CREATE INDEX IF NOT EXISTS idx_staff_role ON staff(location_id, role, is_active);
CREATE INDEX idx_services_active ON services(is_active); CREATE INDEX IF NOT EXISTS idx_services_active ON services(is_active);
CREATE INDEX idx_customers_tier ON customers(tier); CREATE INDEX IF NOT EXISTS idx_customers_tier ON customers(tier);
CREATE INDEX idx_customers_email ON customers(email); CREATE INDEX IF NOT EXISTS idx_customers_email ON customers(email);
CREATE INDEX idx_customers_active ON customers(is_active); CREATE INDEX IF NOT EXISTS idx_customers_active ON customers(is_active);
CREATE INDEX idx_invitations_inviter ON invitations(inviter_id); CREATE INDEX IF NOT EXISTS idx_invitations_inviter ON invitations(inviter_id);
CREATE INDEX idx_invitations_code ON invitations(code); CREATE INDEX IF NOT EXISTS idx_invitations_code ON invitations(code);
CREATE INDEX idx_invitations_week ON invitations(week_start_date, status); CREATE INDEX IF NOT EXISTS idx_invitations_week ON invitations(week_start_date, status);
CREATE INDEX idx_bookings_customer ON bookings(customer_id); CREATE INDEX IF NOT EXISTS idx_bookings_customer ON bookings(customer_id);
CREATE INDEX idx_bookings_staff ON bookings(staff_id); CREATE INDEX IF NOT EXISTS idx_bookings_staff ON bookings(staff_id);
CREATE INDEX idx_bookings_secondary_artist ON bookings(secondary_artist_id); CREATE INDEX IF NOT EXISTS idx_bookings_secondary_artist ON bookings(secondary_artist_id);
CREATE INDEX idx_bookings_location ON bookings(location_id); CREATE INDEX IF NOT EXISTS idx_bookings_location ON bookings(location_id);
CREATE INDEX idx_bookings_resource ON bookings(resource_id); CREATE INDEX IF NOT EXISTS idx_bookings_resource ON bookings(resource_id);
CREATE INDEX idx_bookings_time ON bookings(start_time_utc, end_time_utc); CREATE INDEX IF NOT EXISTS idx_bookings_time ON bookings(start_time_utc, end_time_utc);
CREATE INDEX idx_bookings_status ON bookings(status); CREATE INDEX IF NOT EXISTS idx_bookings_status ON bookings(status);
CREATE INDEX idx_bookings_short_id ON bookings(short_id); CREATE INDEX IF NOT EXISTS idx_bookings_short_id ON bookings(short_id);
CREATE INDEX idx_audit_entity ON audit_logs(entity_type, entity_id); CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_logs(entity_type, entity_id);
CREATE INDEX idx_audit_action ON audit_logs(action, created_at); CREATE INDEX IF NOT EXISTS 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_audit_performed ON audit_logs(performed_by);
-- UPDATED_AT TRIGGER FUNCTION -- UPDATED_AT TRIGGER FUNCTION
CREATE OR REPLACE FUNCTION update_updated_at() CREATE OR REPLACE FUNCTION update_updated_at()
@@ -177,32 +192,39 @@ END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
-- UPDATED_AT TRIGGERS -- UPDATED_AT TRIGGERS
DROP TRIGGER IF EXISTS locations_updated_at ON locations;
CREATE TRIGGER locations_updated_at BEFORE UPDATE ON locations CREATE TRIGGER locations_updated_at BEFORE UPDATE ON locations
FOR EACH ROW EXECUTE FUNCTION update_updated_at(); 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 CREATE TRIGGER resources_updated_at BEFORE UPDATE ON resources
FOR EACH ROW EXECUTE FUNCTION update_updated_at(); 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 CREATE TRIGGER staff_updated_at BEFORE UPDATE ON staff
FOR EACH ROW EXECUTE FUNCTION update_updated_at(); 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 CREATE TRIGGER services_updated_at BEFORE UPDATE ON services
FOR EACH ROW EXECUTE FUNCTION update_updated_at(); 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 CREATE TRIGGER customers_updated_at BEFORE UPDATE ON customers
FOR EACH ROW EXECUTE FUNCTION update_updated_at(); 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 CREATE TRIGGER invitations_updated_at BEFORE UPDATE ON invitations
FOR EACH ROW EXECUTE FUNCTION update_updated_at(); 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 CREATE TRIGGER bookings_updated_at BEFORE UPDATE ON bookings
FOR EACH ROW EXECUTE FUNCTION update_updated_at(); FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- CONSTRAINTS (Simple ones only - no subqueries) -- 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); 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); CHECK (EXTRACT(ISODOW FROM week_start_date) = 1);
-- Trigger for secondary_artist validation (instead of CHECK constraint with subquery) -- Trigger for secondary_artist validation (instead of CHECK constraint with subquery)
@@ -221,6 +243,7 @@ BEGIN
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS validate_booking_secondary_artist ON bookings;
CREATE TRIGGER validate_booking_secondary_artist BEFORE INSERT OR UPDATE ON bookings CREATE TRIGGER validate_booking_secondary_artist BEFORE INSERT OR UPDATE ON bookings
FOR EACH ROW EXECUTE FUNCTION validate_secondary_artist_role(); 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; ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY;
-- LOCATIONS POLICIES -- LOCATIONS POLICIES
DROP POLICY IF EXISTS "locations_select_staff_higher" ON locations;
CREATE POLICY "locations_select_staff_higher" ON locations CREATE POLICY "locations_select_staff_higher" ON locations
FOR SELECT FOR SELECT
USING (is_staff_or_higher() OR is_admin()); 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 CREATE POLICY "locations_modify_admin_manager" ON locations
FOR ALL FOR ALL
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
-- RESOURCES POLICIES -- RESOURCES POLICIES
DROP POLICY IF EXISTS "resources_select_staff_higher" ON resources;
CREATE POLICY "resources_select_staff_higher" ON resources CREATE POLICY "resources_select_staff_higher" ON resources
FOR SELECT FOR SELECT
USING (is_staff_or_higher() OR is_admin()); USING (is_staff_or_higher() OR is_admin());
DROP POLICY IF EXISTS "resources_select_artist" ON resources;
CREATE POLICY "resources_select_artist" ON resources CREATE POLICY "resources_select_artist" ON resources
FOR SELECT FOR SELECT
USING (is_artist()); USING (is_artist());
DROP POLICY IF EXISTS "resources_modify_admin_manager" ON resources;
CREATE POLICY "resources_modify_admin_manager" ON resources CREATE POLICY "resources_modify_admin_manager" ON resources
FOR ALL FOR ALL
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
-- STAFF POLICIES -- STAFF POLICIES
DROP POLICY IF EXISTS "staff_select_admin_manager" ON staff;
CREATE POLICY "staff_select_admin_manager" ON staff CREATE POLICY "staff_select_admin_manager" ON staff
FOR SELECT FOR SELECT
USING (get_current_user_role() IN ('admin', 'manager')); 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 CREATE POLICY "staff_select_same_location" ON staff
FOR SELECT FOR SELECT
USING ( 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 CREATE POLICY "staff_select_artist_view_artists" ON staff
FOR SELECT FOR SELECT
USING ( USING (
@@ -344,74 +375,91 @@ CREATE POLICY "staff_select_artist_view_artists" ON staff
staff.role = 'artist' staff.role = 'artist'
); );
DROP POLICY IF EXISTS "staff_modify_admin_manager" ON staff;
CREATE POLICY "staff_modify_admin_manager" ON staff CREATE POLICY "staff_modify_admin_manager" ON staff
FOR ALL FOR ALL
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
-- SERVICES POLICIES -- SERVICES POLICIES
DROP POLICY IF EXISTS "services_select_all" ON services;
CREATE POLICY "services_select_all" ON services CREATE POLICY "services_select_all" ON services
FOR SELECT FOR SELECT
USING (is_active = true); USING (is_active = true);
DROP POLICY IF EXISTS "services_all_admin_manager" ON services;
CREATE POLICY "services_all_admin_manager" ON services CREATE POLICY "services_all_admin_manager" ON services
FOR ALL FOR ALL
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
-- CUSTOMERS POLICIES (RESTRICTED FOR ARTISTS) -- CUSTOMERS POLICIES (RESTRICTED FOR ARTISTS)
DROP POLICY IF EXISTS "customers_select_admin_manager" ON customers;
CREATE POLICY "customers_select_admin_manager" ON customers CREATE POLICY "customers_select_admin_manager" ON customers
FOR SELECT FOR SELECT
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
DROP POLICY IF EXISTS "customers_select_staff" ON customers;
CREATE POLICY "customers_select_staff" ON customers CREATE POLICY "customers_select_staff" ON customers
FOR SELECT FOR SELECT
USING (is_staff_or_higher()); USING (is_staff_or_higher());
DROP POLICY IF EXISTS "customers_select_artist_restricted" ON customers;
CREATE POLICY "customers_select_artist_restricted" ON customers CREATE POLICY "customers_select_artist_restricted" ON customers
FOR SELECT FOR SELECT
USING (is_artist()); USING (is_artist());
DROP POLICY IF EXISTS "customers_select_own" ON customers;
CREATE POLICY "customers_select_own" ON customers CREATE POLICY "customers_select_own" ON customers
FOR SELECT FOR SELECT
USING (is_customer() AND user_id = auth.uid()); 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 CREATE POLICY "customers_modify_admin_manager" ON customers
FOR ALL FOR ALL
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
DROP POLICY IF EXISTS "customers_modify_staff" ON customers;
CREATE POLICY "customers_modify_staff" ON customers CREATE POLICY "customers_modify_staff" ON customers
FOR ALL FOR ALL
USING (is_staff_or_higher()); USING (is_staff_or_higher());
DROP POLICY IF EXISTS "customers_update_own" ON customers;
CREATE POLICY "customers_update_own" ON customers CREATE POLICY "customers_update_own" ON customers
FOR UPDATE FOR UPDATE
USING (is_customer() AND user_id = auth.uid()); USING (is_customer() AND user_id = auth.uid());
-- INVITATIONS POLICIES -- INVITATIONS POLICIES
DROP POLICY IF EXISTS "invitations_select_admin_manager" ON invitations;
CREATE POLICY "invitations_select_admin_manager" ON invitations CREATE POLICY "invitations_select_admin_manager" ON invitations
FOR SELECT FOR SELECT
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
DROP POLICY IF EXISTS "invitations_select_staff" ON invitations;
CREATE POLICY "invitations_select_staff" ON invitations CREATE POLICY "invitations_select_staff" ON invitations
FOR SELECT FOR SELECT
USING (is_staff_or_higher()); USING (is_staff_or_higher());
DROP POLICY IF EXISTS "invitations_select_own" ON invitations;
CREATE POLICY "invitations_select_own" ON invitations CREATE POLICY "invitations_select_own" ON invitations
FOR SELECT FOR SELECT
USING (is_customer() AND inviter_id = (SELECT id FROM customers WHERE user_id = auth.uid())); 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 CREATE POLICY "invitations_modify_admin_manager" ON invitations
FOR ALL FOR ALL
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
DROP POLICY IF EXISTS "invitations_modify_staff" ON invitations;
CREATE POLICY "invitations_modify_staff" ON invitations CREATE POLICY "invitations_modify_staff" ON invitations
FOR ALL FOR ALL
USING (is_staff_or_higher()); USING (is_staff_or_higher());
-- BOOKINGS POLICIES -- BOOKINGS POLICIES
DROP POLICY IF EXISTS "bookings_select_admin_manager" ON bookings;
CREATE POLICY "bookings_select_admin_manager" ON bookings CREATE POLICY "bookings_select_admin_manager" ON bookings
FOR SELECT FOR SELECT
USING (get_current_user_role() IN ('admin', 'manager')); 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 CREATE POLICY "bookings_select_staff_location" ON bookings
FOR SELECT FOR SELECT
USING ( 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 CREATE POLICY "bookings_select_artist_own" ON bookings
FOR SELECT FOR SELECT
USING ( 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())) 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 CREATE POLICY "bookings_select_own" ON bookings
FOR SELECT FOR SELECT
USING (is_customer() AND customer_id = (SELECT id FROM customers WHERE user_id = auth.uid())); 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 CREATE POLICY "bookings_modify_admin_manager" ON bookings
FOR ALL FOR ALL
USING (get_current_user_role() IN ('admin', 'manager')); 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 CREATE POLICY "bookings_modify_staff_location" ON bookings
FOR ALL FOR ALL
USING ( 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 CREATE POLICY "bookings_no_modify_artist" ON bookings
FOR ALL FOR ALL
USING (NOT is_artist()); USING (NOT is_artist());
DROP POLICY IF EXISTS "bookings_create_own" ON bookings;
CREATE POLICY "bookings_create_own" ON bookings CREATE POLICY "bookings_create_own" ON bookings
FOR INSERT FOR INSERT
WITH CHECK ( WITH CHECK (
@@ -457,6 +511,7 @@ CREATE POLICY "bookings_create_own" ON bookings
customer_id = (SELECT id FROM customers WHERE user_id = auth.uid()) 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 CREATE POLICY "bookings_update_own" ON bookings
FOR UPDATE FOR UPDATE
USING ( USING (
@@ -465,10 +520,12 @@ CREATE POLICY "bookings_update_own" ON bookings
); );
-- AUDIT LOGS POLICIES -- 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 CREATE POLICY "audit_logs_select_admin_manager" ON audit_logs
FOR SELECT FOR SELECT
USING (get_current_user_role() IN ('admin', 'manager')); 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 CREATE POLICY "audit_logs_select_staff_location" ON audit_logs
FOR SELECT FOR SELECT
USING ( 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 CREATE POLICY "audit_logs_no_insert" ON audit_logs
FOR INSERT FOR INSERT
WITH CHECK (false); WITH CHECK (false);
@@ -736,18 +794,23 @@ END;
$$ LANGUAGE plpgsql SECURITY DEFINER; $$ LANGUAGE plpgsql SECURITY DEFINER;
-- APPLY AUDIT LOG TRIGGERS -- APPLY AUDIT LOG TRIGGERS
DROP TRIGGER IF EXISTS audit_bookings ON bookings;
CREATE TRIGGER audit_bookings AFTER INSERT OR UPDATE OR DELETE ON bookings CREATE TRIGGER audit_bookings AFTER INSERT OR UPDATE OR DELETE ON bookings
FOR EACH ROW EXECUTE FUNCTION log_audit(); 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 CREATE TRIGGER audit_customers AFTER INSERT OR UPDATE OR DELETE ON customers
FOR EACH ROW EXECUTE FUNCTION log_audit(); 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 CREATE TRIGGER audit_invitations AFTER INSERT OR UPDATE OR DELETE ON invitations
FOR EACH ROW EXECUTE FUNCTION log_audit(); 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 CREATE TRIGGER audit_staff AFTER INSERT OR UPDATE OR DELETE ON staff
FOR EACH ROW EXECUTE FUNCTION log_audit(); 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 CREATE TRIGGER audit_services AFTER INSERT OR UPDATE OR DELETE ON services
FOR EACH ROW EXECUTE FUNCTION log_audit(); FOR EACH ROW EXECUTE FUNCTION log_audit();
@@ -762,6 +825,7 @@ BEGIN
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS booking_generate_short_id ON bookings;
CREATE TRIGGER booking_generate_short_id BEFORE INSERT ON bookings CREATE TRIGGER booking_generate_short_id BEFORE INSERT ON bookings
FOR EACH ROW EXECUTE FUNCTION generate_booking_short_id(); FOR EACH ROW EXECUTE FUNCTION generate_booking_short_id();
@@ -774,22 +838,11 @@ BEGIN
RAISE NOTICE '==========================================='; RAISE NOTICE '===========================================';
RAISE NOTICE 'SALONOS - DATABASE MIGRATION COMPLETED'; RAISE NOTICE 'SALONOS - DATABASE MIGRATION COMPLETED';
RAISE NOTICE '==========================================='; RAISE NOTICE '===========================================';
RAISE NOTICE 'Tables created: 8'; RAISE NOTICE 'Tables created: 8';
RAISE NOTICE 'Functions created: 14'; RAISE NOTICE 'Functions created: 14';
RAISE NOTICE 'Triggers active: 17+'; RAISE NOTICE 'Triggers active: 17+';
RAISE NOTICE 'RLS policies configured: 20+'; RAISE NOTICE 'RLS policies configured: 20+';
RAISE NOTICE 'ENUM types created: 6'; 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 '==========================================='; RAISE NOTICE '===========================================';
END END
$$; $$;

View File

@@ -8,20 +8,34 @@
-- BEGIN MIGRATION 001: INITIAL SCHEMA -- BEGIN MIGRATION 001: INITIAL SCHEMA
-- ============================================ -- ============================================
-- Habilitar UUID extension -- UUID extension not needed in Supabase (uses gen_random_uuid)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ENUMS -- ENUMS
CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer'); DO $$
CREATE TYPE customer_tier AS ENUM ('free', 'gold'); BEGIN
CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'cancelled', 'completed', 'no_show'); IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN
CREATE TYPE invitation_status AS ENUM ('pending', 'used', 'expired'); CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer');
CREATE TYPE resource_type AS ENUM ('station', 'room', 'equipment'); END IF;
CREATE TYPE audit_action AS ENUM ('create', 'update', 'delete', 'reset_invitations', 'payment', 'status_change'); 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 -- LOCATIONS
CREATE TABLE locations ( CREATE TABLE IF NOT EXISTS locations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
timezone VARCHAR(50) NOT NULL DEFAULT 'UTC', timezone VARCHAR(50) NOT NULL DEFAULT 'UTC',
address TEXT, address TEXT,
@@ -32,8 +46,8 @@ CREATE TABLE locations (
); );
-- RESOURCES -- RESOURCES
CREATE TABLE resources ( CREATE TABLE IF NOT EXISTS resources (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE, location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
type resource_type NOT NULL, type resource_type NOT NULL,
@@ -44,8 +58,8 @@ CREATE TABLE resources (
); );
-- STAFF -- STAFF
CREATE TABLE staff ( CREATE TABLE IF NOT EXISTS staff (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL, user_id UUID NOT NULL,
location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE, location_id UUID NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
role user_role NOT NULL CHECK (role IN ('admin', 'manager', 'staff', 'artist')), role user_role NOT NULL CHECK (role IN ('admin', 'manager', 'staff', 'artist')),
@@ -58,8 +72,8 @@ CREATE TABLE staff (
); );
-- SERVICES -- SERVICES
CREATE TABLE services ( CREATE TABLE IF NOT EXISTS services (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
description TEXT, description TEXT,
duration_minutes INTEGER NOT NULL CHECK (duration_minutes > 0), duration_minutes INTEGER NOT NULL CHECK (duration_minutes > 0),
@@ -72,8 +86,8 @@ CREATE TABLE services (
); );
-- CUSTOMERS -- CUSTOMERS
CREATE TABLE customers ( CREATE TABLE IF NOT EXISTS customers (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID UNIQUE, user_id UUID UNIQUE,
first_name VARCHAR(100) NOT NULL, first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL,
@@ -90,8 +104,8 @@ CREATE TABLE customers (
); );
-- INVITATIONS -- INVITATIONS
CREATE TABLE invitations ( CREATE TABLE IF NOT EXISTS invitations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
inviter_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, inviter_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
code VARCHAR(10) UNIQUE NOT NULL, code VARCHAR(10) UNIQUE NOT NULL,
email VARCHAR(255), email VARCHAR(255),
@@ -104,8 +118,8 @@ CREATE TABLE invitations (
); );
-- BOOKINGS -- BOOKINGS
CREATE TABLE bookings ( CREATE TABLE IF NOT EXISTS bookings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
short_id VARCHAR(6) UNIQUE NOT NULL, short_id VARCHAR(6) UNIQUE NOT NULL,
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT, staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT,
@@ -126,8 +140,8 @@ CREATE TABLE bookings (
); );
-- AUDIT LOGS -- AUDIT LOGS
CREATE TABLE audit_logs ( CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entity_type VARCHAR(50) NOT NULL, entity_type VARCHAR(50) NOT NULL,
entity_id UUID NOT NULL, entity_id UUID NOT NULL,
action audit_action NOT NULL, action audit_action NOT NULL,
@@ -142,30 +156,30 @@ CREATE TABLE audit_logs (
); );
-- INDEXES -- INDEXES
CREATE INDEX idx_locations_active ON locations(is_active); CREATE INDEX IF NOT EXISTS idx_locations_active ON locations(is_active);
CREATE INDEX idx_resources_location ON resources(location_id); CREATE INDEX IF NOT EXISTS idx_resources_location ON resources(location_id);
CREATE INDEX idx_resources_active ON resources(location_id, is_active); CREATE INDEX IF NOT EXISTS idx_resources_active ON resources(location_id, is_active);
CREATE INDEX idx_staff_user ON staff(user_id); CREATE INDEX IF NOT EXISTS idx_staff_user ON staff(user_id);
CREATE INDEX idx_staff_location ON staff(location_id); CREATE INDEX IF NOT EXISTS idx_staff_location ON staff(location_id);
CREATE INDEX idx_staff_role ON staff(location_id, role, is_active); CREATE INDEX IF NOT EXISTS idx_staff_role ON staff(location_id, role, is_active);
CREATE INDEX idx_services_active ON services(is_active); CREATE INDEX IF NOT EXISTS idx_services_active ON services(is_active);
CREATE INDEX idx_customers_tier ON customers(tier); CREATE INDEX IF NOT EXISTS idx_customers_tier ON customers(tier);
CREATE INDEX idx_customers_email ON customers(email); CREATE INDEX IF NOT EXISTS idx_customers_email ON customers(email);
CREATE INDEX idx_customers_active ON customers(is_active); CREATE INDEX IF NOT EXISTS idx_customers_active ON customers(is_active);
CREATE INDEX idx_invitations_inviter ON invitations(inviter_id); CREATE INDEX IF NOT EXISTS idx_invitations_inviter ON invitations(inviter_id);
CREATE INDEX idx_invitations_code ON invitations(code); CREATE INDEX IF NOT EXISTS idx_invitations_code ON invitations(code);
CREATE INDEX idx_invitations_week ON invitations(week_start_date, status); CREATE INDEX IF NOT EXISTS idx_invitations_week ON invitations(week_start_date, status);
CREATE INDEX idx_bookings_customer ON bookings(customer_id); CREATE INDEX IF NOT EXISTS idx_bookings_customer ON bookings(customer_id);
CREATE INDEX idx_bookings_staff ON bookings(staff_id); CREATE INDEX IF NOT EXISTS idx_bookings_staff ON bookings(staff_id);
CREATE INDEX idx_bookings_secondary_artist ON bookings(secondary_artist_id); CREATE INDEX IF NOT EXISTS idx_bookings_secondary_artist ON bookings(secondary_artist_id);
CREATE INDEX idx_bookings_location ON bookings(location_id); CREATE INDEX IF NOT EXISTS idx_bookings_location ON bookings(location_id);
CREATE INDEX idx_bookings_resource ON bookings(resource_id); CREATE INDEX IF NOT EXISTS idx_bookings_resource ON bookings(resource_id);
CREATE INDEX idx_bookings_time ON bookings(start_time_utc, end_time_utc); CREATE INDEX IF NOT EXISTS idx_bookings_time ON bookings(start_time_utc, end_time_utc);
CREATE INDEX idx_bookings_status ON bookings(status); CREATE INDEX IF NOT EXISTS idx_bookings_status ON bookings(status);
CREATE INDEX idx_bookings_short_id ON bookings(short_id); CREATE INDEX IF NOT EXISTS idx_bookings_short_id ON bookings(short_id);
CREATE INDEX idx_audit_entity ON audit_logs(entity_type, entity_id); CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_logs(entity_type, entity_id);
CREATE INDEX idx_audit_action ON audit_logs(action, created_at); CREATE INDEX IF NOT EXISTS 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_audit_performed ON audit_logs(performed_by);
-- UPDATED_AT TRIGGER FUNCTION -- UPDATED_AT TRIGGER FUNCTION
CREATE OR REPLACE FUNCTION update_updated_at() CREATE OR REPLACE FUNCTION update_updated_at()
@@ -177,31 +191,40 @@ END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
-- UPDATED_AT TRIGGERS -- UPDATED_AT TRIGGERS
DROP TRIGGER IF EXISTS locations_updated_at ON locations;
CREATE TRIGGER locations_updated_at BEFORE UPDATE ON locations CREATE TRIGGER locations_updated_at BEFORE UPDATE ON locations
FOR EACH ROW EXECUTE FUNCTION update_updated_at(); 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 CREATE TRIGGER resources_updated_at BEFORE UPDATE ON resources
FOR EACH ROW EXECUTE FUNCTION update_updated_at(); 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 CREATE TRIGGER staff_updated_at BEFORE UPDATE ON staff
FOR EACH ROW EXECUTE FUNCTION update_updated_at(); 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 CREATE TRIGGER services_updated_at BEFORE UPDATE ON services
FOR EACH ROW EXECUTE FUNCTION update_updated_at(); 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 CREATE TRIGGER customers_updated_at BEFORE UPDATE ON customers
FOR EACH ROW EXECUTE FUNCTION update_updated_at(); 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 CREATE TRIGGER invitations_updated_at BEFORE UPDATE ON invitations
FOR EACH ROW EXECUTE FUNCTION update_updated_at(); 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 CREATE TRIGGER bookings_updated_at BEFORE UPDATE ON bookings
FOR EACH ROW EXECUTE FUNCTION update_updated_at(); FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- CONSTRAINTS (Simple ones only - no subqueries) -- CONSTRAINTS (Simple ones only - no subqueries)
ALTER TABLE bookings DROP CONSTRAINT IF EXISTS check_booking_time;
ALTER TABLE bookings ADD CONSTRAINT check_booking_time ALTER TABLE bookings ADD CONSTRAINT check_booking_time
CHECK (end_time_utc > start_time_utc); 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 ALTER TABLE invitations ADD CONSTRAINT check_week_start_is_monday
CHECK (EXTRACT(ISODOW FROM week_start_date) = 1); CHECK (EXTRACT(ISODOW FROM week_start_date) = 1);
@@ -221,6 +244,7 @@ BEGIN
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS validate_booking_secondary_artist ON bookings;
CREATE TRIGGER validate_booking_secondary_artist BEFORE INSERT OR UPDATE ON bookings CREATE TRIGGER validate_booking_secondary_artist BEFORE INSERT OR UPDATE ON bookings
FOR EACH ROW EXECUTE FUNCTION validate_secondary_artist_role(); 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; ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY;
-- LOCATIONS POLICIES -- LOCATIONS POLICIES
DROP POLICY IF EXISTS "locations_select_staff_higher" ON locations;
CREATE POLICY "locations_select_staff_higher" ON locations CREATE POLICY "locations_select_staff_higher" ON locations
FOR SELECT FOR SELECT
USING (is_staff_or_higher() OR is_admin()); 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 CREATE POLICY "locations_modify_admin_manager" ON locations
FOR ALL FOR ALL
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
-- RESOURCES POLICIES -- RESOURCES POLICIES
DROP POLICY IF EXISTS "resources_select_staff_higher" ON resources;
CREATE POLICY "resources_select_staff_higher" ON resources CREATE POLICY "resources_select_staff_higher" ON resources
FOR SELECT FOR SELECT
USING (is_staff_or_higher() OR is_admin()); USING (is_staff_or_higher() OR is_admin());
DROP POLICY IF EXISTS "resources_select_artist" ON resources;
CREATE POLICY "resources_select_artist" ON resources CREATE POLICY "resources_select_artist" ON resources
FOR SELECT FOR SELECT
USING (is_artist()); USING (is_artist());
DROP POLICY IF EXISTS "resources_modify_admin_manager" ON resources;
CREATE POLICY "resources_modify_admin_manager" ON resources CREATE POLICY "resources_modify_admin_manager" ON resources
FOR ALL FOR ALL
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
-- STAFF POLICIES -- STAFF POLICIES
DROP POLICY IF EXISTS "staff_select_admin_manager" ON staff;
CREATE POLICY "staff_select_admin_manager" ON staff CREATE POLICY "staff_select_admin_manager" ON staff
FOR SELECT FOR SELECT
USING (get_current_user_role() IN ('admin', 'manager')); 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 CREATE POLICY "staff_select_same_location" ON staff
FOR SELECT FOR SELECT
USING ( 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 CREATE POLICY "staff_select_artist_view_artists" ON staff
FOR SELECT FOR SELECT
USING ( USING (
@@ -344,74 +376,91 @@ CREATE POLICY "staff_select_artist_view_artists" ON staff
staff.role = 'artist' staff.role = 'artist'
); );
DROP POLICY IF EXISTS "staff_modify_admin_manager" ON staff;
CREATE POLICY "staff_modify_admin_manager" ON staff CREATE POLICY "staff_modify_admin_manager" ON staff
FOR ALL FOR ALL
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
-- SERVICES POLICIES -- SERVICES POLICIES
DROP POLICY IF EXISTS "services_select_all" ON services;
CREATE POLICY "services_select_all" ON services CREATE POLICY "services_select_all" ON services
FOR SELECT FOR SELECT
USING (is_active = true); USING (is_active = true);
DROP POLICY IF EXISTS "services_all_admin_manager" ON services;
CREATE POLICY "services_all_admin_manager" ON services CREATE POLICY "services_all_admin_manager" ON services
FOR ALL FOR ALL
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
-- CUSTOMERS POLICIES (RESTRICTED FOR ARTISTS) -- CUSTOMERS POLICIES (RESTRICTED FOR ARTISTS)
DROP POLICY IF EXISTS "customers_select_admin_manager" ON customers;
CREATE POLICY "customers_select_admin_manager" ON customers CREATE POLICY "customers_select_admin_manager" ON customers
FOR SELECT FOR SELECT
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
DROP POLICY IF EXISTS "customers_select_staff" ON customers;
CREATE POLICY "customers_select_staff" ON customers CREATE POLICY "customers_select_staff" ON customers
FOR SELECT FOR SELECT
USING (is_staff_or_higher()); USING (is_staff_or_higher());
DROP POLICY IF EXISTS "customers_select_artist_restricted" ON customers;
CREATE POLICY "customers_select_artist_restricted" ON customers CREATE POLICY "customers_select_artist_restricted" ON customers
FOR SELECT FOR SELECT
USING (is_artist()); USING (is_artist());
DROP POLICY IF EXISTS "customers_select_own" ON customers;
CREATE POLICY "customers_select_own" ON customers CREATE POLICY "customers_select_own" ON customers
FOR SELECT FOR SELECT
USING (is_customer() AND user_id = auth.uid()); 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 CREATE POLICY "customers_modify_admin_manager" ON customers
FOR ALL FOR ALL
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
DROP POLICY IF EXISTS "customers_modify_staff" ON customers;
CREATE POLICY "customers_modify_staff" ON customers CREATE POLICY "customers_modify_staff" ON customers
FOR ALL FOR ALL
USING (is_staff_or_higher()); USING (is_staff_or_higher());
DROP POLICY IF EXISTS "customers_update_own" ON customers;
CREATE POLICY "customers_update_own" ON customers CREATE POLICY "customers_update_own" ON customers
FOR UPDATE FOR UPDATE
USING (is_customer() AND user_id = auth.uid()); USING (is_customer() AND user_id = auth.uid());
-- INVITATIONS POLICIES -- INVITATIONS POLICIES
DROP POLICY IF EXISTS "invitations_select_admin_manager" ON invitations;
CREATE POLICY "invitations_select_admin_manager" ON invitations CREATE POLICY "invitations_select_admin_manager" ON invitations
FOR SELECT FOR SELECT
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
DROP POLICY IF EXISTS "invitations_select_staff" ON invitations;
CREATE POLICY "invitations_select_staff" ON invitations CREATE POLICY "invitations_select_staff" ON invitations
FOR SELECT FOR SELECT
USING (is_staff_or_higher()); USING (is_staff_or_higher());
DROP POLICY IF EXISTS "invitations_select_own" ON invitations;
CREATE POLICY "invitations_select_own" ON invitations CREATE POLICY "invitations_select_own" ON invitations
FOR SELECT FOR SELECT
USING (is_customer() AND inviter_id = (SELECT id FROM customers WHERE user_id = auth.uid())); 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 CREATE POLICY "invitations_modify_admin_manager" ON invitations
FOR ALL FOR ALL
USING (get_current_user_role() IN ('admin', 'manager')); USING (get_current_user_role() IN ('admin', 'manager'));
DROP POLICY IF EXISTS "invitations_modify_staff" ON invitations;
CREATE POLICY "invitations_modify_staff" ON invitations CREATE POLICY "invitations_modify_staff" ON invitations
FOR ALL FOR ALL
USING (is_staff_or_higher()); USING (is_staff_or_higher());
-- BOOKINGS POLICIES -- BOOKINGS POLICIES
DROP POLICY IF EXISTS "bookings_select_admin_manager" ON bookings;
CREATE POLICY "bookings_select_admin_manager" ON bookings CREATE POLICY "bookings_select_admin_manager" ON bookings
FOR SELECT FOR SELECT
USING (get_current_user_role() IN ('admin', 'manager')); 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 CREATE POLICY "bookings_select_staff_location" ON bookings
FOR SELECT FOR SELECT
USING ( 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 CREATE POLICY "bookings_select_artist_own" ON bookings
FOR SELECT FOR SELECT
USING ( 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())) 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 CREATE POLICY "bookings_select_own" ON bookings
FOR SELECT FOR SELECT
USING (is_customer() AND customer_id = (SELECT id FROM customers WHERE user_id = auth.uid())); 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 CREATE POLICY "bookings_modify_admin_manager" ON bookings
FOR ALL FOR ALL
USING (get_current_user_role() IN ('admin', 'manager')); 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 CREATE POLICY "bookings_modify_staff_location" ON bookings
FOR ALL FOR ALL
USING ( 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 CREATE POLICY "bookings_no_modify_artist" ON bookings
FOR ALL FOR ALL
USING (NOT is_artist()); USING (NOT is_artist());
DROP POLICY IF EXISTS "bookings_create_own" ON bookings;
CREATE POLICY "bookings_create_own" ON bookings CREATE POLICY "bookings_create_own" ON bookings
FOR INSERT FOR INSERT
WITH CHECK ( WITH CHECK (
@@ -457,6 +512,7 @@ CREATE POLICY "bookings_create_own" ON bookings
customer_id = (SELECT id FROM customers WHERE user_id = auth.uid()) 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 CREATE POLICY "bookings_update_own" ON bookings
FOR UPDATE FOR UPDATE
USING ( USING (
@@ -465,10 +521,12 @@ CREATE POLICY "bookings_update_own" ON bookings
); );
-- AUDIT LOGS POLICIES -- 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 CREATE POLICY "audit_logs_select_admin_manager" ON audit_logs
FOR SELECT FOR SELECT
USING (get_current_user_role() IN ('admin', 'manager')); 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 CREATE POLICY "audit_logs_select_staff_location" ON audit_logs
FOR SELECT FOR SELECT
USING ( 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 CREATE POLICY "audit_logs_no_insert" ON audit_logs
FOR INSERT FOR INSERT
WITH CHECK (false); WITH CHECK (false);
@@ -521,18 +580,18 @@ CREATE OR REPLACE FUNCTION generate_invitation_code()
RETURNS VARCHAR(10) AS $$ RETURNS VARCHAR(10) AS $$
DECLARE DECLARE
chars VARCHAR(36) := '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; chars VARCHAR(36) := '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
code VARCHAR(10); new_code VARCHAR(10);
attempts INT := 0; attempts INT := 0;
max_attempts INT := 10; max_attempts INT := 10;
BEGIN BEGIN
LOOP LOOP
code := ''; new_code := '';
FOR i IN 1..10 LOOP 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; END LOOP;
IF NOT EXISTS (SELECT 1 FROM invitations WHERE code = code) THEN IF NOT EXISTS (SELECT 1 FROM invitations WHERE code = new_code) THEN
RETURN code; RETURN new_code;
END IF; END IF;
attempts := attempts + 1; attempts := attempts + 1;
@@ -593,7 +652,7 @@ BEGIN
customer_uuid, customer_uuid,
'reset_invitations', 'reset_invitations',
'{"week_start": null}'::JSONB, '{"week_start": null}'::JSONB,
'{"week_start": "' || week_start || '", "count": 5}'::JSONB, jsonb_build_object('week_start', week_start, 'count', 5),
NULL, NULL,
'system', 'system',
'{"reset_type": "weekly", "invitations_created": 5}'::JSONB '{"reset_type": "weekly", "invitations_created": 5}'::JSONB
@@ -637,7 +696,7 @@ BEGIN
) )
VALUES ( VALUES (
'invitations', 'invitations',
uuid_generate_v4(), gen_random_uuid(),
'reset_invitations', 'reset_invitations',
'{}'::JSONB, '{}'::JSONB,
result, result,
@@ -736,18 +795,23 @@ END;
$$ LANGUAGE plpgsql SECURITY DEFINER; $$ LANGUAGE plpgsql SECURITY DEFINER;
-- APPLY AUDIT LOG TRIGGERS -- APPLY AUDIT LOG TRIGGERS
DROP TRIGGER IF EXISTS audit_bookings ON bookings;
CREATE TRIGGER audit_bookings AFTER INSERT OR UPDATE OR DELETE ON bookings CREATE TRIGGER audit_bookings AFTER INSERT OR UPDATE OR DELETE ON bookings
FOR EACH ROW EXECUTE FUNCTION log_audit(); 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 CREATE TRIGGER audit_customers AFTER INSERT OR UPDATE OR DELETE ON customers
FOR EACH ROW EXECUTE FUNCTION log_audit(); 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 CREATE TRIGGER audit_invitations AFTER INSERT OR UPDATE OR DELETE ON invitations
FOR EACH ROW EXECUTE FUNCTION log_audit(); 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 CREATE TRIGGER audit_staff AFTER INSERT OR UPDATE OR DELETE ON staff
FOR EACH ROW EXECUTE FUNCTION log_audit(); 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 CREATE TRIGGER audit_services AFTER INSERT OR UPDATE OR DELETE ON services
FOR EACH ROW EXECUTE FUNCTION log_audit(); FOR EACH ROW EXECUTE FUNCTION log_audit();
@@ -762,6 +826,7 @@ BEGIN
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS booking_generate_short_id ON bookings;
CREATE TRIGGER booking_generate_short_id BEFORE INSERT ON bookings CREATE TRIGGER booking_generate_short_id BEFORE INSERT ON bookings
FOR EACH ROW EXECUTE FUNCTION generate_booking_short_id(); FOR EACH ROW EXECUTE FUNCTION generate_booking_short_id();

View File

@@ -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
$$;

View File

@@ -57,56 +57,69 @@ SALONOS - DATABASE MIGRATION COMPLETED
```sql ```sql
INSERT INTO locations (name, timezone, address, phone, is_active) INSERT INTO locations (name, timezone, address, phone, is_active)
VALUES VALUES
('Salón Principal - Centro', 'America/Mexico_City', 'Av. Reforma 222, Centro Histórico, Ciudad de México', '+52 55 1234 5678', true), ('ANCHOR:23 - Via KLAVA', 'America/Monterrey', 'Blvd. Moctezuma 2370, Los Pinos 2do y 3er Sector, 25204 Saltillo, Coah.', '+52 844 123 4567', true),
('Salón Norte - Polanco', 'America/Mexico_City', 'Av. Masaryk 123, Polanco, Ciudad de México', '+52 55 2345 6789', true), ('TEST - Salón Principal', 'America/Monterrey', 'Av. Universidad 456, Zona Centro, 25000 Saltillo, Coah.', '+52 844 234 5678', true);
('Salón Sur - Coyoacán', 'America/Mexico_City', 'Calle Hidalgo 456, Coyoacán, Ciudad de México', '+52 55 3456 7890', true);
``` ```
3. Deberías ver: `Success, no rows returned` 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: 1. Nuevo query, copiar y ejecutar:
```sql ```sql
-- ANCHOR:23 - Via KLAVA (4 estaciones: 1 maquillaje + 3 pedicure)
INSERT INTO resources (location_id, name, type, capacity, is_active) INSERT INTO resources (location_id, name, type, capacity, is_active)
SELECT SELECT
(SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1),
'Estación ' || generate_series(1, 3)::TEXT, 'Estación de Maquillaje',
'station', 'station'::resource_type,
1, 1,
true true
UNION ALL UNION ALL
SELECT SELECT
(SELECT id FROM locations WHERE name = 'Salón Norte - Polanco' LIMIT 1), (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' LIMIT 1),
'Estación ' || generate_series(1, 2)::TEXT, 'Sillón Pedicure ' || generate_series(1, 3)::TEXT,
'station', '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, 1,
true true
UNION ALL UNION ALL
SELECT SELECT
(SELECT id FROM locations WHERE name = 'Salón Sur - Coyoacán' LIMIT 1), (SELECT id FROM locations WHERE name = 'TEST - Salón Principal' LIMIT 1),
'Estación 1', 'Sillón Pedicure ' || generate_series(1, 3)::TEXT,
'station', 'station'::resource_type,
1, 1,
true; true;
``` ```
2. Deberías ver: `Success, no rows returned` 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: 1. Nuevo query, copiar y ejecutar:
```sql ```sql
INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active) INSERT INTO staff (user_id, location_id, role, display_name, phone, is_active)
VALUES VALUES
(uuid_generate_v4(), (SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), 'admin', 'Admin Principal', '+52 55 1111 2222', true), -- ANCHOR:23 - Via KLAVA (5 empleadas)
(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 = 'ANCHOR:23 - Via KLAVA' LIMIT 1), 'admin', 'Mariana González', '+52 844 111 2222', 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 = 'ANCHOR:23 - Via KLAVA' LIMIT 1), 'manager', 'Daniela Sánchez', '+52 844 222 3333', 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 = 'ANCHOR:23 - Via KLAVA' LIMIT 1), 'artist', 'Karla Rodríguez', '+52 844 333 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 = 'ANCHOR:23 - Via KLAVA' LIMIT 1), 'artist', 'Fernanda Martínez', '+52 844 444 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 = 'ANCHOR:23 - Via KLAVA' LIMIT 1), 'artist', 'Paola Hernández', '+52 844 555 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); -- 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` 2. Deberías ver: `Success, no rows returned`
@@ -117,26 +130,61 @@ VALUES
```sql ```sql
INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active) INSERT INTO services (name, description, duration_minutes, base_price, requires_dual_artist, premium_fee_enabled, is_active)
VALUES VALUES
('Corte y Estilizado', 'Corte de cabello profesional con lavado y estilizado', 60, 500.00, false, false, true), ('Maquillaje Social', 'Maquillaje para eventos sociales', 45, 450.00, false, false, true),
('Color Completo', 'Tinte completo con protección capilar', 120, 1200.00, false, true, true), ('Maquillaje de Novia', 'Maquillaje profesional para novias', 90, 1200.00, false, true, true),
('Balayage Premium', 'Técnica de balayage con productos premium', 180, 2000.00, true, true, true), ('Pedicure Básico', 'Pedicure con esmaltado tradicional', 45, 300.00, false, false, true),
('Tratamiento Kératina', 'Tratamiento de kératina para cabello dañado', 90, 1500.00, false, false, true), ('Pedicure Spa', 'Pedicure con exfoliación y masaje', 60, 450.00, false, false, true),
('Peinado Evento', 'Peinado para eventos especiales', 45, 800.00, false, true, true), ('Pedicure Premium', 'Pedicure completo con tratamiento de parafina', 75, 600.00, false, true, true),
('Servicio Express (Dual Artist)', 'Servicio rápido con dos artists simultáneas', 30, 600.00, true, 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. 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 ### 2.5 Crear Customers
1. Nuevo query, copiar y ejecutar: 1. Nuevo query, copiar y ejecutar:
```sql ```sql
INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, notes, total_spent, total_visits, last_visit_date, is_active) INSERT INTO customers (user_id, first_name, last_name, email, phone, tier, notes, total_spent, total_visits, last_visit_date, is_active)
VALUES 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(), '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(), '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(), '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(), '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(), '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(), '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(), '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` 2. Deberías ver: `Success, no rows returned`
@@ -145,9 +193,10 @@ VALUES
1. Nuevo query, copiar y ejecutar: 1. Nuevo query, copiar y ejecutar:
```sql ```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 = 'mariajose.villarreal@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 = 'anapaula.garza@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 = '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: 2. Deberías ver:
@@ -178,49 +227,64 @@ INSERT INTO bookings (
notes notes
) )
SELECT SELECT
(SELECT id FROM customers WHERE email = 'sofia.ramirez@example.com' LIMIT 1), (SELECT id FROM customers WHERE email = 'mariajose.villarreal@example.com' LIMIT 1),
(SELECT id FROM staff WHERE display_name = 'Artist María García' LIMIT 1), (SELECT id FROM staff WHERE display_name = 'Karla Rodríguez' LIMIT 1),
(SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' 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 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 = 'Balayage Premium' LIMIT 1), (SELECT id FROM services WHERE name = 'Maquillaje de Novia' LIMIT 1),
NOW() + INTERVAL '1 day', NOW() + INTERVAL '1 day',
NOW() + INTERVAL '4 hours', NOW() + INTERVAL '1 day 1 hour 30 minutes',
'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', 'confirmed',
200.00, 200.00,
1200.00, 1200.00,
true, true,
'pay_test_002', 'pay_test_001',
'Color Completo para Valentina' 'Maquillaje de novia para María José'
UNION ALL UNION ALL
SELECT SELECT
(SELECT id FROM customers WHERE email = 'camila.lopez@example.com' LIMIT 1), (SELECT id FROM customers WHERE email = 'anapaula.garza@example.com' LIMIT 1),
(SELECT id FROM staff WHERE display_name = 'Artist María García' LIMIT 1), (SELECT id FROM staff WHERE display_name = 'Fernanda Martínez' LIMIT 1),
(SELECT id FROM locations WHERE name = 'Salón Principal - Centro' LIMIT 1), (SELECT id FROM locations WHERE name = 'ANCHOR:23 - Via KLAVA' 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 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 = 'Corte y Estilizado' 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 '3 days',
NOW() + INTERVAL '1 hour', NOW() + INTERVAL '3 days 45 minutes',
'confirmed', 'confirmed',
50.00, 50.00,
500.00, 300.00,
true, true,
'pay_test_003', '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` 2. Deberías ver: `Success, no rows returned`
@@ -247,19 +311,21 @@ SELECT 'Bookings: ' || COUNT(*) FROM bookings;
2. Deberías ver: 2. Deberías ver:
``` ```
resumen resumen
Locations: 3 Locations: 2
Resources: 6 Resources: 8
Staff: 8 Staff: 10
Services: 6 Services: 7
Customers: 4 Customers: 5
Invitaciones: 15 Invitaciones: 20
Bookings: 3 Bookings: 4
``` ```
**Si ves números diferentes:** **Si ves números diferentes:**
- Verifica que todos los queries anteriores se ejecutaron correctamente - Verifica que todos los queries anteriores se ejecutaron correctamente
- Revisa los errores (si hubo) - 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) ## 📋 PASO 3: CREAR USUARIOS AUTH (10 min)
@@ -277,7 +343,7 @@ Bookings: 3
### 3.3 Crear Usuario Customer (para probar) ### 3.3 Crear Usuario Customer (para probar)
1. Clic en botón "Add user" 1. Clic en botón "Add user"
2. Email: `sofia.ramirez@example.com` 2. Email: `mariajose.villarreal@example.com`
3. Password: `Customer123!` 3. Password: `Customer123!`
4. **Auto Confirm User:** ON (marcar la casilla) 4. **Auto Confirm User:** ON (marcar la casilla)
5. Clic en "Create user" 5. Clic en "Create user"
@@ -286,7 +352,7 @@ Bookings: 3
### 3.4 Verificar Usuarios ### 3.4 Verificar Usuarios
1. En la página de Auth Users, deberías ver 2 usuarios: 1. En la página de Auth Users, deberías ver 2 usuarios:
- admin@salonos.com - 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 -- REEMPLAZA [USER_ID_DEL_CUSTOMER] con el ID que copiaste del paso 3.3
UPDATE customers UPDATE customers
SET user_id = '[USER_ID_DEL_CUSTOMER]' 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): 3. Ejemplo de cómo debe verse (NO ejecutar esto, es solo ejemplo):
```sql ```sql
UPDATE customers UPDATE customers
SET user_id = '01234567-89ab-cdef-0123-456789abcdef' 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 ### 4.2 Verificar Actualización
@@ -321,7 +387,7 @@ SELECT
au.email as auth_user_email au.email as auth_user_email
FROM customers c FROM customers c
LEFT JOIN auth.users au ON c.user_id = au.id 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` 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:** **Antes de ir a la junta, marca cada paso:**
- [ ] PASO 1: Migraciones ejecutadas (verificar mensaje "MIGRATION COMPLETED") - [ ] PASO 1: Migraciones ejecutadas (verificar mensaje "MIGRATION COMPLETED")
- [ ] PASO 2.1: Locations creadas (3) - [ ] PASO 2.1: Locations creadas (2 - Saltillo)
- [ ] PASO 2.2: Resources creados (6) - [ ] PASO 2.2: Resources creados (8 - 4 por salón)
- [ ] PASO 2.3: Staff creado (8) - [ ] PASO 2.3: Staff creado (10 - 5 por salón)
- [ ] PASO 2.4: Services creados (6) - [ ] PASO 2.4: Services creados (7 - maquillaje y pedicure)
- [ ] PASO 2.5: Customers creados (4) - [ ] PASO 2.4.1: Nuevos tiers agregados (black, VIP)
- [ ] PASO 2.6: Invitaciones creadas (15) - [ ] PASO 2.5: Customers creados (5 - varios tiers)
- [ ] PASO 2.7: Bookings creados (3) - [ ] PASO 2.6: Invitaciones creadas (20 - 4 clientes premium)
- [ ] PASO 2.8: Verificación correcta (3-6-8-6-4-15-3) - [ ] 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.2: Usuario Admin creado
- [ ] PASO 3.3: Usuario Customer creado - [ ] PASO 3.3: Usuario Customer creado
- [ ] PASO 3.4: Verificación Auth Users correcta - [ ] PASO 3.4: Verificación Auth Users correcta
@@ -421,7 +488,7 @@ Guarda esto en un lugar seguro:
- Password: `Admin123!` - Password: `Admin123!`
**Customer (para probar):** **Customer (para probar):**
- Email: `sofia.ramirez@example.com` - Email: `mariajose.villarreal@example.com`
- Password: `Customer123!` - Password: `Customer123!`
--- ---