mirror of
https://github.com/marcogll/AnchorOS.git
synced 2026-03-15 14:24:27 +00:00
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:
118
AGENTS.md
118
AGENTS.md
@@ -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.
|
|
||||||
@@ -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! 🎉
|
|
||||||
376
FASE_1_STATUS.md
376
FASE_1_STATUS.md
@@ -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)
|
|
||||||
229
SIMPLE_GUIDE.md
229
SIMPLE_GUIDE.md
@@ -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!** 🚀
|
|
||||||
114
db/migrate.sh
114
db/migrate.sh
@@ -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 ""
|
|
||||||
@@ -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
|
|
||||||
-- ============================================
|
|
||||||
@@ -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
|
|
||||||
-- ============================================
|
|
||||||
@@ -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
|
|
||||||
-- ============================================
|
|
||||||
@@ -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
|
|
||||||
$$;
|
|
||||||
@@ -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
|
|
||||||
@@ -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 '===========================================';
|
|
||||||
@@ -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
|
|
||||||
@@ -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.
|
|
||||||
@@ -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
|
|
||||||
@@ -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?**
|
|
||||||
@@ -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!** 🚀
|
|
||||||
@@ -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?**
|
|
||||||
@@ -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?**
|
|
||||||
@@ -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.
|
|
||||||
@@ -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?**
|
|
||||||
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
|
|
||||||
$$;
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
8
supabase/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Supabase
|
||||||
|
.branches
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# dotenvx
|
||||||
|
.env.keys
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
384
supabase/config.toml
Normal file
384
supabase/config.toml
Normal 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)"
|
||||||
@@ -12,15 +12,30 @@
|
|||||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
-- ENUMS
|
-- ENUMS
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN
|
||||||
CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer');
|
CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer');
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'customer_tier') THEN
|
||||||
CREATE TYPE customer_tier AS ENUM ('free', 'gold');
|
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');
|
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');
|
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');
|
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');
|
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
|
||||||
$$;
|
$$;
|
||||||
@@ -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
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN
|
||||||
CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer');
|
CREATE TYPE user_role AS ENUM ('admin', 'manager', 'staff', 'artist', 'customer');
|
||||||
CREATE TYPE customer_tier AS ENUM ('free', 'gold');
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'customer_tier') THEN
|
||||||
|
CREATE TYPE customer_tier AS ENUM ('free', 'gold', 'black', 'VIP');
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'booking_status') THEN
|
||||||
CREATE TYPE booking_status AS ENUM ('pending', 'confirmed', 'cancelled', 'completed', 'no_show');
|
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');
|
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');
|
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');
|
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();
|
||||||
|
|
||||||
393
supabase/migrations/20260115235900_seed_data.sql
Normal file
393
supabase/migrations/20260115235900_seed_data.sql
Normal 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
|
||||||
|
$$;
|
||||||
241
tasks_mg.md
241
tasks_mg.md
@@ -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!`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user