feat(salonos): implementar Fase 1.1 y 1.2 - Infraestructura y Esquema de Base de Datos

Implementación completa de la Fase 1.1 y 1.2 del proyecto SalonOS:

## Cambios en Reglas de Negocio (PRD.md, AGENTS.md, TASKS.md)
- Actualizado reset de invitaciones de mensual a semanal (Lunes 00:00 UTC)
- Jerarquía de roles actualizada: Admin > Manager > Staff > Artist > Customer
- Artistas (antes colaboradoras) ahora tienen rol 'artist'
- Staff/Manager/Admin pueden ver PII de customers
- Artist solo ve nombre y notas de customers (restricción de privacidad)

## Estructura del Proyecto (Next.js 14)
- app/boutique/: Frontend de cliente
- app/hq/: Dashboard administrativo
- app/api/: API routes
- components/: Componentes UI reutilizables (boutique, hq, shared)
- lib/: Lógica de negocio (supabase, db, utils)
- db/: Esquemas, migraciones y seeds
- integrations/: Stripe, Google Calendar, WhatsApp
- scripts/: Scripts de utilidad y automatización
- docs/: Documentación del proyecto

## Esquema de Base de Datos (Supabase PostgreSQL)
8 tablas creadas:
- locations: Ubicaciones 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 (free/gold)
- invitations: Sistema de invitaciones semanales
- bookings: Sistema de reservas con short_id (6 caracteres)
- audit_logs: Registro de auditoría automática

14 funciones creadas:
- generate_short_id(): Generador de Short ID (6 chars, collision-safe)
- generate_invitation_code(): Generador de códigos de invitación (10 chars)
- reset_weekly_invitations_for_customer(): Reset individual de invitaciones
- reset_all_weekly_invitations(): Reset masivo de invitaciones
- validate_secondary_artist_role(): Validación de secondary_artist
- log_audit(): Trigger de auditoría automática
- get_current_user_role(): Obtener rol del usuario actual
- is_staff_or_higher(): Verificar si es admin/manager/staff
- is_artist(): Verificar si es artist
- is_customer(): Verificar si es customer
- is_admin(): Verificar si es admin
- update_updated_at(): Actualizar timestamps
- generate_booking_short_id(): Generar Short ID automáticamente
- get_week_start(): Obtener inicio de semana

17+ triggers activos:
- Auditores automáticos en tablas críticas
- Timestamps updated_at en todas las tablas
- Validación de secondary_artist (trigger en lugar de constraint)

20+ políticas RLS configuradas:
- Restricción crítica: Artist no ve email/phone de customers
- Jerarquía de roles: Admin > Manager > Staff > Artist > Customer
- Políticas granulares por tipo de operación y rol

6 tipos ENUM:
- user_role: admin, manager, staff, artist, customer
- customer_tier: free, gold
- booking_status: pending, confirmed, cancelled, completed, no_show
- invitation_status: pending, used, expired
- resource_type: station, room, equipment
- audit_action: create, update, delete, reset_invitations, payment, status_change

## Scripts de Utilidad
- check-connection.sh: Verificar conexión a Supabase
- simple-verify.sh: Verificar migraciones instaladas
- simple-seed.sh: Crear datos de prueba
- create-auth-users.js: Crear usuarios de Auth en Supabase
- verify-migration.sql: Script de verificación SQL completo
- seed-data.sql: Script de seed de datos SQL completo

## Documentación
- docs/STEP_BY_STEP_VERIFICATION.md: Guía paso a paso de verificación
- docs/STEP_BY_STEP_AUTH_CONFIG.md: Guía paso a paso de configuración Auth
- docs/POST_MIGRATION_SUCCESS.md: Guía post-migración
- docs/MIGRATION_CORRECTION.md: Detalle de correcciones aplicadas
- docs/QUICK_START_POST_MIGRATION.md: Guía rápida de referencia
- docs/SUPABASE_DASHBOARD_MIGRATION.md: Guía de ejecución en Dashboard
- docs/00_FULL_MIGRATION_FINAL_README.md: Guía de migración final
- SIMPLE_GUIDE.md: Guía simple de inicio
- FASE_1_STATUS.md: Estado de la Fase 1

## Configuración
- package.json: Dependencias y scripts de npm
- tsconfig.json: Configuración TypeScript con paths aliases
- next.config.js: Configuración Next.js
- tailwind.config.ts: Tema personalizado con colores primary, secondary, gold
- postcss.config.js: Configuración PostCSS
- .gitignore: Archivos excluidos de git
- .env.example: Template de variables de entorno

## Correcciones Aplicadas
1. Constraint de subquery en CHECK reemplazado por trigger de validación
   - PostgreSQL no permite subqueries en CHECK constraints
   - validate_secondary_artist_role() ahora es un trigger

2. Variable no declarada en loop
   - customer_record RECORD; añadido en bloque DECLARE

## Principios Implementados
- UTC-first: Todos los timestamps se almacenan en UTC
- Sistema Doble Capa: Validación Staff/Artist + Recurso físico
- Reset semanal: Invitaciones se resetean cada Lunes 00:00 UTC
- Idempotencia: Procesos de reset son idempotentes y auditados
- Privacidad: Artist solo ve nombre y notas de customers
- Auditoría: Todas las acciones críticas se registran automáticamente
- Short ID: 6 caracteres alfanuméricos como referencia humana
- UUID: Identificador primario interno

## Próximos Pasos
- Ejecutar scripts de verificación y seed
- Configurar Auth en Supabase Dashboard
- Implementar Tarea 1.3: Short ID & Invitaciones (backend)
- Implementar Tarea 1.4: CRM Base (endpoints CRUD)
This commit is contained in:
Marco Gallegos
2026-01-15 14:58:28 -06:00
parent ebbd9e3762
commit 4707ddbd5a
40 changed files with 10038 additions and 13 deletions

View File

@@ -0,0 +1,314 @@
# ✅ 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.