Initial commit: monorepo Naturcalabacera reservas (apps/api + apps/web + packages/shared)
This commit is contained in:
2
supabase/migrations/001_add_observations.sql
Normal file
2
supabase/migrations/001_add_observations.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- Migration: Add observations column
|
||||
alter table public.reservations add column observations text;
|
||||
15
supabase/migrations/002_add_property.sql
Normal file
15
supabase/migrations/002_add_property.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- Migración 002: Añadir soporte multi-propiedad
|
||||
-- PREREQUISITO: Backup/export de datos existentes antes de ejecutar.
|
||||
-- Todos los registros existentes se asignan a 'los_dragos' por defecto.
|
||||
-- Confirmar con el negocio si es correcto antes de ejecutar.
|
||||
|
||||
-- Crear tipo enum para propiedades
|
||||
CREATE TYPE property_type AS ENUM ('los_dragos', 'la_esquinita');
|
||||
|
||||
-- Añadir columna property con default para registros existentes
|
||||
ALTER TABLE public.reservations
|
||||
ADD COLUMN property property_type NOT NULL DEFAULT 'los_dragos';
|
||||
|
||||
-- Eliminar el default una vez aplicado (queda el valor, pero nuevos registros deben especificarlo)
|
||||
-- Comentar esta línea si se prefiere mantener el default permanentemente
|
||||
ALTER TABLE public.reservations ALTER COLUMN property DROP DEFAULT;
|
||||
23
supabase/migrations/003_add_pricing_snapshot.sql
Normal file
23
supabase/migrations/003_add_pricing_snapshot.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
-- Migración 003: Añadir campos de precio para reservas Natur
|
||||
-- Se usa un snapshot JSON para congelar el cálculo acordado en el momento de guardar.
|
||||
-- El cálculo dinámico vive en packages/shared/src/utils/pricing.ts
|
||||
-- Estas columnas son nullable — solo se populan para origin = 'Naturcalabacera'
|
||||
|
||||
ALTER TABLE public.reservations
|
||||
ADD COLUMN igic_rate NUMERIC(5,4) DEFAULT 0.07,
|
||||
ADD COLUMN pricing_snapshot JSONB;
|
||||
|
||||
-- Estructura del JSONB pricing_snapshot:
|
||||
-- {
|
||||
-- "basePrice": 1800, -- Canon base * noches
|
||||
-- "extraPersonsFee": 120, -- Personas extra * tarifa * noches
|
||||
-- "subtotal": 1920, -- basePrice + extraPersonsFee
|
||||
-- "igicAmount": 134.4, -- subtotal * igicRate
|
||||
-- "total": 2054.4, -- subtotal + igicAmount
|
||||
-- "calculatedAt": "2026-04-10T12:00:00Z"
|
||||
-- }
|
||||
|
||||
COMMENT ON COLUMN public.reservations.pricing_snapshot IS
|
||||
'Snapshot congelado del cálculo de precio al guardar. Solo para origin=Naturcalabacera.';
|
||||
COMMENT ON COLUMN public.reservations.igic_rate IS
|
||||
'Tasa IGIC aplicada. Default 0.07 (7%). Configurable por reserva.';
|
||||
29
supabase/migrations/004_add_contracts_table.sql
Normal file
29
supabase/migrations/004_add_contracts_table.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
-- Migración 004: Tabla de contratos adjuntos a reservas
|
||||
-- Diseño: tabla separada (no columnas en reservations) para soportar múltiples contratos por reserva
|
||||
|
||||
CREATE TABLE public.reservation_contracts (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
reservation_id UUID NOT NULL REFERENCES public.reservations(id) ON DELETE CASCADE,
|
||||
file_path TEXT NOT NULL, -- Path en Supabase Storage: contracts/{reservation_id}/{filename}
|
||||
filename TEXT NOT NULL, -- Nombre visible del archivo
|
||||
mime_type TEXT NOT NULL, -- application/pdf, image/jpeg, image/png
|
||||
size_bytes INTEGER NOT NULL,
|
||||
uploaded_by UUID REFERENCES auth.users(id),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
-- Índice para consultas por reserva
|
||||
CREATE INDEX idx_reservation_contracts_reservation_id
|
||||
ON public.reservation_contracts(reservation_id);
|
||||
|
||||
ALTER TABLE public.reservation_contracts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Por ahora: staff puede gestionar contratos (RLS se refina en migración 009)
|
||||
CREATE POLICY "Temp: authenticated can manage contracts"
|
||||
ON public.reservation_contracts
|
||||
FOR ALL TO authenticated
|
||||
USING (true)
|
||||
WITH CHECK (true);
|
||||
|
||||
COMMENT ON TABLE public.reservation_contracts IS
|
||||
'Contratos adjuntos a reservas. Archivos almacenados en Supabase Storage bucket "contracts".';
|
||||
43
supabase/migrations/005_add_user_roles.sql
Normal file
43
supabase/migrations/005_add_user_roles.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
-- Migración 005: Sistema de roles de usuario
|
||||
-- Roles definidos para ser extensibles sin reescribir lógica:
|
||||
-- admin: acceso total
|
||||
-- internal_staff: acceso completo, sin gestión de usuarios (futuro uso)
|
||||
-- external_availability_viewer: solo ve ocupado/libre (Teneriffa)
|
||||
|
||||
CREATE TYPE user_role AS ENUM ('admin', 'internal_staff', 'external_availability_viewer');
|
||||
|
||||
CREATE TABLE public.user_profiles (
|
||||
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
email TEXT NOT NULL,
|
||||
role user_role NOT NULL DEFAULT 'admin',
|
||||
display_name TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Los usuarios pueden leer su propio perfil
|
||||
CREATE POLICY "Users can read own profile"
|
||||
ON public.user_profiles FOR SELECT TO authenticated
|
||||
USING (auth.uid() = id);
|
||||
|
||||
-- Solo admin puede gestionar perfiles (INSERT/UPDATE/DELETE se hace via service_role en el API)
|
||||
|
||||
-- Función para obtener el rol del usuario actual (usada en otras policies RLS)
|
||||
CREATE OR REPLACE FUNCTION public.get_user_role()
|
||||
RETURNS user_role AS $$
|
||||
SELECT role FROM public.user_profiles WHERE id = auth.uid();
|
||||
$$ LANGUAGE sql SECURITY DEFINER STABLE;
|
||||
|
||||
COMMENT ON FUNCTION public.get_user_role IS
|
||||
'Devuelve el rol del usuario autenticado actual. Usada en políticas RLS.';
|
||||
|
||||
-- ============================================================
|
||||
-- ACCIÓN MANUAL REQUERIDA antes de ejecutar migración 009:
|
||||
-- Insertar perfil admin para el usuario actual:
|
||||
--
|
||||
-- INSERT INTO public.user_profiles (id, email, role)
|
||||
-- SELECT id, email, 'admin'
|
||||
-- FROM auth.users
|
||||
-- WHERE email = 'tu-email@ejemplo.com';
|
||||
-- ============================================================
|
||||
24
supabase/migrations/006_add_audit_fields.sql
Normal file
24
supabase/migrations/006_add_audit_fields.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
-- Migración 006: Campos de auditoría en reservations
|
||||
-- Trazabilidad mínima: quién creó/modificó y cuándo
|
||||
|
||||
ALTER TABLE public.reservations
|
||||
ADD COLUMN created_by UUID REFERENCES auth.users(id),
|
||||
ADD COLUMN updated_by UUID REFERENCES auth.users(id),
|
||||
ADD COLUMN updated_at TIMESTAMPTZ;
|
||||
|
||||
-- Función para actualizar updated_at automáticamente en cada UPDATE
|
||||
CREATE OR REPLACE FUNCTION public.set_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_reservations_updated_at
|
||||
BEFORE UPDATE ON public.reservations
|
||||
FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
|
||||
|
||||
COMMENT ON COLUMN public.reservations.created_by IS 'UUID del usuario que creó la reserva.';
|
||||
COMMENT ON COLUMN public.reservations.updated_by IS 'UUID del usuario que realizó el último cambio.';
|
||||
COMMENT ON COLUMN public.reservations.updated_at IS 'Timestamp del último cambio (auto-actualizado por trigger).';
|
||||
49
supabase/migrations/007_add_notification_events.sql
Normal file
49
supabase/migrations/007_add_notification_events.sql
Normal file
@@ -0,0 +1,49 @@
|
||||
-- Migración 007: Tabla de eventos de notificación
|
||||
-- Fuente de verdad para el sistema de jobs. Garantiza idempotencia mediante UNIQUE constraint.
|
||||
--
|
||||
-- Tipos de evento soportados:
|
||||
-- reservation.created — Teneriffa CRUD
|
||||
-- reservation.updated — Teneriffa CRUD
|
||||
-- reservation.cancelled — Teneriffa CRUD
|
||||
-- reservation.reminder_24h — 24h antes check-in (Teneriffa + Los Dragos)
|
||||
-- reservation.invoice_second_notice — 10 días antes check-in (Natur, ambas casas)
|
||||
-- reservation.pool_heating_notice — 48h antes check-in (si has_pool_heating = true)
|
||||
|
||||
CREATE TABLE public.notification_events (
|
||||
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
|
||||
reservation_id UUID REFERENCES public.reservations(id) ON DELETE CASCADE,
|
||||
event_type TEXT NOT NULL,
|
||||
scheduled_for TIMESTAMPTZ NOT NULL,
|
||||
sent_at TIMESTAMPTZ,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
-- 'pending': esperando ejecución
|
||||
-- 'processing': siendo procesado por el runner (previene duplicados)
|
||||
-- 'sent': enviado correctamente
|
||||
-- 'failed': fallido después de max_retries intentos
|
||||
attempts INTEGER DEFAULT 0 NOT NULL,
|
||||
last_error TEXT,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
|
||||
|
||||
-- CONSTRAINT DE IDEMPOTENCIA:
|
||||
-- Un mismo evento (mismo tipo + misma fecha programada) para una reserva
|
||||
-- solo puede existir una vez. ON CONFLICT DO NOTHING previene duplicados.
|
||||
CONSTRAINT uq_notification_event
|
||||
UNIQUE(reservation_id, event_type, scheduled_for)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_notification_events_pending
|
||||
ON public.notification_events(scheduled_for)
|
||||
WHERE status = 'pending';
|
||||
|
||||
ALTER TABLE public.notification_events ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- Solo admin puede ver eventos de notificación (se refina en migración 009)
|
||||
CREATE POLICY "Temp: authenticated can read notifications"
|
||||
ON public.notification_events FOR SELECT TO authenticated
|
||||
USING (true);
|
||||
|
||||
COMMENT ON TABLE public.notification_events IS
|
||||
'Eventos de notificación programados. El runner en apps/api los procesa periódicamente.';
|
||||
COMMENT ON COLUMN public.notification_events.status IS
|
||||
'pending → processing → sent | failed. El estado "processing" previene que múltiples instancias procesen el mismo evento.';
|
||||
25
supabase/migrations/008_add_availability_view.sql
Normal file
25
supabase/migrations/008_add_availability_view.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
-- Migración 008: Vista segura para usuarios con rol external_availability_viewer
|
||||
--
|
||||
-- Problema: RLS limita FILAS pero no COLUMNAS.
|
||||
-- Los usuarios Teneriffa solo deben ver ocupado/libre, sin datos personales.
|
||||
-- Solución: vista que expone únicamente los campos necesarios.
|
||||
-- El frontend de Teneriffa consulta esta vista, no la tabla directa.
|
||||
|
||||
CREATE OR REPLACE VIEW public.reservations_availability AS
|
||||
SELECT
|
||||
id,
|
||||
start_date,
|
||||
end_date,
|
||||
property,
|
||||
'Ocupado' AS status -- Nunca expone el origen real ni el cliente
|
||||
FROM public.reservations;
|
||||
|
||||
-- Dar acceso de SELECT a usuarios authenticated a la vista
|
||||
-- La seguridad real se gestiona en el frontend con useUserRole()
|
||||
-- y en la migración 009 mediante RLS en la tabla base.
|
||||
GRANT SELECT ON public.reservations_availability TO authenticated;
|
||||
|
||||
COMMENT ON VIEW public.reservations_availability IS
|
||||
'Vista de disponibilidad segura. Solo expone fechas y propiedad. '
|
||||
'Usada por usuarios external_availability_viewer (Teneriffa). '
|
||||
'No expone cliente, precios, notas ni datos personales.';
|
||||
82
supabase/migrations/009_update_rls_policies.sql
Normal file
82
supabase/migrations/009_update_rls_policies.sql
Normal file
@@ -0,0 +1,82 @@
|
||||
-- Migración 009: Políticas RLS de producción
|
||||
--
|
||||
-- ⚠️ PREREQUISITO OBLIGATORIO antes de ejecutar:
|
||||
-- Insertar perfil admin para TU usuario actual. Si no lo haces,
|
||||
-- quedarás bloqueado y no podrás acceder a los datos.
|
||||
--
|
||||
-- Ejecuta PRIMERO (reemplaza el email):
|
||||
-- INSERT INTO public.user_profiles (id, email, role)
|
||||
-- SELECT id, email, 'admin' FROM auth.users
|
||||
-- WHERE email = 'tu-email@ejemplo.com'
|
||||
-- ON CONFLICT (id) DO NOTHING;
|
||||
--
|
||||
-- Verifica que tienes acceso ANTES de continuar.
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- TABLA: reservations
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
|
||||
-- Eliminar políticas dev-mode abiertas
|
||||
DROP POLICY IF EXISTS "Enable read access for all users" ON public.reservations;
|
||||
DROP POLICY IF EXISTS "Enable insert for all users" ON public.reservations;
|
||||
DROP POLICY IF EXISTS "Enable update for all users" ON public.reservations;
|
||||
DROP POLICY IF EXISTS "Enable delete for all users" ON public.reservations;
|
||||
|
||||
-- Admin e internal_staff: acceso completo a la tabla
|
||||
CREATE POLICY "Staff: full read"
|
||||
ON public.reservations FOR SELECT TO authenticated
|
||||
USING (public.get_user_role() IN ('admin', 'internal_staff'));
|
||||
|
||||
CREATE POLICY "Staff: insert"
|
||||
ON public.reservations FOR INSERT TO authenticated
|
||||
WITH CHECK (public.get_user_role() IN ('admin', 'internal_staff'));
|
||||
|
||||
CREATE POLICY "Staff: update"
|
||||
ON public.reservations FOR UPDATE TO authenticated
|
||||
USING (public.get_user_role() IN ('admin', 'internal_staff'))
|
||||
WITH CHECK (public.get_user_role() IN ('admin', 'internal_staff'));
|
||||
|
||||
CREATE POLICY "Staff: delete"
|
||||
ON public.reservations FOR DELETE TO authenticated
|
||||
USING (public.get_user_role() IN ('admin', 'internal_staff'));
|
||||
|
||||
-- external_availability_viewer: SIN acceso directo a la tabla reservations.
|
||||
-- Solo accede a la vista reservations_availability (migración 008).
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- TABLA: reservation_contracts
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
|
||||
DROP POLICY IF EXISTS "Temp: authenticated can manage contracts" ON public.reservation_contracts;
|
||||
|
||||
CREATE POLICY "Staff: manage contracts"
|
||||
ON public.reservation_contracts FOR ALL TO authenticated
|
||||
USING (public.get_user_role() IN ('admin', 'internal_staff'))
|
||||
WITH CHECK (public.get_user_role() IN ('admin', 'internal_staff'));
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- TABLA: notification_events
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
|
||||
DROP POLICY IF EXISTS "Temp: authenticated can read notifications" ON public.notification_events;
|
||||
|
||||
CREATE POLICY "Admin: read notifications"
|
||||
ON public.notification_events FOR SELECT TO authenticated
|
||||
USING (public.get_user_role() = 'admin');
|
||||
|
||||
-- El API usa service_role para INSERT/UPDATE en notification_events (bypasea RLS).
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- TABLA: user_profiles
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
|
||||
-- La policy existente "Users can read own profile" se mantiene.
|
||||
-- El API usa service_role para gestionar perfiles.
|
||||
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
-- REVOCAR acceso anónimo a todas las tablas
|
||||
-- ────────────────────────────────────────────────────────────
|
||||
REVOKE ALL ON public.reservations FROM anon;
|
||||
REVOKE ALL ON public.reservation_contracts FROM anon;
|
||||
REVOKE ALL ON public.notification_events FROM anon;
|
||||
REVOKE ALL ON public.user_profiles FROM anon;
|
||||
13
supabase/migrations/010_add_document_type.sql
Normal file
13
supabase/migrations/010_add_document_type.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- Migración 010: Soporte para múltiples tipos de documento (contrato, factura, otros)
|
||||
-- La tabla reservation_contracts pasa a ser un repositorio genérico de documentos
|
||||
-- de reserva. La columna document_type clasifica cada archivo.
|
||||
|
||||
ALTER TABLE natur_reservas.reservation_contracts
|
||||
ADD COLUMN IF NOT EXISTS document_type TEXT NOT NULL DEFAULT 'contract'
|
||||
CHECK (document_type IN ('contract', 'invoice', 'other'));
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_reservation_contracts_doc_type
|
||||
ON natur_reservas.reservation_contracts(reservation_id, document_type);
|
||||
|
||||
COMMENT ON COLUMN natur_reservas.reservation_contracts.document_type IS
|
||||
'Tipo de documento: contract (contrato firmado), invoice (factura), other.';
|
||||
Reference in New Issue
Block a user