From 6e5244b612c3e67b0894577f39796bab950b14fd Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sun, 25 Jan 2026 06:29:04 -0600 Subject: [PATCH] [SAAS-022] feat: Add Goals module DDL - 00-schema.sql: Schema creation with grants - 01-enums.sql: 8 enums (goal_type, metric_type, period_type, etc.) - 02-tables.sql: 4 tables (definitions, assignments, progress_log, milestone_notifications) - 04-rls.sql: 16 RLS policies for tenant isolation - 05-indexes.sql: Performance indexes Co-Authored-By: Claude Opus 4.5 --- ddl/schemas/goals/00-schema.sql | 14 +++ ddl/schemas/goals/01-enums.sql | 70 ++++++++++++++ ddl/schemas/goals/02-tables.sql | 155 +++++++++++++++++++++++++++++++ ddl/schemas/goals/04-rls.sql | 72 ++++++++++++++ ddl/schemas/goals/05-indexes.sql | 109 ++++++++++++++++++++++ 5 files changed, 420 insertions(+) create mode 100644 ddl/schemas/goals/00-schema.sql create mode 100644 ddl/schemas/goals/01-enums.sql create mode 100644 ddl/schemas/goals/02-tables.sql create mode 100644 ddl/schemas/goals/04-rls.sql create mode 100644 ddl/schemas/goals/05-indexes.sql diff --git a/ddl/schemas/goals/00-schema.sql b/ddl/schemas/goals/00-schema.sql new file mode 100644 index 0000000..9eddc94 --- /dev/null +++ b/ddl/schemas/goals/00-schema.sql @@ -0,0 +1,14 @@ +-- ============================================ +-- SAAS-022: Goals Schema +-- Sistema de metas y objetivos +-- ============================================ + +-- Create schema +CREATE SCHEMA IF NOT EXISTS goals; + +-- Grant permissions +GRANT USAGE ON SCHEMA goals TO template_saas_user; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA goals TO template_saas_user; +GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA goals TO template_saas_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA goals GRANT ALL ON TABLES TO template_saas_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA goals GRANT ALL ON SEQUENCES TO template_saas_user; diff --git a/ddl/schemas/goals/01-enums.sql b/ddl/schemas/goals/01-enums.sql new file mode 100644 index 0000000..7b4644a --- /dev/null +++ b/ddl/schemas/goals/01-enums.sql @@ -0,0 +1,70 @@ +-- ============================================ +-- SAAS-022: Goals Enums +-- ============================================ + +-- Tipo de meta +CREATE TYPE goals.goal_type AS ENUM ( + 'target', -- Alcanzar un objetivo + 'limit', -- No exceder un limite + 'maintain' -- Mantener un valor +); + +-- Tipo de metrica +CREATE TYPE goals.metric_type AS ENUM ( + 'number', -- Numero entero + 'currency', -- Monto monetario + 'percentage', -- Porcentaje + 'boolean', -- Si/No + 'count' -- Conteo +); + +-- Tipo de periodo +CREATE TYPE goals.period_type AS ENUM ( + 'daily', + 'weekly', + 'monthly', + 'quarterly', + 'yearly', + 'custom' +); + +-- Fuente de datos para tracking automatico +CREATE TYPE goals.data_source AS ENUM ( + 'manual', -- Actualizado manualmente + 'sales', -- Desde modulo Sales + 'billing', -- Desde modulo Billing + 'commissions', -- Desde modulo Commissions + 'custom' -- Fuente personalizada +); + +-- Estado de la meta +CREATE TYPE goals.goal_status AS ENUM ( + 'draft', -- Borrador + 'active', -- Activa + 'paused', -- Pausada + 'completed', -- Completada + 'cancelled' -- Cancelada +); + +-- Tipo de asignado +CREATE TYPE goals.assignee_type AS ENUM ( + 'user', -- Usuario individual + 'team', -- Equipo + 'tenant' -- Todo el tenant +); + +-- Estado de asignacion +CREATE TYPE goals.assignment_status AS ENUM ( + 'active', -- En progreso + 'achieved', -- Lograda + 'failed', -- No alcanzada + 'cancelled' -- Cancelada +); + +-- Fuente del progreso +CREATE TYPE goals.progress_source AS ENUM ( + 'manual', -- Entrada manual + 'automatic', -- Tracking automatico + 'import', -- Importacion + 'api' -- Via API externa +); diff --git a/ddl/schemas/goals/02-tables.sql b/ddl/schemas/goals/02-tables.sql new file mode 100644 index 0000000..3e5d8ef --- /dev/null +++ b/ddl/schemas/goals/02-tables.sql @@ -0,0 +1,155 @@ +-- ============================================ +-- SAAS-022: Goals Tables +-- ============================================ + +-- ───────────────────────────────────────────── +-- Definiciones de metas +-- ───────────────────────────────────────────── +CREATE TABLE goals.definitions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, + + -- Info basica + name VARCHAR(200) NOT NULL, + description TEXT, + category VARCHAR(100), + + -- Tipo de meta + type goals.goal_type NOT NULL DEFAULT 'target', + metric goals.metric_type NOT NULL DEFAULT 'number', + + -- Objetivo + target_value DECIMAL(15,2) NOT NULL, + unit VARCHAR(50), -- 'USD', 'units', '%', etc. + + -- Periodo + period goals.period_type NOT NULL DEFAULT 'monthly', + starts_at DATE NOT NULL, + ends_at DATE NOT NULL, + + -- Fuente de datos (para tracking automatico) + source goals.data_source NOT NULL DEFAULT 'manual', + source_config JSONB DEFAULT '{}', + -- Ejemplo: + -- { + -- "module": "sales", + -- "entity": "opportunities", + -- "filter": { "status": "won" }, + -- "aggregation": "sum", + -- "field": "amount" + -- } + + -- Hitos para notificaciones + milestones JSONB DEFAULT '[]', + -- Ejemplo: [{ "percentage": 25, "notify": true }, { "percentage": 50, "notify": true }] + + -- Estado + status goals.goal_status NOT NULL DEFAULT 'draft', + + -- Metadata + tags JSONB DEFAULT '[]', + + -- Timestamps + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES users.users(id), + + -- Constraints + CONSTRAINT valid_date_range CHECK (ends_at >= starts_at), + CONSTRAINT positive_target CHECK (target_value > 0) +); + +-- ───────────────────────────────────────────── +-- Asignaciones de metas +-- ───────────────────────────────────────────── +CREATE TABLE goals.assignments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, + definition_id UUID NOT NULL REFERENCES goals.definitions(id) ON DELETE CASCADE, + + -- Asignado a + assignee_type goals.assignee_type NOT NULL DEFAULT 'user', + user_id UUID REFERENCES users.users(id) ON DELETE CASCADE, + team_id UUID, -- Para uso futuro con modulo de equipos + + -- Objetivo personalizado (override del definition) + custom_target DECIMAL(15,2), + + -- Progreso + current_value DECIMAL(15,2) NOT NULL DEFAULT 0, + progress_percentage DECIMAL(5,2) NOT NULL DEFAULT 0, + last_updated_at TIMESTAMPTZ, + + -- Estado + status goals.assignment_status NOT NULL DEFAULT 'active', + achieved_at TIMESTAMPTZ, + + -- Metadata + notes TEXT, + + -- Timestamps + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Constraints + CONSTRAINT valid_progress CHECK (progress_percentage >= 0 AND progress_percentage <= 100), + CONSTRAINT valid_assignee CHECK ( + (assignee_type = 'user' AND user_id IS NOT NULL) OR + (assignee_type = 'team' AND team_id IS NOT NULL) OR + (assignee_type = 'tenant') + ) +); + +-- ───────────────────────────────────────────── +-- Historial de progreso +-- ───────────────────────────────────────────── +CREATE TABLE goals.progress_log ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, + assignment_id UUID NOT NULL REFERENCES goals.assignments(id) ON DELETE CASCADE, + + -- Valores + previous_value DECIMAL(15,2), + new_value DECIMAL(15,2) NOT NULL, + change_amount DECIMAL(15,2), + + -- Fuente del cambio + source goals.progress_source NOT NULL DEFAULT 'manual', + source_reference VARCHAR(200), -- ID de la transaccion origen + + notes TEXT, + + -- Timestamps + logged_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + logged_by UUID REFERENCES users.users(id) +); + +-- ───────────────────────────────────────────── +-- Notificaciones de hitos alcanzados +-- ───────────────────────────────────────────── +CREATE TABLE goals.milestone_notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, + assignment_id UUID NOT NULL REFERENCES goals.assignments(id) ON DELETE CASCADE, + + milestone_percentage INTEGER NOT NULL, + achieved_value DECIMAL(15,2) NOT NULL, + + notified_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Evitar duplicados + CONSTRAINT unique_milestone_notification UNIQUE (assignment_id, milestone_percentage) +); + +-- ───────────────────────────────────────────── +-- Triggers para updated_at +-- ───────────────────────────────────────────── +CREATE TRIGGER update_definitions_updated_at + BEFORE UPDATE ON goals.definitions + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_assignments_updated_at + BEFORE UPDATE ON goals.assignments + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); diff --git a/ddl/schemas/goals/04-rls.sql b/ddl/schemas/goals/04-rls.sql new file mode 100644 index 0000000..8c9ae89 --- /dev/null +++ b/ddl/schemas/goals/04-rls.sql @@ -0,0 +1,72 @@ +-- ============================================ +-- SAAS-022: Goals RLS Policies +-- Tenant isolation for all tables +-- ============================================ + +-- ───────────────────────────────────────────── +-- Definitions RLS +-- ───────────────────────────────────────────── +ALTER TABLE goals.definitions ENABLE ROW LEVEL SECURITY; + +CREATE POLICY definitions_tenant_isolation_select ON goals.definitions + FOR SELECT USING (tenant_id = auth.get_current_tenant()); + +CREATE POLICY definitions_tenant_isolation_insert ON goals.definitions + FOR INSERT WITH CHECK (tenant_id = auth.get_current_tenant()); + +CREATE POLICY definitions_tenant_isolation_update ON goals.definitions + FOR UPDATE USING (tenant_id = auth.get_current_tenant()); + +CREATE POLICY definitions_tenant_isolation_delete ON goals.definitions + FOR DELETE USING (tenant_id = auth.get_current_tenant()); + +-- ───────────────────────────────────────────── +-- Assignments RLS +-- ───────────────────────────────────────────── +ALTER TABLE goals.assignments ENABLE ROW LEVEL SECURITY; + +CREATE POLICY assignments_tenant_isolation_select ON goals.assignments + FOR SELECT USING (tenant_id = auth.get_current_tenant()); + +CREATE POLICY assignments_tenant_isolation_insert ON goals.assignments + FOR INSERT WITH CHECK (tenant_id = auth.get_current_tenant()); + +CREATE POLICY assignments_tenant_isolation_update ON goals.assignments + FOR UPDATE USING (tenant_id = auth.get_current_tenant()); + +CREATE POLICY assignments_tenant_isolation_delete ON goals.assignments + FOR DELETE USING (tenant_id = auth.get_current_tenant()); + +-- ───────────────────────────────────────────── +-- Progress Log RLS +-- ───────────────────────────────────────────── +ALTER TABLE goals.progress_log ENABLE ROW LEVEL SECURITY; + +CREATE POLICY progress_log_tenant_isolation_select ON goals.progress_log + FOR SELECT USING (tenant_id = auth.get_current_tenant()); + +CREATE POLICY progress_log_tenant_isolation_insert ON goals.progress_log + FOR INSERT WITH CHECK (tenant_id = auth.get_current_tenant()); + +CREATE POLICY progress_log_tenant_isolation_update ON goals.progress_log + FOR UPDATE USING (tenant_id = auth.get_current_tenant()); + +CREATE POLICY progress_log_tenant_isolation_delete ON goals.progress_log + FOR DELETE USING (tenant_id = auth.get_current_tenant()); + +-- ───────────────────────────────────────────── +-- Milestone Notifications RLS +-- ───────────────────────────────────────────── +ALTER TABLE goals.milestone_notifications ENABLE ROW LEVEL SECURITY; + +CREATE POLICY milestone_notifications_tenant_isolation_select ON goals.milestone_notifications + FOR SELECT USING (tenant_id = auth.get_current_tenant()); + +CREATE POLICY milestone_notifications_tenant_isolation_insert ON goals.milestone_notifications + FOR INSERT WITH CHECK (tenant_id = auth.get_current_tenant()); + +CREATE POLICY milestone_notifications_tenant_isolation_update ON goals.milestone_notifications + FOR UPDATE USING (tenant_id = auth.get_current_tenant()); + +CREATE POLICY milestone_notifications_tenant_isolation_delete ON goals.milestone_notifications + FOR DELETE USING (tenant_id = auth.get_current_tenant()); diff --git a/ddl/schemas/goals/05-indexes.sql b/ddl/schemas/goals/05-indexes.sql new file mode 100644 index 0000000..13bebf8 --- /dev/null +++ b/ddl/schemas/goals/05-indexes.sql @@ -0,0 +1,109 @@ +-- ============================================ +-- SAAS-022: Goals Indexes +-- Performance optimization +-- ============================================ + +-- ───────────────────────────────────────────── +-- Definitions Indexes +-- ───────────────────────────────────────────── + +-- Tenant + status (most common query) +CREATE INDEX idx_definitions_tenant_status + ON goals.definitions (tenant_id, status); + +-- Period queries +CREATE INDEX idx_definitions_tenant_period + ON goals.definitions (tenant_id, period, starts_at, ends_at); + +-- Date range queries +CREATE INDEX idx_definitions_dates + ON goals.definitions (starts_at, ends_at); + +-- Category filtering +CREATE INDEX idx_definitions_category + ON goals.definitions (tenant_id, category) + WHERE category IS NOT NULL; + +-- Active goals (partial index) +CREATE INDEX idx_definitions_active + ON goals.definitions (tenant_id, starts_at, ends_at) + WHERE status = 'active'; + +-- Created by user +CREATE INDEX idx_definitions_created_by + ON goals.definitions (created_by) + WHERE created_by IS NOT NULL; + +-- Tags (GIN for JSONB array) +CREATE INDEX idx_definitions_tags + ON goals.definitions USING GIN (tags); + +-- ───────────────────────────────────────────── +-- Assignments Indexes +-- ───────────────────────────────────────────── + +-- Tenant + status (most common query) +CREATE INDEX idx_assignments_tenant_status + ON goals.assignments (tenant_id, status); + +-- Definition lookup +CREATE INDEX idx_assignments_definition + ON goals.assignments (definition_id); + +-- User assignments (most common filter) +CREATE INDEX idx_assignments_user + ON goals.assignments (user_id, status) + WHERE user_id IS NOT NULL; + +-- Team assignments +CREATE INDEX idx_assignments_team + ON goals.assignments (team_id, status) + WHERE team_id IS NOT NULL; + +-- Active assignments (partial index) +CREATE INDEX idx_assignments_active + ON goals.assignments (tenant_id, user_id) + WHERE status = 'active'; + +-- Progress percentage (for reports) +CREATE INDEX idx_assignments_progress + ON goals.assignments (tenant_id, progress_percentage); + +-- Achieved goals +CREATE INDEX idx_assignments_achieved + ON goals.assignments (achieved_at) + WHERE status = 'achieved'; + +-- ───────────────────────────────────────────── +-- Progress Log Indexes +-- ───────────────────────────────────────────── + +-- Assignment history +CREATE INDEX idx_progress_log_assignment + ON goals.progress_log (assignment_id, logged_at DESC); + +-- Tenant + date range +CREATE INDEX idx_progress_log_tenant_date + ON goals.progress_log (tenant_id, logged_at); + +-- Source reference (for deduplication) +CREATE INDEX idx_progress_log_source_ref + ON goals.progress_log (source_reference) + WHERE source_reference IS NOT NULL; + +-- By user who logged +CREATE INDEX idx_progress_log_logged_by + ON goals.progress_log (logged_by) + WHERE logged_by IS NOT NULL; + +-- ───────────────────────────────────────────── +-- Milestone Notifications Indexes +-- ───────────────────────────────────────────── + +-- Assignment milestones +CREATE INDEX idx_milestone_notifications_assignment + ON goals.milestone_notifications (assignment_id); + +-- Tenant notifications +CREATE INDEX idx_milestone_notifications_tenant + ON goals.milestone_notifications (tenant_id, notified_at);