feat: Implement Phase 1 & 2 - Full monorepo architecture

## Backend API (apps/api)
- Express.js server with TypeScript
- JWT authentication with access/refresh tokens
- Multi-tenant middleware (schema per tenant)
- Complete CRUD routes: auth, cfdis, transactions, contacts, categories, metrics, alerts
- SAT integration: CFDI 4.0 XML parser, FIEL authentication
- Metrics engine: 50+ financial metrics (Core, Startup, Enterprise)
- Rate limiting, CORS, Helmet security

## Frontend Web (apps/web)
- Next.js 14 with App Router
- Authentication pages: login, register, forgot-password
- Dashboard layout with Sidebar and Header
- Dashboard pages: overview, cash-flow, revenue, expenses, metrics
- Zustand stores for auth and UI state
- Theme support with flash prevention

## Database Package (packages/database)
- PostgreSQL migrations with multi-tenant architecture
- Public schema: plans, tenants, users, sessions, subscriptions
- Tenant schema: sat_credentials, cfdis, transactions, contacts, accounts, alerts
- Tenant management functions
- Seed data for plans and super admin

## Shared Package (packages/shared)
- TypeScript types: auth, tenant, financial, metrics, reports
- Zod validation schemas for all entities
- Utility functions for formatting

## UI Package (packages/ui)
- Chart components: LineChart, BarChart, AreaChart, PieChart
- Data components: DataTable, MetricCard, KPICard, AlertBadge
- PeriodSelector and Skeleton components

## Infrastructure
- Docker Compose: PostgreSQL 15, Redis 7, MinIO, Mailhog
- Makefile with 25+ development commands
- Development scripts: dev-setup.sh, dev-down.sh
- Complete .env.example template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 11:05:24 +00:00
parent c1321c3f0c
commit a9b1994c48
110 changed files with 40788 additions and 0 deletions

View File

