Files
AnchorOS/docs/MIGRATION_CORRECTION.md
Marco Gallegos 4707ddbd5a 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)
2026-01-15 14:58:28 -06:00

9.6 KiB

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:

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

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

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:

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:

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

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

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

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


¿Necesitas ayuda adicional? Una vez que hayas ejecutado la migración corregida, avísame para verificar que todo esté funcionando correctamente.