🔢 Sistema de Numeración y Nomenclatura Oficial - Implementación¶
El sistema de numeración de GDI garantiza la asignación única y secuencial de números oficiales a documentos con validez legal.
🎯 Objetivo del Sistema¶
Asegurar que cada documento oficial tenga un identificador único, secuencial y trazable que cumpla con normativas municipales y permita búsqueda, auditoría y validación legal.
📋 Formato de Numeración Oficial¶
Estructura Estándar¶
Componentes del Formato¶
| Componente | Descripción | Fuente en BD | Ejemplo |
|---|---|---|---|
| TIPO | Acrónimo del tipo de documento | document_types.acronym |
DECRE |
| AAAA | Año de la fecha oficial | EXTRACT(YEAR FROM NOW()) |
2025 |
| NNNNNN | Número correlativo (6 dígitos) | numeration_requests.reserved_number |
000123 |
| SIGLA_ECO | Sigla del ecosistema/municipio | municipalities.acronym |
TN |
| SIGLA_DEPARTMENT | Sigla del department numerador | departments.acronym |
INTEN |
Ejemplos Reales¶
DECRE-2025-000123-TN-INTEN (Decreto de Intendencia)
RESOL-2025-000045-TN-SECGOB (Resolución de Secretaría de Gobierno)
DISP-2025-000067-TN-DIROBR (Disposición de Dirección de Obras)
IF-2025-001234-TN-SECGOB (Informe de Secretaría de Gobierno)
🏗️ Arquitectura del Sistema de Numeración¶
Tablas Involucradas¶
1. numeration_requests (Reserva de Números)¶
CREATE TABLE numeration_requests (
numeration_requests_id UUID PRIMARY KEY,
document_type_id UUID NOT NULL,
user_id UUID NOT NULL, -- Usuario que solicita
department_id UUID NOT NULL, -- Department numerador
year SMALLINT NOT NULL, -- Año del documento
reserved_number VARCHAR UNIQUE NOT NULL, -- Número reservado
reserved_at TIMESTAMP NOT NULL, -- Momento de reserva
is_confirmed BOOLEAN DEFAULT false, -- Si fue confirmado
confirmed_at TIMESTAMP, -- Momento de confirmación
validation_status validation_status_enum NOT NULL
);
2. official_documents (Documentos Oficiales)¶
CREATE TABLE official_documents (
document_id UUID PRIMARY KEY,
numeration_requests_id UUID NOT NULL,
official_number VARCHAR UNIQUE NOT NULL, -- Número final formateado
year SMALLINT NOT NULL,
numerator_id UUID NOT NULL, -- Usuario numerador
signed_at TIMESTAMP NOT NULL, -- Fecha oficial
signed_pdf_url VARCHAR NOT NULL -- PDF firmado
);
Estados de Numeración¶
CREATE TYPE validation_status_enum AS ENUM (
'pending', -- Número reservado, esperando confirmación
'valid', -- Número confirmado y válido
'invalid' -- Número invalidado por error
);
🔄 Proceso de Numeración Completo¶
FASE 1: Reserva de Numero¶
Trigger: Numerador inicia proceso de firma final
La reserva utiliza un advisory lock (888888) para serializar el acceso y una global_sequence compartida entre todos los tipos de documento.
-- 1. Adquirir advisory lock para prevenir race conditions
SELECT pg_advisory_lock(888888);
-- 2. Calcular siguiente numero de la secuencia global
WITH next_number AS (
SELECT COALESCE(MAX(CAST(reserved_number AS INTEGER)), 0) + 1 as next_num
FROM numeration_requests
WHERE year = EXTRACT(YEAR FROM NOW())
)
-- 3. Reservar numero
INSERT INTO numeration_requests (
document_type_id,
user_id,
department_id,
year,
reserved_number,
reserved_at,
validation_status
)
SELECT
?, -- document_type_id
?, -- numerator user_id
?, -- department_id del numerador
EXTRACT(YEAR FROM NOW()),
LPAD(next_num::TEXT, 6, '0'), -- Numero con ceros a la izquierda
NOW(),
'pending'
FROM next_number;
-- 4. Liberar advisory lock
SELECT pg_advisory_unlock(888888);
FASE 2: Generación del Número Oficial¶
Trigger: Numerador completa firma digital
-- Construcción del número oficial completo
SELECT
CONCAT(
dt.acronym, '-', -- TIPO
nr.year, '-', -- AÑO
nr.reserved_number, '-', -- NÚMERO
m.acronym, '-', -- ECOSISTEMA
d.acronym -- DEPARTMENT
) as official_number
FROM numeration_requests nr
JOIN document_types dt ON nr.document_type_id = dt.document_type_id
JOIN departments d ON nr.department_id = d.department_id
JOIN municipalities m ON d.municipality_id = m.id_municipality
WHERE nr.numeration_requests_id = ?;
FASE 3: Confirmación y Oficialización¶
BEGIN TRANSACTION;
-- 1. Confirmar reserva
UPDATE numeration_requests
SET
is_confirmed = true,
confirmed_at = NOW(),
validation_status = 'valid'
WHERE numeration_requests_id = ?;
-- 2. Crear documento oficial
INSERT INTO official_documents (
document_id,
numeration_requests_id,
official_number,
year,
numerator_id,
signed_at,
signed_pdf_url
) VALUES (?, ?, ?, ?, ?, NOW(), ?);
-- 3. Finalizar documento draft
UPDATE document_draft
SET status = 'signed'
WHERE document_id = ?;
COMMIT;
¶
BEGIN TRANSACTION;
-- 1. Confirmar reserva
UPDATE numeration_requests
SET
is_confirmed = true,
confirmed_at = NOW(),
validation_status = 'valid'
WHERE numeration_requests_id = ?;
-- 2. Crear documento oficial
INSERT INTO official_documents (
document_id,
numeration_requests_id,
official_number,
year,
numerator_id,
signed_at,
signed_pdf_url
) VALUES (?, ?, ?, ?, ?, NOW(), ?);
-- 3. Finalizar documento draft
UPDATE document_draft
SET status = 'signed'
WHERE document_id = ?;
COMMIT;
🔐 Control de Concurrencia¶
Problema de Concurrencia¶
Multiples usuarios intentando numerar documentos simultaneamente.
Solucion Implementada¶
1. Advisory Lock y Global Sequence¶
El sistema utiliza un advisory lock con ID 888888 para serializar las operaciones de numeracion. La secuencia es global_sequence compartida entre todos los tipos de documento, lo que garantiza unicidad absoluta.
-- Advisory lock para prevenir race conditions
SELECT pg_advisory_lock(888888);
-- Obtener siguiente numero de la secuencia global
-- La global_sequence es compartida entre todos los tipos de documento
SELECT COALESCE(MAX(CAST(reserved_number AS INTEGER)), 0) + 1 as next_num
FROM numeration_requests
WHERE year = EXTRACT(YEAR FROM NOW());
-- Insertar reserva con el numero obtenido
INSERT INTO numeration_requests (
document_type_id, user_id, department_id,
year, reserved_number, reserved_at, validation_status
) VALUES (
?, ?, ?,
EXTRACT(YEAR FROM NOW()),
LPAD(next_num::TEXT, 6, '0'),
NOW(),
'pending'
);
-- Liberar lock
SELECT pg_advisory_unlock(888888);
2. Constraint de Unicidad¶
-- Constraint en BD para prevenir duplicados
ALTER TABLE numeration_requests
ADD CONSTRAINT unique_reserved_number UNIQUE (reserved_number);
-- Constraint en documento oficial
ALTER TABLE official_documents
ADD CONSTRAINT unique_official_number UNIQUE (official_number);
3. Timeout de Reserva¶
-- Cleanup automatico de reservas vencidas (cron job)
UPDATE numeration_requests
SET validation_status = 'invalid'
WHERE validation_status = 'pending'
AND reserved_at < (NOW() - INTERVAL '1 hour')
AND is_confirmed = false;
📊 Secuencia Global de Numeracion¶
global_sequence compartida¶
El sistema utiliza una secuencia global unica compartida entre todos los tipos de documento. Esto significa que los numeros correlativos no se asignan por tipo de documento, sino de forma global para todo el sistema en un ano dado.
Ejemplo de secuencia global:
000001 - DECRE-2025-000001-TN-INTEN (Decreto)
000002 - RESOL-2025-000002-TN-SECGOB (Resolucion)
000003 - DECRE-2025-000003-TN-INTEN (Decreto)
000004 - IF-2025-000004-TN-SECGOB (Informe)
El numero correlativo es compartido, pero el numero oficial completo es unico gracias a la combinacion con el acronimo del tipo de documento.
Reinicio Anual¶
La secuencia global se reinicia automaticamente cada ano:
-- La secuencia se reinicia al cambiar el ano
SELECT COALESCE(MAX(CAST(reserved_number AS INTEGER)), 0) + 1 as next_num
FROM numeration_requests
WHERE year = EXTRACT(YEAR FROM NOW());
🏛️ Asignación de Department Numerador¶
Lógica de Asignación¶
La sigla del department que aparece en el número oficial se determina por:
- Department del usuario numerador (quien firma y numera)
- NO el department del creador del documento
Casos de Uso¶
Caso 1: Numerador = Creador¶
Usuario: Juan Pérez (INTEN)
Crea documento tipo DECRE
Numera él mismo
Resultado: DECRE-2025-000123-TN-INTEN
Caso 2: Numerador ≠ Creador¶
Usuario Creador: María García (SECGOB)
Crea documento tipo DECRE
Numerador: Intendente (INTEN)
Resultado: DECRE-2025-000123-TN-INTEN
Validación de Autorización¶
-- Verificar que usuario puede numerar este tipo
SELECT EXISTS (
SELECT 1
FROM document_types_allowed_by_rank dtar
JOIN departments d ON d.rank_id = dtar.rank_id
JOIN users u ON u.sector_id = d.department_id
WHERE u.user_id = ? -- numerador
AND dtar.document_type_id = ? -- tipo documento
) as can_numerize;
¶
-- Verificar que usuario puede numerar este tipo
SELECT EXISTS (
SELECT 1
FROM document_types_allowed_by_rank dtar
JOIN departments d ON d.rank_id = dtar.rank_id
JOIN users u ON u.sector_id = d.department_id
WHERE u.user_id = ? -- numerador
AND dtar.document_type_id = ? -- tipo documento
) as can_numerize;
🔍 Consultas y Búsquedas¶
Búsqueda por Número Oficial¶
-- Búsqueda exacta por número completo
SELECT
dd.reference,
dd.content,
od.official_number,
od.signed_at,
od.signed_pdf_url,
dt.name as document_type,
u.full_name as numerator
FROM official_documents od
JOIN document_draft dd ON od.document_id = dd.document_id
JOIN document_types dt ON dd.document_type_id = dt.document_type_id
JOIN users u ON od.numerator_id = u.user_id
WHERE od.official_number = ?;
Búsqueda por Componentes¶
-- Búsqueda por tipo y año
SELECT od.official_number, dd.reference, od.signed_at
FROM official_documents od
JOIN document_draft dd ON od.document_id = dd.document_id
JOIN document_types dt ON dd.document_type_id = dt.document_type_id
WHERE dt.acronym = ? -- Ej: 'DECRE'
AND od.year = ? -- Ej: 2025
ORDER BY od.official_number;
Búsqueda por Rango¶
-- Documentos entre números
SELECT od.official_number, dd.reference
FROM official_documents od
JOIN document_draft dd ON od.document_id = dd.document_id
JOIN numeration_requests nr ON od.numeration_requests_id = nr.numeration_requests_id
WHERE CAST(nr.reserved_number AS INTEGER) BETWEEN ? AND ?
AND nr.year = ?
ORDER BY CAST(nr.reserved_number AS INTEGER);
📈 Métricas y Estadísticas¶
Secuencias por Tipo y Año¶
-- Estado actual de secuencias
SELECT
dt.acronym,
dt.name,
COUNT(nr.numeration_requests_id) as total_reserved,
COUNT(CASE WHEN nr.is_confirmed THEN 1 END) as confirmed,
MAX(CAST(nr.reserved_number AS INTEGER)) as last_number
FROM document_types dt
LEFT JOIN numeration_requests nr ON dt.document_type_id = nr.document_type_id
AND nr.year = EXTRACT(YEAR FROM NOW())
GROUP BY dt.document_type_id, dt.acronym, dt.name
ORDER BY dt.acronym;
Volumen de Numeración¶
-- Documentos numerados por mes
SELECT
DATE_TRUNC('month', od.signed_at) as month,
dt.acronym,
COUNT(*) as documents_signed
FROM official_documents od
JOIN document_draft dd ON od.document_id = dd.document_id
JOIN document_types dt ON dd.document_type_id = dt.document_type_id
WHERE od.signed_at >= DATE_TRUNC('year', NOW())
GROUP BY DATE_TRUNC('month', od.signed_at), dt.acronym
ORDER BY month, dt.acronym;
Eficiencia del Proceso¶
-- Tiempo promedio de numeración
SELECT
dt.name,
AVG(nr.confirmed_at - nr.reserved_at) as avg_numerization_time,
COUNT(CASE WHEN nr.validation_status = 'invalid' THEN 1 END) as failed_reservations
FROM numeration_requests nr
JOIN document_types dt ON nr.document_type_id = dt.document_type_id
WHERE nr.reserved_at >= (NOW() - INTERVAL '30 days')
GROUP BY dt.document_type_id, dt.name;
⚠️ Gestión de Errores y Excepciones¶
Errores Comunes¶
1. Duplicación de Números¶
Causa: Fallo en control de concurrencia
Detección:
-- Detectar duplicados
SELECT reserved_number, COUNT(*)
FROM numeration_requests
WHERE year = EXTRACT(YEAR FROM NOW())
GROUP BY reserved_number
HAVING COUNT(*) > 1;
Resolución:
-- Invalidar duplicados excepto el primero
UPDATE numeration_requests
SET validation_status = 'invalid'
WHERE numeration_requests_id IN (
SELECT numeration_requests_id
FROM (
SELECT numeration_requests_id,
ROW_NUMBER() OVER (PARTITION BY reserved_number ORDER BY reserved_at) as rn
FROM numeration_requests
WHERE reserved_number = ?
) ranked
WHERE rn > 1
);
2. Reservas Huérfanas¶
Causa: Proceso interrumpido antes de confirmación
Detección:
-- Reservas pendientes por más de 1 hora
SELECT *
FROM numeration_requests
WHERE validation_status = 'pending'
AND is_confirmed = false
AND reserved_at < (NOW() - INTERVAL '1 hour');
Resolución:
-- Cleanup automático (cron job diario)
UPDATE numeration_requests
SET validation_status = 'invalid'
WHERE validation_status = 'pending'
AND is_confirmed = false
AND reserved_at < (NOW() - INTERVAL '24 hours');
3. Gaps en Secuencia¶
Causa: Números invalidados o reservas fallidas
Detección:
-- Detectar gaps en secuencia
WITH RECURSIVE number_series AS (
SELECT 1 as num
UNION ALL
SELECT num + 1
FROM number_series
WHERE num < (
SELECT MAX(CAST(reserved_number AS INTEGER))
FROM numeration_requests
WHERE document_type_id = ? AND year = ?
)
)
SELECT ns.num as missing_number
FROM number_series ns
LEFT JOIN numeration_requests nr ON CAST(nr.reserved_number AS INTEGER) = ns.num
AND nr.document_type_id = ? AND nr.year = ?
WHERE nr.numeration_requests_id IS NULL;
Recovery Procedures¶
Reasignación de Números¶
-- Procedimiento para reasignar número específico
CREATE OR REPLACE FUNCTION reassign_document_number(
p_document_id UUID,
p_new_number INTEGER
) RETURNS BOOLEAN AS $$
DECLARE
v_type_id UUID;
v_current_year INTEGER;
BEGIN
-- Obtener información del documento
SELECT document_type_id INTO v_type_id
FROM document_draft
WHERE document_id = p_document_id;
v_current_year := EXTRACT(YEAR FROM NOW());
-- Verificar que el nuevo número esté disponible
IF EXISTS (
SELECT 1 FROM numeration_requests
WHERE document_type_id = v_type_id
AND year = v_current_year
AND CAST(reserved_number AS INTEGER) = p_new_number
) THEN
RETURN false; -- Número ya ocupado
END IF;
-- Reasignar número
UPDATE numeration_requests
SET reserved_number = LPAD(p_new_number::TEXT, 6, '0'),
validation_status = 'valid'
WHERE document_id = p_document_id;
RETURN true;
END;
$$ LANGUAGE plpgsql;
🔧 Administración del Sistema¶
Comandos de Mantenimiento¶
Reinicio de Secuencias Anuales¶
-- Procedimiento de inicio de año
CREATE OR REPLACE FUNCTION reset_annual_sequences()
RETURNS VOID AS $$
DECLARE
current_year INTEGER := EXTRACT(YEAR FROM NOW());
BEGIN
-- Log del reset
INSERT INTO audit_logs (action, details, created_at)
VALUES ('ANNUAL_SEQUENCE_RESET',
json_build_object('year', current_year),
NOW());
-- Las secuencias se reinician automáticamente
-- al usar el año como parte de la query
RAISE NOTICE 'Annual sequences reset for year %', current_year;
END;
$$ LANGUAGE plpgsql;
Backup de Numeración¶
-- Backup de estado de numeración
CREATE TABLE numeration_backup AS
SELECT
dt.acronym,
nr.year,
MAX(CAST(nr.reserved_number AS INTEGER)) as last_number,
COUNT(*) as total_documents,
NOW() as backup_date
FROM numeration_requests nr
JOIN document_types dt ON nr.document_type_id = dt.document_type_id
WHERE nr.is_confirmed = true
GROUP BY dt.document_type_id, dt.acronym, nr.year
ORDER BY dt.acronym, nr.year;
Validación de Integridad¶
-- Función de validación completa
CREATE OR REPLACE FUNCTION validate_numeracion_integrity()
RETURNS TABLE (
issue_type TEXT,
description TEXT,
affected_documents INTEGER
) AS $$
BEGIN
-- Verificar duplicados
RETURN QUERY
SELECT
'DUPLICATES'::TEXT,
'Duplicate reserved numbers found'::TEXT,
COUNT(*)::INTEGER
FROM (
SELECT reserved_number
FROM numeration_requests
WHERE year = EXTRACT(YEAR FROM NOW())
GROUP BY reserved_number
HAVING COUNT(*) > 1
) dups;
-- Verificar huérfanos
RETURN QUERY
SELECT
'ORPHANS'::TEXT,
'Pending reservations older than 24h'::TEXT,
COUNT(*)::INTEGER
FROM numeration_requests
WHERE validation_status = 'pending'
AND reserved_at < (NOW() - INTERVAL '24 hours');
-- Verificar inconsistencias oficial
RETURN QUERY
SELECT
'INCONSISTENT'::TEXT,
'Official documents without valid reservation'::TEXT,
COUNT(*)::INTEGER
FROM official_documents od
LEFT JOIN numeration_requests nr ON od.numeration_requests_id = nr.numeration_requests_id
WHERE nr.validation_status != 'valid' OR nr.is_confirmed = false;
END;
$$ LANGUAGE plpgsql;
📋 Configuración por Municipality¶
Personalización de Formato¶
-- Configuración específica por municipio
CREATE TABLE municipality_numeration_config (
municipality_id UUID PRIMARY KEY,
number_format VARCHAR NOT NULL DEFAULT '{TIPO}-{YEAR}-{NUMBER}-{ECO}-{DEPT}',
number_padding INTEGER DEFAULT 6,
year_format VARCHAR DEFAULT 'YYYY',
separator VARCHAR DEFAULT '-',
created_at TIMESTAMP DEFAULT NOW()
);
Templates de Numeración¶
-- Diferentes formatos según necesidades locales
INSERT INTO municipality_numeration_config VALUES
('municipality-1', '{TIPO}-{YEAR}-{NUMBER}-{ECO}-{DEPT}', 6, 'YYYY', '-'),
('municipality-2', '{TIPO}/{YEAR}/{NUMBER}', 4, 'YY', '/'),
('municipality-3', '{ECO}.{TIPO}.{YEAR}.{NUMBER}', 5, 'YYYY', '.');