@@ -0,0 +1,658 @@
-- ============================================================================
-- Horux Strategy - Public Schema Migration
-- Version: 001
-- Description: Core tables for multi-tenant SaaS platform
-- ============================================================================
-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- For text search
-- ============================================================================
-- ENUM TYPES
-- ============================================================================
-- User roles enum
CREATE TYPE user_role AS ENUM (
'super_admin', -- Platform administrator (Horux team)
'owner', -- Tenant owner (company owner)
'admin', -- Tenant administrator
'manager', -- Department manager
'analyst', -- Financial analyst (read + limited write)
'viewer' -- Read-only access
);
-- Subscription status enum
CREATE TYPE subscription_status AS ENUM (
'active',
'trial',
'past_due',
'cancelled',
'suspended',
'expired'
);
-- Tenant status enum
CREATE TYPE tenant_status AS ENUM (
'active',
'suspended',
'pending',
'deleted'
);
-- Job status enum
CREATE TYPE job_status AS ENUM (
'pending',
'running',
'completed',
'failed',
'cancelled'
);
-- ============================================================================
-- PLANS TABLE
-- Subscription plans available in the platform
-- ============================================================================
CREATE TABLE plans (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
-- Pricing (in MXN cents to avoid floating point issues)
price_monthly_cents INTEGER NOT NULL DEFAULT 0,
price_yearly_cents INTEGER NOT NULL DEFAULT 0,
-- Limits
max_users INTEGER NOT NULL DEFAULT 1,
max_cfdis_monthly INTEGER NOT NULL DEFAULT 100,
max_storage_mb INTEGER NOT NULL DEFAULT 1024,
max_api_calls_daily INTEGER NOT NULL DEFAULT 1000,
max_reports_monthly INTEGER NOT NULL DEFAULT 10,
-- Features (JSON for flexibility)
features JSONB NOT NULL DEFAULT '{}',
-- Feature flags
has_sat_sync BOOLEAN NOT NULL DEFAULT false,
has_bank_sync BOOLEAN NOT NULL DEFAULT false,
has_ai_insights BOOLEAN NOT NULL DEFAULT false,
has_custom_reports BOOLEAN NOT NULL DEFAULT false,
has_api_access BOOLEAN NOT NULL DEFAULT false,
has_white_label BOOLEAN NOT NULL DEFAULT false,
has_priority_support BOOLEAN NOT NULL DEFAULT false,
has_dedicated_account_manager BOOLEAN NOT NULL DEFAULT false,
-- Retention
data_retention_months INTEGER NOT NULL DEFAULT 12,
-- Display
display_order INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT true,
is_popular BOOLEAN NOT NULL DEFAULT false,
-- Metadata
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Create index for active plans
CREATE INDEX idx_plans_active ON plans(is_active, display_order);
-- ============================================================================
-- TENANTS TABLE
-- Companies/organizations using the platform
-- ============================================================================
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Basic info
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL UNIQUE,
schema_name VARCHAR(100) NOT NULL UNIQUE,
-- Tax info (Mexican RFC)
rfc VARCHAR(13),
razon_social VARCHAR(500),
-- Contact
email VARCHAR(255),
phone VARCHAR(50),
-- Address
address_street VARCHAR(500),
address_city VARCHAR(100),
address_state VARCHAR(100),
address_zip VARCHAR(10),
address_country VARCHAR(2) DEFAULT 'MX',
-- Branding
logo_url VARCHAR(500),
primary_color VARCHAR(7),
-- Owner and plan
owner_id UUID NOT NULL,
plan_id VARCHAR(50) NOT NULL REFERENCES plans(id),
-- Status
status tenant_status NOT NULL DEFAULT 'pending',
-- Settings (JSON for flexibility)
settings JSONB NOT NULL DEFAULT '{}',
-- Metadata
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP WITH TIME ZONE
);
-- Indexes for tenants
CREATE INDEX idx_tenants_slug ON tenants(slug);
CREATE INDEX idx_tenants_rfc ON tenants(rfc) WHERE rfc IS NOT NULL;
CREATE INDEX idx_tenants_owner ON tenants(owner_id);
CREATE INDEX idx_tenants_plan ON tenants(plan_id);
CREATE INDEX idx_tenants_status ON tenants(status);
CREATE INDEX idx_tenants_created ON tenants(created_at);
-- ============================================================================
-- USERS TABLE
-- Platform users (can belong to multiple tenants via user_tenants)
-- ============================================================================
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Authentication
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255),
-- Profile
first_name VARCHAR(100),
last_name VARCHAR(100),
phone VARCHAR(50),
avatar_url VARCHAR(500),
-- Default role (for super_admin only)
default_role user_role NOT NULL DEFAULT 'viewer',
-- Status
is_active BOOLEAN NOT NULL DEFAULT true,
is_email_verified BOOLEAN NOT NULL DEFAULT false,
email_verified_at TIMESTAMP WITH TIME ZONE,
-- Security
failed_login_attempts INTEGER NOT NULL DEFAULT 0,
locked_until TIMESTAMP WITH TIME ZONE,
password_changed_at TIMESTAMP WITH TIME ZONE,
must_change_password BOOLEAN NOT NULL DEFAULT false,
-- Two-factor authentication
two_factor_enabled BOOLEAN NOT NULL DEFAULT false,
two_factor_secret VARCHAR(255),
two_factor_recovery_codes TEXT[],
-- Preferences
preferences JSONB NOT NULL DEFAULT '{}',
timezone VARCHAR(50) DEFAULT 'America/Mexico_City',
locale VARCHAR(10) DEFAULT 'es-MX',
-- Metadata
last_login_at TIMESTAMP WITH TIME ZONE,
last_login_ip INET,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Indexes for users
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_active ON users(is_active);
CREATE INDEX idx_users_default_role ON users(default_role) WHERE default_role = 'super_admin';
CREATE INDEX idx_users_last_login ON users(last_login_at);
-- ============================================================================
-- USER_TENANTS TABLE
-- Association between users and tenants with role
-- ============================================================================
CREATE TABLE user_tenants (
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
-- Role within this tenant
role user_role NOT NULL DEFAULT 'viewer',
-- Status
is_active BOOLEAN NOT NULL DEFAULT true,
-- Invitation
invited_by UUID REFERENCES users(id),
invited_at TIMESTAMP WITH TIME ZONE,
accepted_at TIMESTAMP WITH TIME ZONE,
-- Metadata
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, tenant_id)
);
-- Indexes for user_tenants
CREATE INDEX idx_user_tenants_tenant ON user_tenants(tenant_id);
CREATE INDEX idx_user_tenants_user ON user_tenants(user_id);
CREATE INDEX idx_user_tenants_role ON user_tenants(role);
-- ============================================================================
-- USER_SESSIONS TABLE
-- Active user sessions for authentication
-- ============================================================================
CREATE TABLE user_sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
-- Session token (hashed)
token_hash VARCHAR(255) NOT NULL UNIQUE,
refresh_token_hash VARCHAR(255),
-- Device info
user_agent TEXT,
ip_address INET,
device_type VARCHAR(50),
device_name VARCHAR(255),
-- Location (approximate)
location_city VARCHAR(100),
location_country VARCHAR(2),
-- Status
is_active BOOLEAN NOT NULL DEFAULT true,
-- Timestamps
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
refresh_expires_at TIMESTAMP WITH TIME ZONE,
last_activity_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
revoked_at TIMESTAMP WITH TIME ZONE
);
-- Indexes for user_sessions
CREATE INDEX idx_user_sessions_user ON user_sessions(user_id);
CREATE INDEX idx_user_sessions_tenant ON user_sessions(tenant_id);
CREATE INDEX idx_user_sessions_token ON user_sessions(token_hash);
CREATE INDEX idx_user_sessions_expires ON user_sessions(expires_at) WHERE is_active = true;
CREATE INDEX idx_user_sessions_active ON user_sessions(user_id, is_active) WHERE is_active = true;
-- ============================================================================
-- SUBSCRIPTIONS TABLE
-- Tenant subscription management
-- ============================================================================
CREATE TABLE subscriptions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
plan_id VARCHAR(50) NOT NULL REFERENCES plans(id),
-- Status
status subscription_status NOT NULL DEFAULT 'trial',
-- Billing cycle
billing_cycle VARCHAR(20) NOT NULL DEFAULT 'monthly', -- monthly, yearly
-- Dates
trial_ends_at TIMESTAMP WITH TIME ZONE,
current_period_start TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
current_period_end TIMESTAMP WITH TIME ZONE NOT NULL,
cancelled_at TIMESTAMP WITH TIME ZONE,
cancel_at_period_end BOOLEAN NOT NULL DEFAULT false,
-- Payment info (Stripe or other processor)
payment_processor VARCHAR(50),
external_subscription_id VARCHAR(255),
external_customer_id VARCHAR(255),
-- Pricing at subscription time
price_cents INTEGER NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'MXN',
-- Usage tracking
usage_cfdis_current INTEGER NOT NULL DEFAULT 0,
usage_storage_mb_current DECIMAL(10,2) NOT NULL DEFAULT 0,
usage_api_calls_current INTEGER NOT NULL DEFAULT 0,
usage_reset_at TIMESTAMP WITH TIME ZONE,
-- Metadata
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Indexes for subscriptions
CREATE INDEX idx_subscriptions_tenant ON subscriptions(tenant_id);
CREATE INDEX idx_subscriptions_plan ON subscriptions(plan_id);
CREATE INDEX idx_subscriptions_status ON subscriptions(status);
CREATE INDEX idx_subscriptions_period_end ON subscriptions(current_period_end);
CREATE INDEX idx_subscriptions_external ON subscriptions(external_subscription_id) WHERE external_subscription_id IS NOT NULL;
-- ============================================================================
-- AUDIT_LOG TABLE
-- Comprehensive audit trail for compliance
-- ============================================================================
CREATE TABLE audit_log (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Context
tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL,
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
-- Action info
action VARCHAR(100) NOT NULL,
entity_type VARCHAR(100) NOT NULL,
entity_id VARCHAR(255),
-- Change tracking
old_values JSONB,
new_values JSONB,
details JSONB,
-- Request context
ip_address INET,
user_agent TEXT,
request_id VARCHAR(100),
-- Metadata
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Indexes for audit_log (optimized for queries)
CREATE INDEX idx_audit_log_tenant ON audit_log(tenant_id, created_at DESC);
CREATE INDEX idx_audit_log_user ON audit_log(user_id, created_at DESC);
CREATE INDEX idx_audit_log_action ON audit_log(action, created_at DESC);
CREATE INDEX idx_audit_log_entity ON audit_log(entity_type, entity_id, created_at DESC);
CREATE INDEX idx_audit_log_created ON audit_log(created_at DESC);
-- Partition audit_log by month for better performance (optional, can be enabled later)
-- This is a placeholder comment - actual partitioning would require more setup
-- ============================================================================
-- BACKGROUND_JOBS TABLE
-- Async job queue for long-running tasks
-- ============================================================================
CREATE TABLE background_jobs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Context
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
-- Job info
job_type VARCHAR(100) NOT NULL,
job_name VARCHAR(255),
queue VARCHAR(50) NOT NULL DEFAULT 'default',
priority INTEGER NOT NULL DEFAULT 0,
-- Payload
payload JSONB NOT NULL DEFAULT '{}',
-- Status
status job_status NOT NULL DEFAULT 'pending',
progress INTEGER DEFAULT 0, -- 0-100
-- Results
result JSONB,
error_message TEXT,
error_stack TEXT,
-- Retry logic
attempts INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 3,
-- Scheduling
scheduled_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
started_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
-- Timeout
timeout_seconds INTEGER DEFAULT 3600, -- 1 hour default
-- Metadata
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Indexes for background_jobs
CREATE INDEX idx_background_jobs_tenant ON background_jobs(tenant_id);
CREATE INDEX idx_background_jobs_status ON background_jobs(status, scheduled_at) WHERE status IN ('pending', 'running');
CREATE INDEX idx_background_jobs_queue ON background_jobs(queue, priority DESC, scheduled_at) WHERE status = 'pending';
CREATE INDEX idx_background_jobs_type ON background_jobs(job_type, status);
CREATE INDEX idx_background_jobs_scheduled ON background_jobs(scheduled_at) WHERE status = 'pending';
-- ============================================================================
-- API_KEYS TABLE
-- API keys for external integrations
-- ============================================================================
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
-- Key info
name VARCHAR(255) NOT NULL,
description TEXT,
-- The key (only prefix stored for display, full hash for verification)
key_prefix VARCHAR(10) NOT NULL, -- First 8 chars for identification
key_hash VARCHAR(255) NOT NULL UNIQUE, -- SHA-256 hash of full key
-- Permissions (scopes)
scopes TEXT[] NOT NULL DEFAULT '{}',
-- Restrictions
allowed_ips INET[],
allowed_origins TEXT[],
-- Rate limiting
rate_limit_per_minute INTEGER DEFAULT 60,
rate_limit_per_day INTEGER DEFAULT 10000,
-- Usage tracking
last_used_at TIMESTAMP WITH TIME ZONE,
usage_count BIGINT NOT NULL DEFAULT 0,
-- Status
is_active BOOLEAN NOT NULL DEFAULT true,
-- Expiration
expires_at TIMESTAMP WITH TIME ZONE,
-- Metadata
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
revoked_at TIMESTAMP WITH TIME ZONE,
revoked_by UUID REFERENCES users(id) ON DELETE SET NULL
);
-- Indexes for api_keys
CREATE INDEX idx_api_keys_tenant ON api_keys(tenant_id);
CREATE INDEX idx_api_keys_hash ON api_keys(key_hash) WHERE is_active = true;
CREATE INDEX idx_api_keys_prefix ON api_keys(key_prefix);
CREATE INDEX idx_api_keys_active ON api_keys(tenant_id, is_active) WHERE is_active = true;
-- ============================================================================
-- PASSWORD_RESET_TOKENS TABLE
-- Tokens for password reset functionality
-- ============================================================================
CREATE TABLE password_reset_tokens (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(255) NOT NULL UNIQUE,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
used_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Index for password_reset_tokens
CREATE INDEX idx_password_reset_tokens_user ON password_reset_tokens(user_id);
CREATE INDEX idx_password_reset_tokens_token ON password_reset_tokens(token_hash) WHERE used_at IS NULL;
-- ============================================================================
-- EMAIL_VERIFICATION_TOKENS TABLE
-- Tokens for email verification
-- ============================================================================
CREATE TABLE email_verification_tokens (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
email VARCHAR(255) NOT NULL,
token_hash VARCHAR(255) NOT NULL UNIQUE,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
verified_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Index for email_verification_tokens
CREATE INDEX idx_email_verification_tokens_user ON email_verification_tokens(user_id);
CREATE INDEX idx_email_verification_tokens_token ON email_verification_tokens(token_hash) WHERE verified_at IS NULL;
-- ============================================================================
-- INVITATION_TOKENS TABLE
-- Tokens for user invitations to tenants
-- ============================================================================
CREATE TABLE invitation_tokens (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
email VARCHAR(255) NOT NULL,
role user_role NOT NULL DEFAULT 'viewer',
token_hash VARCHAR(255) NOT NULL UNIQUE,
invited_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
accepted_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Index for invitation_tokens
CREATE INDEX idx_invitation_tokens_tenant ON invitation_tokens(tenant_id);
CREATE INDEX idx_invitation_tokens_email ON invitation_tokens(email);
CREATE INDEX idx_invitation_tokens_token ON invitation_tokens(token_hash) WHERE accepted_at IS NULL;
-- ============================================================================
-- SYSTEM_SETTINGS TABLE
-- Global platform settings
-- ============================================================================
CREATE TABLE system_settings (
key VARCHAR(100) PRIMARY KEY,
value TEXT NOT NULL,
value_type VARCHAR(20) NOT NULL DEFAULT 'string', -- string, integer, boolean, json
category VARCHAR(50) NOT NULL DEFAULT 'general',
description TEXT,
is_sensitive BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- ============================================================================
-- NOTIFICATIONS TABLE
-- System and user notifications
-- ============================================================================
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
-- Notification content
type VARCHAR(50) NOT NULL,
title VARCHAR(255) NOT NULL,
message TEXT,
data JSONB,
-- Action
action_url VARCHAR(500),
action_label VARCHAR(100),
-- Status
is_read BOOLEAN NOT NULL DEFAULT false,
read_at TIMESTAMP WITH TIME ZONE,
-- Delivery
channels TEXT[] NOT NULL DEFAULT '{"in_app"}', -- in_app, email, push
email_sent_at TIMESTAMP WITH TIME ZONE,
push_sent_at TIMESTAMP WITH TIME ZONE,
-- Metadata
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE
);
-- Indexes for notifications
CREATE INDEX idx_notifications_user ON notifications(user_id, created_at DESC);
CREATE INDEX idx_notifications_unread ON notifications(user_id, is_read, created_at DESC) WHERE is_read = false;
CREATE INDEX idx_notifications_tenant ON notifications(tenant_id, created_at DESC);
-- ============================================================================
-- FUNCTIONS AND TRIGGERS
-- ============================================================================
-- Function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Apply update_updated_at trigger to all relevant tables
CREATE TRIGGER update_plans_updated_at BEFORE UPDATE ON plans
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_tenants_updated_at BEFORE UPDATE ON tenants
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_user_tenants_updated_at BEFORE UPDATE ON user_tenants
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_subscriptions_updated_at BEFORE UPDATE ON subscriptions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_background_jobs_updated_at BEFORE UPDATE ON background_jobs
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_api_keys_updated_at BEFORE UPDATE ON api_keys
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_system_settings_updated_at BEFORE UPDATE ON system_settings
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================================
-- ROW LEVEL SECURITY (RLS) POLICIES
-- Enable RLS for multi-tenant security
-- ============================================================================
-- Note: RLS policies would be configured here in production
-- For now, security is handled at the application layer with schema-based isolation
-- ============================================================================
-- COMMENTS FOR DOCUMENTATION
-- ============================================================================
COMMENT ON TABLE plans IS 'Subscription plans available in the platform';
COMMENT ON TABLE tenants IS 'Companies/organizations using the platform (each gets their own schema)';
COMMENT ON TABLE users IS 'Platform users (authentication and profile)';
COMMENT ON TABLE user_tenants IS 'Many-to-many relationship between users and tenants with role';
COMMENT ON TABLE user_sessions IS 'Active user sessions for JWT-based authentication';
COMMENT ON TABLE subscriptions IS 'Tenant subscription and billing information';
COMMENT ON TABLE audit_log IS 'Comprehensive audit trail for security and compliance';
COMMENT ON TABLE background_jobs IS 'Async job queue for long-running tasks (SAT sync, reports, etc.)';
COMMENT ON TABLE api_keys IS 'API keys for external integrations and third-party access';
COMMENT ON TABLE notifications IS 'In-app and push notifications for users';

View File

@@ -0,0 +1,889 @@
-- ============================================================================
-- Horux Strategy - Tenant Schema Template
-- Version: 002
-- Description: Tables created for each tenant in their own schema
-- Note: ${SCHEMA_NAME} will be replaced with the actual schema name
-- ============================================================================
-- ============================================================================
-- ENUM TYPES (tenant-specific)
-- ============================================================================
-- Transaction type
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'transaction_type' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema())) THEN
CREATE TYPE transaction_type AS ENUM ('income', 'expense', 'transfer', 'adjustment');
END IF;
END$$;
-- Transaction status
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'transaction_status' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema())) THEN
CREATE TYPE transaction_status AS ENUM ('pending', 'confirmed', 'reconciled', 'voided');
END IF;
END$$;
-- CFDI status
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cfdi_status' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema())) THEN
CREATE TYPE cfdi_status AS ENUM ('active', 'cancelled', 'pending_cancellation');
END IF;
END$$;
-- CFDI type (comprobante)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cfdi_type' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema())) THEN
CREATE TYPE cfdi_type AS ENUM ('I', 'E', 'T', 'N', 'P'); -- Ingreso, Egreso, Traslado, Nomina, Pago
END IF;
END$$;
-- Contact type
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'contact_type' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema())) THEN
CREATE TYPE contact_type AS ENUM ('customer', 'supplier', 'both', 'employee');
END IF;
END$$;
-- Category type
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'category_type' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema())) THEN
CREATE TYPE category_type AS ENUM ('income', 'expense', 'cost', 'other');
END IF;
END$$;
-- Account type
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'account_type' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema())) THEN
CREATE TYPE account_type AS ENUM ('asset', 'liability', 'equity', 'revenue', 'expense');
END IF;
END$$;
-- Alert severity
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'alert_severity' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema())) THEN
CREATE TYPE alert_severity AS ENUM ('info', 'warning', 'critical');
END IF;
END$$;
-- Report status
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'report_status' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema())) THEN
CREATE TYPE report_status AS ENUM ('draft', 'generating', 'completed', 'failed', 'archived');
END IF;
END$$;
-- ============================================================================
-- SAT_CREDENTIALS TABLE
-- Encrypted FIEL (e.firma) credentials for SAT integration
-- ============================================================================
CREATE TABLE sat_credentials (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- RFC associated with credentials
rfc VARCHAR(13) NOT NULL UNIQUE,
-- FIEL Components (encrypted with AES-256)
-- The actual encryption key is stored securely in environment variables
cer_file_encrypted BYTEA NOT NULL, -- .cer file content (encrypted)
key_file_encrypted BYTEA NOT NULL, -- .key file content (encrypted)
password_encrypted BYTEA NOT NULL, -- FIEL password (encrypted)
-- Certificate metadata
cer_serial_number VARCHAR(50),
cer_issued_at TIMESTAMP WITH TIME ZONE,
cer_expires_at TIMESTAMP WITH TIME ZONE,
cer_issuer VARCHAR(255),
-- CIEC credentials (optional, for portal access)
ciec_password_encrypted BYTEA,
-- Status
is_active BOOLEAN NOT NULL DEFAULT true,
is_valid BOOLEAN NOT NULL DEFAULT false, -- Set after validation
last_validated_at TIMESTAMP WITH TIME ZONE,
validation_error TEXT,
-- SAT sync settings
sync_enabled BOOLEAN NOT NULL DEFAULT false,
sync_frequency_hours INTEGER DEFAULT 24,
last_sync_at TIMESTAMP WITH TIME ZONE,
last_sync_status VARCHAR(50),
last_sync_error TEXT,
-- Encryption metadata
encryption_version INTEGER NOT NULL DEFAULT 1,
encryption_iv BYTEA, -- Initialization vector
-- Metadata
created_by UUID NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Indexes for sat_credentials
CREATE INDEX idx_sat_credentials_rfc ON sat_credentials(rfc);
CREATE INDEX idx_sat_credentials_active ON sat_credentials(is_active, is_valid);
CREATE INDEX idx_sat_credentials_sync ON sat_credentials(sync_enabled, last_sync_at) WHERE sync_enabled = true;
-- ============================================================================
-- CFDIS TABLE
-- CFDI 4.0 compliant invoice storage
-- ============================================================================
CREATE TABLE cfdis (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- CFDI Identifiers
uuid_fiscal UUID NOT NULL UNIQUE, -- UUID from SAT (timbre fiscal)
serie VARCHAR(25),
folio VARCHAR(40),
-- Type and status
tipo_comprobante cfdi_type NOT NULL, -- I, E, T, N, P
status cfdi_status NOT NULL DEFAULT 'active',
-- Dates
fecha_emision TIMESTAMP WITH TIME ZONE NOT NULL,
fecha_timbrado TIMESTAMP WITH TIME ZONE,
fecha_cancelacion TIMESTAMP WITH TIME ZONE,
-- Emisor (seller)
emisor_rfc VARCHAR(13) NOT NULL,
emisor_nombre VARCHAR(500) NOT NULL,
emisor_regimen_fiscal VARCHAR(3) NOT NULL, -- SAT catalog code
-- Receptor (buyer)
receptor_rfc VARCHAR(13) NOT NULL,
receptor_nombre VARCHAR(500) NOT NULL,
receptor_regimen_fiscal VARCHAR(3),
receptor_domicilio_fiscal VARCHAR(5), -- CP
receptor_uso_cfdi VARCHAR(4) NOT NULL, -- SAT catalog code
-- Amounts
subtotal DECIMAL(18,2) NOT NULL,
descuento DECIMAL(18,2) DEFAULT 0,
total DECIMAL(18,2) NOT NULL,
-- Tax breakdown
total_impuestos_trasladados DECIMAL(18,2) DEFAULT 0,
total_impuestos_retenidos DECIMAL(18,2) DEFAULT 0,
-- IVA specific
iva_16 DECIMAL(18,2) DEFAULT 0, -- IVA 16%
iva_8 DECIMAL(18,2) DEFAULT 0, -- IVA 8% (frontera)
iva_0 DECIMAL(18,2) DEFAULT 0, -- IVA 0%
iva_exento DECIMAL(18,2) DEFAULT 0, -- IVA exento
-- ISR retention
isr_retenido DECIMAL(18,2) DEFAULT 0,
iva_retenido DECIMAL(18,2) DEFAULT 0,
-- Currency
moneda VARCHAR(3) NOT NULL DEFAULT 'MXN',
tipo_cambio DECIMAL(18,6) DEFAULT 1,
-- Payment info
forma_pago VARCHAR(2), -- SAT catalog
metodo_pago VARCHAR(3), -- PUE, PPD
condiciones_pago VARCHAR(255),
-- Related documents
cfdi_relacionados JSONB, -- Array of related CFDI UUIDs
tipo_relacion VARCHAR(2), -- SAT catalog
-- Concepts/items (denormalized for quick access)
conceptos JSONB NOT NULL, -- Array of line items
-- Full XML storage
xml_content TEXT, -- Original XML
xml_hash VARCHAR(64), -- SHA-256 of XML
-- Digital stamps
sello_cfdi TEXT, -- Digital signature
sello_sat TEXT, -- SAT signature
certificado_sat VARCHAR(50),
cadena_original_tfd TEXT,
-- Direction (for the tenant)
is_emitted BOOLEAN NOT NULL, -- true = we issued it, false = we received it
-- Categorization
category_id UUID,
contact_id UUID,
-- Reconciliation
is_reconciled BOOLEAN NOT NULL DEFAULT false,
reconciled_at TIMESTAMP WITH TIME ZONE,
reconciled_by UUID,
-- AI-generated insights
ai_category_suggestion VARCHAR(100),
ai_confidence_score DECIMAL(5,4),
-- Source
source VARCHAR(50) NOT NULL DEFAULT 'sat_sync', -- sat_sync, manual, api
-- Metadata
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Indexes for cfdis (optimized for reporting queries)
CREATE INDEX idx_cfdis_uuid_fiscal ON cfdis(uuid_fiscal);
CREATE INDEX idx_cfdis_fecha_emision ON cfdis(fecha_emision DESC);
CREATE INDEX idx_cfdis_emisor_rfc ON cfdis(emisor_rfc);
CREATE INDEX idx_cfdis_receptor_rfc ON cfdis(receptor_rfc);
CREATE INDEX idx_cfdis_tipo ON cfdis(tipo_comprobante);
CREATE INDEX idx_cfdis_status ON cfdis(status);
CREATE INDEX idx_cfdis_is_emitted ON cfdis(is_emitted);
CREATE INDEX idx_cfdis_category ON cfdis(category_id) WHERE category_id IS NOT NULL;
CREATE INDEX idx_cfdis_contact ON cfdis(contact_id) WHERE contact_id IS NOT NULL;
CREATE INDEX idx_cfdis_reconciled ON cfdis(is_reconciled, fecha_emision DESC) WHERE is_reconciled = false;
-- Composite indexes for common queries
CREATE INDEX idx_cfdis_emitted_date ON cfdis(is_emitted, fecha_emision DESC);
CREATE INDEX idx_cfdis_type_date ON cfdis(tipo_comprobante, fecha_emision DESC);
CREATE INDEX idx_cfdis_month_report ON cfdis(DATE_TRUNC('month', fecha_emision), tipo_comprobante, is_emitted);
-- Full-text search index
CREATE INDEX idx_cfdis_search ON cfdis USING gin(to_tsvector('spanish', emisor_nombre || ' ' || receptor_nombre || ' ' || COALESCE(serie, '') || ' ' || COALESCE(folio, '')));
-- ============================================================================
-- TRANSACTIONS TABLE
-- Unified financial transaction model
-- ============================================================================
CREATE TABLE transactions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Transaction info
type transaction_type NOT NULL,
status transaction_status NOT NULL DEFAULT 'pending',
-- Amount
amount DECIMAL(18,2) NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'MXN',
exchange_rate DECIMAL(18,6) DEFAULT 1,
amount_mxn DECIMAL(18,2) NOT NULL, -- Always in MXN for reporting
-- Dates
transaction_date DATE NOT NULL,
value_date DATE, -- Settlement date
recorded_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
-- Description
description TEXT,
reference VARCHAR(255),
notes TEXT,
-- Categorization
category_id UUID,
account_id UUID,
contact_id UUID,
-- Related documents
cfdi_id UUID REFERENCES cfdis(id) ON DELETE SET NULL,
-- Bank reconciliation
bank_transaction_id VARCHAR(255),
bank_account_id VARCHAR(100),
bank_description TEXT,
-- Recurring
is_recurring BOOLEAN NOT NULL DEFAULT false,
recurring_pattern JSONB, -- Frequency, end date, etc.
parent_transaction_id UUID REFERENCES transactions(id) ON DELETE SET NULL,
-- Attachments
attachments JSONB, -- Array of file references
-- Tags for custom classification
tags TEXT[],
-- Reconciliation status
is_reconciled BOOLEAN NOT NULL DEFAULT false,
reconciled_at TIMESTAMP WITH TIME ZONE,
reconciled_by UUID,
-- Approval workflow (for larger amounts)
requires_approval BOOLEAN NOT NULL DEFAULT false,
approved_at TIMESTAMP WITH TIME ZONE,
approved_by UUID,
-- AI categorization
ai_category_id UUID,
ai_confidence DECIMAL(5,4),
ai_notes TEXT,
-- Metadata
created_by UUID,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
voided_at TIMESTAMP WITH TIME ZONE,
voided_by UUID,
void_reason TEXT
);
-- Indexes for transactions
CREATE INDEX idx_transactions_date ON transactions(transaction_date DESC);
CREATE INDEX idx_transactions_type ON transactions(type);
CREATE INDEX idx_transactions_status ON transactions(status);
CREATE INDEX idx_transactions_category ON transactions(category_id);
CREATE INDEX idx_transactions_account ON transactions(account_id);
CREATE INDEX idx_transactions_contact ON transactions(contact_id);
CREATE INDEX idx_transactions_cfdi ON transactions(cfdi_id) WHERE cfdi_id IS NOT NULL;
CREATE INDEX idx_transactions_reconciled ON transactions(is_reconciled, transaction_date DESC) WHERE is_reconciled = false;
-- Composite indexes for reporting
CREATE INDEX idx_transactions_monthly ON transactions(DATE_TRUNC('month', transaction_date), type, status);
CREATE INDEX idx_transactions_category_date ON transactions(category_id, transaction_date DESC) WHERE category_id IS NOT NULL;
-- ============================================================================
-- CONTACTS TABLE
-- Customers and suppliers
-- ============================================================================
CREATE TABLE contacts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Type
type contact_type NOT NULL,
-- Basic info
name VARCHAR(500) NOT NULL,
trade_name VARCHAR(500), -- Nombre comercial
-- Tax info (RFC)
rfc VARCHAR(13),
regimen_fiscal VARCHAR(3), -- SAT catalog
uso_cfdi_default VARCHAR(4), -- Default uso CFDI
-- Contact info
email VARCHAR(255),
phone VARCHAR(50),
mobile VARCHAR(50),
website VARCHAR(255),
-- Address
address_street VARCHAR(500),
address_interior VARCHAR(50),
address_exterior VARCHAR(50),
address_neighborhood VARCHAR(200), -- Colonia
address_city VARCHAR(100),
address_municipality VARCHAR(100), -- Municipio/Delegacion
address_state VARCHAR(100),
address_zip VARCHAR(5),
address_country VARCHAR(2) DEFAULT 'MX',
-- Bank info
bank_name VARCHAR(100),
bank_account VARCHAR(20),
bank_clabe VARCHAR(18),
-- Credit terms
credit_days INTEGER DEFAULT 0,
credit_limit DECIMAL(18,2) DEFAULT 0,
-- Balances (denormalized for performance)
balance_receivable DECIMAL(18,2) DEFAULT 0,
balance_payable DECIMAL(18,2) DEFAULT 0,
-- Classification
category VARCHAR(100),
tags TEXT[],
-- Status
is_active BOOLEAN NOT NULL DEFAULT true,
-- Notes
notes TEXT,
-- Metadata
created_by UUID,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Indexes for contacts
CREATE INDEX idx_contacts_type ON contacts(type);
CREATE INDEX idx_contacts_rfc ON contacts(rfc) WHERE rfc IS NOT NULL;
CREATE INDEX idx_contacts_name ON contacts(name);
CREATE INDEX idx_contacts_active ON contacts(is_active);
CREATE INDEX idx_contacts_search ON contacts USING gin(to_tsvector('spanish', name || ' ' || COALESCE(trade_name, '') || ' ' || COALESCE(rfc, '')));
-- ============================================================================
-- CATEGORIES TABLE
-- Transaction/expense categories
-- ============================================================================
CREATE TABLE categories (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Identification
code VARCHAR(20) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL,
description TEXT,
-- Type
type category_type NOT NULL,
-- Hierarchy
parent_id UUID REFERENCES categories(id) ON DELETE SET NULL,
level INTEGER NOT NULL DEFAULT 0,
path TEXT, -- Materialized path for hierarchy
-- SAT mapping
sat_key VARCHAR(10), -- Clave producto/servicio SAT
-- Budget
budget_monthly DECIMAL(18,2),
budget_yearly DECIMAL(18,2),
-- Display
color VARCHAR(7), -- Hex color
icon VARCHAR(50),
display_order INTEGER DEFAULT 0,
-- Status
is_active BOOLEAN NOT NULL DEFAULT true,
is_system BOOLEAN NOT NULL DEFAULT false, -- Prevent deletion of system categories
-- Metadata
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Indexes for categories
CREATE INDEX idx_categories_code ON categories(code);
CREATE INDEX idx_categories_type ON categories(type);
CREATE INDEX idx_categories_parent ON categories(parent_id);
CREATE INDEX idx_categories_active ON categories(is_active);
-- ============================================================================
-- ACCOUNTS TABLE
-- Chart of accounts (catalogo de cuentas)
-- ============================================================================
CREATE TABLE accounts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Identification
code VARCHAR(20) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL,
description TEXT,
-- Type
type account_type NOT NULL,
-- Hierarchy
parent_id UUID REFERENCES accounts(id) ON DELETE SET NULL,
level INTEGER NOT NULL DEFAULT 0,
path TEXT, -- Materialized path
-- SAT mapping (for Contabilidad Electronica)
sat_code VARCHAR(20), -- Codigo agrupador SAT
sat_nature VARCHAR(1), -- D = Deudora, A = Acreedora
-- Balances (denormalized)
balance_debit DECIMAL(18,2) DEFAULT 0,
balance_credit DECIMAL(18,2) DEFAULT 0,
balance_current DECIMAL(18,2) DEFAULT 0,
-- Status
is_active BOOLEAN NOT NULL DEFAULT true,
is_system BOOLEAN NOT NULL DEFAULT false,
allows_movements BOOLEAN NOT NULL DEFAULT true, -- Can have direct transactions
-- Display
display_order INTEGER DEFAULT 0,
-- Metadata
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Indexes for accounts
CREATE INDEX idx_accounts_code ON accounts(code);
CREATE INDEX idx_accounts_type ON accounts(type);
CREATE INDEX idx_accounts_parent ON accounts(parent_id);
CREATE INDEX idx_accounts_active ON accounts(is_active);
-- ============================================================================
-- METRICS_CACHE TABLE
-- Pre-computed metrics for dashboard performance
-- ============================================================================
CREATE TABLE metrics_cache (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Metric identification
metric_key VARCHAR(100) NOT NULL,
period_type VARCHAR(20) NOT NULL, -- daily, weekly, monthly, yearly
period_start DATE NOT NULL,
period_end DATE NOT NULL,
-- Dimension (optional filtering)
dimension_type VARCHAR(50), -- category, contact, account, etc.
dimension_id UUID,
-- Values
value_numeric DECIMAL(18,4),
value_json JSONB,
-- Comparison
previous_value DECIMAL(18,4),
change_percent DECIMAL(8,4),
change_absolute DECIMAL(18,4),
-- Validity
computed_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
valid_until TIMESTAMP WITH TIME ZONE,
is_stale BOOLEAN NOT NULL DEFAULT false,
-- Unique constraint on metric + period + dimension
UNIQUE(metric_key, period_type, period_start, dimension_type, dimension_id)
);
-- Indexes for metrics_cache
CREATE INDEX idx_metrics_cache_key ON metrics_cache(metric_key);
CREATE INDEX idx_metrics_cache_period ON metrics_cache(period_type, period_start DESC);
CREATE INDEX idx_metrics_cache_dimension ON metrics_cache(dimension_type, dimension_id) WHERE dimension_type IS NOT NULL;
CREATE INDEX idx_metrics_cache_stale ON metrics_cache(is_stale) WHERE is_stale = true;
-- ============================================================================
-- ALERTS TABLE
-- Financial alerts and notifications
-- ============================================================================
CREATE TABLE alerts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Alert info
type VARCHAR(50) NOT NULL,
title VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
severity alert_severity NOT NULL DEFAULT 'info',
-- Related entity
entity_type VARCHAR(50),
entity_id UUID,
-- Threshold that triggered the alert
threshold_type VARCHAR(50),
threshold_value DECIMAL(18,4),
current_value DECIMAL(18,4),
-- Actions
action_url VARCHAR(500),
action_label VARCHAR(100),
action_data JSONB,
-- Status
is_read BOOLEAN NOT NULL DEFAULT false,
is_dismissed BOOLEAN NOT NULL DEFAULT false,
read_at TIMESTAMP WITH TIME ZONE,
dismissed_at TIMESTAMP WITH TIME ZONE,
dismissed_by UUID,
-- Recurrence
is_recurring BOOLEAN NOT NULL DEFAULT false,
last_triggered_at TIMESTAMP WITH TIME ZONE,
trigger_count INTEGER DEFAULT 1,
-- Auto-resolve
auto_resolved BOOLEAN NOT NULL DEFAULT false,
resolved_at TIMESTAMP WITH TIME ZONE,
resolved_by UUID,
resolution_notes TEXT,
-- Metadata
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE
);
-- Indexes for alerts
CREATE INDEX idx_alerts_type ON alerts(type);
CREATE INDEX idx_alerts_severity ON alerts(severity);
CREATE INDEX idx_alerts_unread ON alerts(is_read, created_at DESC) WHERE is_read = false;
CREATE INDEX idx_alerts_entity ON alerts(entity_type, entity_id) WHERE entity_type IS NOT NULL;
CREATE INDEX idx_alerts_active ON alerts(is_dismissed, created_at DESC) WHERE is_dismissed = false;
-- ============================================================================
-- REPORTS TABLE
-- Generated reports
-- ============================================================================
CREATE TABLE reports (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Report info
type VARCHAR(100) NOT NULL, -- balance_general, estado_resultados, flujo_efectivo, etc.
name VARCHAR(255) NOT NULL,
description TEXT,
-- Period
period_start DATE NOT NULL,
period_end DATE NOT NULL,
comparison_period_start DATE,
comparison_period_end DATE,
-- Status
status report_status NOT NULL DEFAULT 'draft',
-- Parameters used to generate
parameters JSONB,
-- Output
data JSONB, -- Report data
file_url VARCHAR(500), -- PDF/Excel URL
file_format VARCHAR(10), -- pdf, xlsx, csv
-- Scheduling
is_scheduled BOOLEAN NOT NULL DEFAULT false,
schedule_cron VARCHAR(50),
next_scheduled_at TIMESTAMP WITH TIME ZONE,
last_generated_at TIMESTAMP WITH TIME ZONE,
-- Sharing
is_shared BOOLEAN NOT NULL DEFAULT false,
shared_with UUID[],
share_token VARCHAR(100),
share_expires_at TIMESTAMP WITH TIME ZONE,
-- Metadata
generated_by UUID,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Indexes for reports
CREATE INDEX idx_reports_type ON reports(type);
CREATE INDEX idx_reports_status ON reports(status);
CREATE INDEX idx_reports_period ON reports(period_start, period_end);
CREATE INDEX idx_reports_scheduled ON reports(is_scheduled, next_scheduled_at) WHERE is_scheduled = true;
-- ============================================================================
-- SETTINGS TABLE
-- Tenant-specific settings
-- ============================================================================
CREATE TABLE settings (
key VARCHAR(100) PRIMARY KEY,
value TEXT NOT NULL,
value_type VARCHAR(20) NOT NULL DEFAULT 'string', -- string, integer, boolean, json
category VARCHAR(50) NOT NULL DEFAULT 'general',
label VARCHAR(200),
description TEXT,
is_sensitive BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- ============================================================================
-- BANK_ACCOUNTS TABLE
-- Connected bank accounts
-- ============================================================================
CREATE TABLE bank_accounts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Bank info
bank_name VARCHAR(100) NOT NULL,
bank_code VARCHAR(10), -- SPEI bank code
-- Account info
account_number VARCHAR(20),
clabe VARCHAR(18),
account_type VARCHAR(50), -- checking, savings, credit
-- Display
alias VARCHAR(100),
currency VARCHAR(3) DEFAULT 'MXN',
-- Balance (cached from bank sync)
balance_available DECIMAL(18,2),
balance_current DECIMAL(18,2),
balance_updated_at TIMESTAMP WITH TIME ZONE,
-- Connection
connection_provider VARCHAR(50), -- belvo, finerio, manual
connection_id VARCHAR(255),
connection_status VARCHAR(50),
last_sync_at TIMESTAMP WITH TIME ZONE,
last_sync_error TEXT,
-- Categorization
account_id UUID REFERENCES accounts(id), -- Link to chart of accounts
-- Status
is_active BOOLEAN NOT NULL DEFAULT true,
-- Metadata
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Indexes for bank_accounts
CREATE INDEX idx_bank_accounts_active ON bank_accounts(is_active);
CREATE INDEX idx_bank_accounts_connection ON bank_accounts(connection_provider, connection_id) WHERE connection_id IS NOT NULL;
-- ============================================================================
-- BUDGET_ITEMS TABLE
-- Budget planning
-- ============================================================================
CREATE TABLE budget_items (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Period
year INTEGER NOT NULL,
month INTEGER NOT NULL, -- 1-12, or 0 for yearly
-- Category
category_id UUID REFERENCES categories(id) ON DELETE CASCADE,
account_id UUID REFERENCES accounts(id) ON DELETE CASCADE,
-- Amounts
amount_budgeted DECIMAL(18,2) NOT NULL,
amount_actual DECIMAL(18,2) DEFAULT 0,
amount_variance DECIMAL(18,2) GENERATED ALWAYS AS (amount_actual - amount_budgeted) STORED,
-- Notes
notes TEXT,
-- Status
is_locked BOOLEAN NOT NULL DEFAULT false,
-- Metadata
created_by UUID,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
-- One budget per category/account per period
UNIQUE(year, month, category_id),
UNIQUE(year, month, account_id)
);
-- Indexes for budget_items
CREATE INDEX idx_budget_items_period ON budget_items(year, month);
CREATE INDEX idx_budget_items_category ON budget_items(category_id);
CREATE INDEX idx_budget_items_account ON budget_items(account_id);
-- ============================================================================
-- ATTACHMENTS TABLE
-- File attachments for various entities
-- ============================================================================
CREATE TABLE attachments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
-- Related entity
entity_type VARCHAR(50) NOT NULL, -- cfdi, transaction, contact, etc.
entity_id UUID NOT NULL,
-- File info
file_name VARCHAR(255) NOT NULL,
file_type VARCHAR(100),
file_size INTEGER,
file_url VARCHAR(500) NOT NULL,
-- Storage
storage_provider VARCHAR(50) DEFAULT 'local', -- local, s3, gcs
storage_path VARCHAR(500),
-- Metadata
uploaded_by UUID,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Indexes for attachments
CREATE INDEX idx_attachments_entity ON attachments(entity_type, entity_id);
-- ============================================================================
-- TRIGGERS FOR TENANT SCHEMA
-- ============================================================================
-- Update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Apply triggers
CREATE TRIGGER update_sat_credentials_updated_at BEFORE UPDATE ON sat_credentials
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_cfdis_updated_at BEFORE UPDATE ON cfdis
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_transactions_updated_at BEFORE UPDATE ON transactions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_contacts_updated_at BEFORE UPDATE ON contacts
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_categories_updated_at BEFORE UPDATE ON categories
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_accounts_updated_at BEFORE UPDATE ON accounts
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_reports_updated_at BEFORE UPDATE ON reports
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_settings_updated_at BEFORE UPDATE ON settings
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_bank_accounts_updated_at BEFORE UPDATE ON bank_accounts
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_budget_items_updated_at BEFORE UPDATE ON budget_items
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================================
-- FOREIGN KEY CONSTRAINTS
-- ============================================================================
-- Add foreign keys after all tables are created
ALTER TABLE cfdis ADD CONSTRAINT fk_cfdis_category
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL;
ALTER TABLE cfdis ADD CONSTRAINT fk_cfdis_contact
FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE SET NULL;
ALTER TABLE transactions ADD CONSTRAINT fk_transactions_category
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL;
ALTER TABLE transactions ADD CONSTRAINT fk_transactions_account
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE SET NULL;
ALTER TABLE transactions ADD CONSTRAINT fk_transactions_contact
FOREIGN KEY (contact_id) REFERENCES contacts(id) ON DELETE SET NULL;
-- ============================================================================
-- COMMENTS
-- ============================================================================
COMMENT ON TABLE sat_credentials IS 'Encrypted SAT FIEL credentials for each tenant';
COMMENT ON TABLE cfdis IS 'CFDI 4.0 invoices (emitted and received)';
COMMENT ON TABLE transactions IS 'Unified financial transactions';
COMMENT ON TABLE contacts IS 'Customers and suppliers';
COMMENT ON TABLE categories IS 'Transaction categorization';
COMMENT ON TABLE accounts IS 'Chart of accounts (catalogo de cuentas)';
COMMENT ON TABLE metrics_cache IS 'Pre-computed metrics for dashboard';
COMMENT ON TABLE alerts IS 'Financial alerts and notifications';
COMMENT ON TABLE reports IS 'Generated financial reports';
COMMENT ON TABLE settings IS 'Tenant-specific configuration';