Saltar a contenido

Catálogos

Datos que administración gobierna sin desplegar código: catálogos de negocio, parámetros del motor, plantillas de mensaje, archivos y consentimiento legal.

Las tablas de estado siguen el mismo principio pero tienen su propia página: Estados.

Requerimientos cubiertos

Alérgenos

El sistema no administra un catálogo de alérgenos. La declaración es responsabilidad del comercio dentro del texto de su publicación, no un dato estructurado que la plataforma administre y por tanto parezca garantizar.


parametro_sistema

Los valores que gobiernan el motor de reservas, en una sola columna valor sin tipar en vez de una columna física por tipo: la forma de la fila sería fea (tres NULL siempre presentes) para ganar una tipificación que un trigger da igual de estricta.

  • Régimen: Mutable protegida
  • Volumen estimado: 23 filas activas, ampliable sin migración

Columnas

Campo Tipo Nulo Descripción
clave varchar(60) no Única, con espacio de nombres: reserva.max_unidades
grupo varchar(20) no usuario
incumplimiento
comercio
precio
verificacion
impacto
alertas
comunicacion
tipo_dato varchar(16) no entero
decimal
duracion_min
porcentaje
booleano
lista_entero
valor jsonb no Un Decimal se guarda como string JSON, nunca como number
descripcion text no Vacía por defecto
actualizado_by uuid sí Llave foránea RESTRICT a usuario

Constraints

Solo dos constraints declarativas; la tercera regla, la que importa de verdad, es un trigger:

CONSTRAINT unq_parametrosistema_clave UNIQUE (clave)

CONSTRAINT chk_parametrosistema_valor_tipo
CHECK (
  (tipo_dato IN ('entero', 'duracion_min', 'decimal', 'porcentaje')
   AND jsonb_typeof(valor) IN ('number', 'string'))
  OR (tipo_dato = 'booleano' AND jsonb_typeof(valor) = 'boolean')
  OR (tipo_dato = 'lista_entero' AND jsonb_typeof(valor) = 'array')
)

chk_parametrosistema_valor_tipo es la barrera barata de primera línea: descarta la forma gruesa equivocada (un objeto donde se espera un número) sin riesgo de reventar, porque jsonb_typeof nunca lanza error.

trg_parametrosistema_especificacion

BEFORE INSERT OR UPDATE, ROW. Reemplaza lo que sería un segundo CHECK de veintitrés ramas: Postgres no garantiza evaluar OR de izquierda a derecha, así que una expresión pura arriesga castear a numeric el valor de una fila que en realidad es un array o un booleano de otra clave, y revienta con un error de casteo en vez de simplemente rechazar la fila. Un trigger con CASE NEW.clave WHEN ... no tiene ese problema: cada rama numérica solo se evalúa cuando su propia clave coincidió.

Por cada clave conocida, la rama generada verifica que grupo/tipo_dato correspondan, extrae valor como número ((NEW.valor #>> '{}')::numeric, dentro de un bloque que convierte cualquier fallo de casteo en un mensaje limpio) y lo compara contra el rango declarado. Una clave que no coincide con ningún WHEN cae en el ELSE y se rechaza. La única rama que no sigue este patrón escalar es exploracion.distancias_km, que recorre jsonb_array_elements(valor) y exige que cada elemento sea numérico y positivo.

Cualquier violación se levanta como RAISE check_violation, la misma condición real de Postgres que dispara un CHECK, así Django la sigue clasificando como IntegrityError sin que el llamador necesite conocer el código 23514 a mano.

Triggers

Nombre Evento Momento Nivel Regla Origen
trg_parametrosistema_especificacion INSERT
UPDATE
BEFORE ROW grupo, tipo_dato y rango de valor por clave RF-A-37
trg_parametrosistema_coherencia INSERT
UPDATE
BEFORE ROW Coherencia entre parámetros de un mismo par RF-A-40
trg_parametrosistema_autor UPDATE OF valor BEFORE ROW actualizado_by obligatorio al cambiar un valor RF-A-37
trg_parametrosistema_readonly_clave UPDATE OF clave BEFORE ROW clave es inmutable una vez creada RF-A-37

trg_parametrosistema_coherencia cubre las dos parejas de parámetros que no tienen sentido por separado: ventana.duracion_min_minutos no puede superar a ventana.duracion_max_minutos, y precio.descuento_minimo_pct no puede superar a precio.descuento_revision_pct. Son reglas entre filas distintas de la misma tabla, así que ningún CHECK las alcanza y el CASE del trigger de especificación tampoco: cada rama de ese CASE solo ve la fila que se está escribiendo.

trg_parametrosistema_readonly_clave es lo que hace confiable al resto. Cambiar la clave de una fila la haría caer en otra rama del CASE con un valor validado contra el rango equivocado, y todo trigger que lea hz_parametro('reserva.max_activas') empezaría a leer otra cosa. RF-A-37 exige historial y reversión del valor, no de la identidad del parámetro.

Los 23 parámetros y su rango válido

clave grupo tipo_dato Rango
reserva.max_unidades usuario entero 1 – 50
reserva.max_activas usuario entero 1 – 20
reserva.max_activas_por_comercio usuario entero 1 – 20
reserva.retencion_min usuario duracion_min 1 – 60
reserva.plazo_cancelacion_min usuario duracion_min 0 – 1440
valoracion.plazo_horas usuario entero 1 – 720
exploracion.distancias_km usuario lista_entero cada elemento > 0
sancion.expiradas_para_bloqueo incumplimiento entero 1 – 20
sancion.periodo_conteo_dias incumplimiento entero 1 – 365
sancion.duracion_bloqueo_dias incumplimiento entero 1 – 365
ventana.duracion_min_minutos comercio duracion_min 5 – 1440
ventana.duracion_max_minutos comercio duracion_min 5 – 1440
ventana.max_por_dia comercio entero 1 – 20
publicacion.horizonte_horas comercio entero 1 – 720
publicacion.corte_reservas_min comercio duracion_min 0 – 1440
validacion.tolerancia_min comercio duracion_min 0 – 1440
plantilla.max_activas comercio entero 1 – 50
comercio.max_operadores comercio entero 1 – 50
precio.minimo precio decimal ≥ 0.01
precio.descuento_minimo_pct precio porcentaje 0 – 100
precio.descuento_revision_pct precio porcentaje 0 – 100
publicacion.max_unidades precio entero 1 – 1000
campana.max_por_semana alertas entero 0 – 50

Los grupos verificacion, impacto y comunicacion existen en el TextChoices de grupo sin ningún parámetro activo todavía. Quedan reservados para cuando ese vocabulario tenga una clave concreta que necesite validarse, sin requerir una migración para agregar el grupo en sí.

Notas de diseño

valor es jsonb y no columnas tipadas porque los parámetros son heterogéneos: enteros, minutos, porcentajes con decimal y una lista de distancias. El costo de esa flexibilidad, que nada fuera de la aplicación impida un valor con la forma equivocada, lo cierra el trigger, no una convención de servicio: un UPDATE hecho desde el admin de Django, una consola o una migración de datos futura pasa por la misma barrera.

No existe una tabla parametro_cambio para historial o reversión: la tabla de eventos gemela que genera el régimen mutable protegida ya guarda el estado completo de la fila en cada UPDATE. Reconstruir un valor anterior o revertir un cambio es una consulta sobre esa tabla de eventos, no un modelo aparte.


motivo

Catálogo único de motivos de lista cerrada, discriminado por contexto.

Columnas

Campo Tipo Nulo Descripción
contexto varchar(30) no Dónde se ofrece este motivo
codigo varchar(50) no Estable
etiqueta varchar(80) no Visible
require_nota boolean no Implementa "otro con nota obligatoria"
visible_para varchar(20) no usuario
comercio
administracion
todos
activo boolean no

Constraints

CONSTRAINT chk_motivo_codigo_format
CHECK (codigo ~ '^[a-z][a-z0-9_]*$')

CONSTRAINT chk_motivo_contexto
CHECK (
  contexto IN (
    'cancelacion_usuario', 'cancelacion_comercio', 'cancelacion_administrativa',
    'rechazo_afiliacion', 'suspension_comercio', 'suspension_usuario',
    'despublicacion', 'incidencia_categoria', 'entrega_parcial',
    'pausa_comercio', 'moderacion_valoracion', 'rechazo_cambio_comercio'
  )
)

CONSTRAINT chk_motivo_visible_para
CHECK (visible_para IN ('usuario', 'comercio', 'administracion', 'todos'))

Unicidad

Nombre Definición
unq_motivo_contexto_codigo (contexto, codigo)

Índices

Nombre Definición
idx_motivo_contexto_isactive (contexto) WHERE is_active

Notas de diseño

Una sola tabla en lugar de doce evita doce pantallas de administración con la misma lógica, y los requerimientos describen la gestión de motivos como un módulo único.

require_nota lo consume la familia de triggers trg_<tabla>_motivo_<papel>, una instancia por cada llave foránea a esta tabla. Cada una lo lee desde aquí en vez de copiarlo, porque ninguna de esas reglas alimenta un índice parcial.

Esa familia es además la única barrera contra el riesgo estructural de tener un catálogo discriminado: hoy nada impide que reserva.motivo_id apunte a un motivo de rechazo_afiliacion. La llave foránea garantiza que el motivo existe, no que sea del contexto correcto, y un CHECK no puede consultar la columna contexto de la otra tabla.


categoria_comida

Columnas

codigo, etiqueta, icono (vacío por defecto), activa.

Constraints

CONSTRAINT chk_categoriacomida_codigo_format
CHECK (codigo ~ '^[a-z][a-z0-9_]*$')

Unicidad

Nombre Definición
unq_categoriacomida_codigo (codigo)

Índices

Nombre Definición
idx_categoriacomida_codigo (codigo) WHERE is_active

Notas de diseño

Referenciada con ON DELETE RESTRICT en comercio, plantilla_oferta, publicacion y factor_impacto. Como una categoría con ofertas asociadas solo se desactiva, la llave foránea convierte el borrado en imposible en lugar de improbable.


tipo_negocio

Catálogo simple.

Columnas

codigo, etiqueta, activo.

Constraints

CONSTRAINT chk_tiponegocio_codigo_format
CHECK (codigo ~ '^[a-z][a-z0-9_]*$')

Unicidad

Nombre Definición
unq_tiponegocio_codigo (codigo)

factor_impacto

Factores de conversión por categoría, versionados por vigencia.

  • Régimen: Mutable protegida, no append-only puro: cerrar la vigencia de un factor anterior al insertar uno nuevo requiere un UPDATE acotado sobre vigencia, que ese régimen no admite.
  • Origen:

Columnas

Campo Tipo Nulo Descripción
categoria_comida_id uuid no Llave foránea RESTRICT
kg_por_unidad numeric(6,3) no Kilogramos estimados por unidad rescatada
co2e_por_kg numeric(6,3) no Kilogramos de CO₂e evitados por kilogramo
vigencia daterange no [), con extremo superior abierto si sigue vigente
creado_by uuid no Llave foránea RESTRICT a usuario

Constraints

CONSTRAINT chk_factorimpacto_kg_por_unidad CHECK (kg_por_unidad > 0)
CONSTRAINT chk_factorimpacto_co2e_por_kg CHECK (co2e_por_kg > 0)
CONSTRAINT chk_factorimpacto_vigencia_no_vacia CHECK (NOT isempty(vigencia))

CONSTRAINT excl_factorimpacto_vigencia
EXCLUDE USING gist (categoria_comida_id WITH =, vigencia WITH &&)

Un factor en cero o negativo contamina de forma permanente cualquier cálculo de impacto que lo use, y el régimen de esta tabla no permite corregirlo después salvo cerrando su vigencia, de ahí que ambos límites sean CHECK, no una validación de trigger.

Índices

Nombre Definición
idx_factorimpacto_vigente (categoria_comida_id) WHERE upper_inf(vigencia)

Notas de diseño

Los factores son tabla versionada y no columnas de categoria_comida porque un reporte de impacto ya generado debe seguir siendo defendible después de que el factor cambie. Se usa daterange en lugar de dos columnas porque el operador de solapamiento && es lo que hace posible la restricción de exclusión, que garantiza que una categoría no tenga dos factores vigentes el mismo día.


zona

Barrios y zonas seleccionables como ubicación manual.

Columnas

nombre, municipio_id (llave foránea RESTRICT a municipio), punto_referencia (geography(Point,4326), obligatorio), poligono (geography(Polygon,4326), opcional).

Unicidad

Nombre Definición Estado
unq_zona_ubicacion (punto_referencia, poligono) Vigente
unq_zona_nombre_municipio (municipio_id, upper(immutable_unaccent(nombre))) Vigente

Índices

Nombre Definición Propósito
gist_zona_poligono GIST (poligono) WHERE poligono IS NOT NULL Sostiene la derivación automática de comercio.zona_id

Triggers

Nombre Evento Momento Nivel Regla Origen
trg_zona_punto_en_poligono INSERT
UPDATE
BEFORE ROW Con polígono, punto_referencia debe caer dentro de él RF-A-47
trg_zona_poligono_valido INSERT
UPDATE
BEFORE ROW El polígono debe ser una geometría válida y cerrada RF-A-47

Ninguno puede ser CHECK; ST_Covers y ST_IsValid sobre geography son STABLE, no IMMUTABLE, y una restricción de verificación solo admite expresiones inmutables.

Un polígono autointersecante no lanza error al insertarse; lanza resultados silenciosamente incoherentes cada vez que alguien pregunta si un comercio cae dentro, que es justo lo que este catálogo existe para responder.

Notas de diseño

El polígono es opcional: una zona sirve para que el usuario elija ubicación manual con solo el punto de referencia. Cuando existe, además permite asignar comercios a zona sin intervención manual, mediante trg_comercio_zona.


departamento, municipio

División política administrativa, dos niveles. zona cuelga de municipio y municipio de departamento.

  • Régimen: [append-only]]auditoria
  • Volumen estimado: 17 departamentos, 153 municipios

Columnas

  • departamento: nombre (varchar(30)).
  • municipio: departamento_id (llave foránea RESTRICT) y nombre (varchar(30)).

Unicidad

Nombre Definición
unq_departamento_nombre (upper(immutable_unaccent(nombre)))
unq_municipio_nombre_departamento (departamento_id, upper(immutable_unaccent(nombre)))

Ambas normalizan con upper(immutable_unaccent(...)) por la misma razón que los índices de búsqueda de comercio: "León" y "Leon" son el mismo municipio, y admitir los dos rompería la asignación de zona del comercio que los cita.


zona_cobertura

Área geográfica habilitada del piloto. Versionada por vigencia: nunca se sobrescribe, para no invalidar el motivo histórico de un rechazo de afiliación fuera de cobertura.

Columnas

Campo Tipo Nulo Descripción
tipo varchar(10) no radio
poligono
centro geography(Point,4326) sí Solo si tipo = 'radio'
radio integer sí Metros; solo si tipo = 'radio'
poligono geography(Polygon,4326) sí Solo si tipo = 'poligono'
vigente_desde timestamptz no
vigente_hasta timestamptz sí Marca de cierre

Constraints

CONSTRAINT chk_zonacobertura_variante
CHECK (
  (tipo = 'radio' AND centro IS NOT NULL AND radio IS NOT NULL AND poligono IS NULL)
  OR
  (tipo = 'poligono' AND poligono IS NOT NULL AND centro IS NULL AND radio IS NULL)
)

CONSTRAINT chk_zonacobertura_radio
CHECK (radio IS NULL OR radio BETWEEN 100 AND 100000)

CONSTRAINT chk_zonacobertura_vigencia_orden
CHECK (vigente_hasta IS NULL OR vigente_hasta > vigente_desde)

Sin ella, una vigencia invertida deja el área sin ningún instante en que esté activa, y las solicitudes se marcan fuera de cobertura sin que nada lo explique.

Triggers

Nombre Evento Momento Nivel Regla Origen
trg_zonacobertura_poligono_valido INSERT
UPDATE
BEFORE ROW El polígono debe ser una geometría válida RF-A-31

Igual que su gemelo en zona: un área de cobertura mal formada rechaza afiliaciones legítimas y las acepta al revés, sin producir nunca un error.

Notas de diseño

Se versiona por vigencia y no se sobrescribe porque ampliar la cobertura no debe invalidar el motivo por el que se rechazó una solicitud el mes pasado.


plantilla_mensaje

Textos de las notificaciones automáticas, versionados.

Columnas

Campo Tipo Nulo Descripción
codigo varchar(50) no Nombre del evento que la dispara
canal varchar(10) no push
sms
email
version integer no Incremental por (codigo, canal)
asunto varchar(150) no Vacío salvo en email
cuerpo text no Con marcadores de variable
variables text[] no Nombres de los marcadores que cuerpo espera
estado_id uuid no Llave foránea a estado_plantilla_mensaje
vigente_desde timestamptz sí
vigente_hasta timestamptz sí Marca de cierre
creada_by uuid no Llave foránea RESTRICT a usuario

variables es un arreglo nativo de texto, no un segundo jsonb describiendo la forma esperada por convención, mismo criterio que alerta_especificacion.claves_requeridas más abajo: Postgres exige por tipo de columna que sea una lista de texto, sin necesitar un CHECK de jsonb_typeof.

Constraints

CONSTRAINT chk_plantillamensaje_version CHECK (version >= 1)
CONSTRAINT chk_plantillamensaje_canal CHECK (canal IN ('push', 'sms', 'email'))

CONSTRAINT chk_plantillamensaje_asunto_email
CHECK (canal <> 'email' OR length(asunto) > 0)

CONSTRAINT chk_plantillamensaje_vigencia_coherente
CHECK (vigente_hasta IS NULL OR vigente_desde IS NOT NULL)

Unicidad

Nombre Definición
unq_plantillamensaje_codigo_canal_version (codigo, canal, version)

No hay una constraint de base de datos para "una sola plantilla vigente por código y canal": esa unicidad depende de una columna del catálogo de estado relacionado (estado_plantilla_mensaje.vigente), no de una columna propia, así que Django no puede expresarla como índice único parcial. La impone trg_plantillamensaje_vigente_unica.

Triggers

Nombre Evento Momento Nivel Regla Origen
trg_plantillamensaje_vigente_unica INSERT
UPDATE
BEFORE ROW Una sola fila con estado vigente por (codigo, canal) RF-A-50
trg_plantillamensaje_version INSERT BEFORE ROW version es el consecutivo siguiente de su (codigo, canal) RF-A-50
trg_plantillamensaje_variables INSERT
UPDATE
BEFORE ROW variables y los marcadores de cuerpo deben coincidir exactamente RF-A-50
trg_plantillamensaje_allow_edit UPDATE BEFORE ROW asunto, cuerpo y variables solo cambian si el estado tiene allow_edit RF-A-50

trg_plantillamensaje_variables recorre cuerpo con una expresión regular y compara el conjunto de marcadores encontrados contra el arreglo variables, en las dos direcciones: un marcador sin declarar se enviaría literal al usuario, y una variable declarada de más haría fallar a trg_campana_variables_especificacion, que exige que la campaña traiga cada una de ellas.

trg_plantillamensaje_allow_edit es lo que hace real al versionado. Sin él, allow_edit del catálogo de estado es una etiqueta que nadie lee, y editar el cuerpo de la versión vigente reescribiría el texto que RF-A-50 promete poder reconstruir desde cada notificacion enviada.

Notas de diseño

Cada notificacion enviada guarda plantilla_id, de modo que el texto exacto que recibió el usuario es reconstruible aunque la plantilla se haya editado después: el snapshot ocurre dentro del registro que consume el dato volátil, mismo criterio que D-15.


media

Tabla única de archivos.

Columnas

Campo Tipo Nulo Descripción
tipo varchar(20) no logo
foto_local
foto_oferta
fachada
licencia
avatar
exportacion
mime varchar(100) no
bytes integer no
width integer sí Nulo en documentos
height integer sí
storage_key varchar(255) no Ruta en el almacenamiento de objetos, única
variantes jsonb no Un objeto por variante generada, ver el trigger de especificación abajo
checksum varchar(64) no
subida_by uuid no Llave foránea RESTRICT a usuario

Constraints

CONSTRAINT chk_media_bytes CHECK (bytes > 0 AND bytes <= 5242880)

CONSTRAINT chk_media_dimensiones_coherentes
CHECK ((width IS NULL) = (height IS NULL))

CONSTRAINT chk_media_logo_dimensiones
CHECK (tipo <> 'logo' OR (width >= 400 AND height >= 400))

CONSTRAINT chk_media_tipo
CHECK (tipo IN ('logo', 'foto_local', 'foto_oferta', 'fachada', 'licencia', 'avatar', 'exportacion'))

CONSTRAINT chk_media_variantes_tipo
CHECK (jsonb_typeof(variantes) = 'object')

CONSTRAINT chk_media_checksum_formato CHECK (checksum ~ '^[0-9a-f]{64}$')
CONSTRAINT chk_media_mime_formato CHECK (mime ~ '^[a-z]+/[a-zA-Z0-9.+-]+$')

CONSTRAINT chk_media_imagen_dimensiones
CHECK (mime NOT LIKE 'image/%' OR width IS NOT NULL)

checksum se declara como SHA-256 en la ficha y nada lo obliga a serlo; una cadena vacía pasa hoy sin objeción, y el checksum existe precisamente para detectar que el archivo cambió.

Unicidad

Nombre Definición
unq_media_storagekey (storage_key)

trg_media_variantes_especificacion

BEFORE INSERT OR UPDATE, ROW. variantes guarda una clave por variante generada (por ejemplo, un tamaño comprimido para el modo bajo consumo de datos); el trigger recorre jsonb_object_keys(variantes) y exige que cada entrada sea un objeto, y que sus campos storage_key/width/height, cuando están presentes, tengan el tipo JSON correcto (string o number). Es el mismo problema que resuelve chk_parametrosistema_valor_tipo aplicado a un jsonb con forma libre: sin esta barrera, nada impide una variante con width: "<número enorme>" insertada por fuera del servicio que arma la respuesta.

Notas de diseño

El límite de 5 MB y el mínimo de dimensiones del logo viven en la base, no en el formulario: una validación de formulario protege contra el usuario distraído, un CHECK protege además contra el script de carga masiva, la migración de datos y el segundo cliente que alguien escriba dentro de un año.


Versiones publicadas de términos de uso, privacidad y términos comerciales.

  • Régimen: Mutable protegida, no append-only puro, misma desviación que factor_impacto: cerrar la vigencia de la versión anterior al publicar la siguiente exige un UPDATE acotado.
  • Origen:

Columnas

Campo Tipo Nulo Descripción
tipo varchar(20) no terminos_usuario
privacidad
terminos_comercio
version smallint no
hash varchar(64) no SHA-256 del texto publicado; prueba de que lo aceptado sigue siendo lo vigente
url varchar no
vigencia daterange no [), media abierta si sigue vigente
creado_by uuid no Llave foránea RESTRICT a usuario

Constraints

CONSTRAINT chk_documentolegal_tipo
CHECK (tipo IN ('terminos_usuario', 'privacidad', 'terminos_comercio'))

CONSTRAINT chk_documentolegal_vigencia_no_vacia CHECK (NOT isempty(vigencia))

CONSTRAINT excl_documentolegal_vigencia
EXCLUDE USING gist (tipo WITH =, vigencia WITH &&)

CONSTRAINT chk_documentolegal_hash_formato CHECK (hash ~ '^[0-9a-f]{64}$')

Unicidad

Nombre Definición
unq_documentolegal_tipo_version (tipo, version)

Índices

Nombre Definición
idx_documentolegal_vigente (tipo) WHERE upper_inf(vigencia)

Triggers

Nombre Evento Momento Nivel Regla Origen
trg_readonly_snapshot UPDATE OF tipo
UPDATE OF version
UPDATE OF url
UPDATE OF hash
BEFORE ROW Esas cuatro columnas son inmutables D-07
trg_documentolegal_version INSERT BEFORE ROW version es el consecutivo siguiente de su tipo, y su vigencia empieza donde terminó la anterior RF-A-47

trg_documentolegal_version cubre lo que excl_documentolegal_vigencia no alcanza: la exclusión impide que dos versiones se solapen, pero no impide que la versión 5 vigente desde enero conviva con la versión 3 vigente desde marzo si alguien las crea fuera de orden. El trigger deriva version como el consecutivo del tipo y encadena vigencia a partir de donde terminó la anterior, de modo que reconstruir "qué estaba vigente cuando este usuario aceptó" siempre es una lectura directa del número de versión.

trg_readonly_snapshot cubre tipo, version, url y hash: el texto publicado nunca se edita ni se re-etiqueta. Lo único mutable de una fila ya creada es el cierre de su propia vigencia al publicar la siguiente versión, que es exactamente el UPDATE acotado que motiva el régimen mutable protegida en vez de append-only.


aceptacion_terminos

Registro de que una cuenta aceptó una versión concreta de un documento legal.

  • Régimen: append-only: una aceptación no se edita ni se retira nunca; retirar el consentimiento es aceptar (o no) la versión siguiente, no la desaparición de la anterior.

Columnas

Campo Tipo Nulo Descripción
usuario_id uuid no Llave foránea RESTRICT
documento_id uuid no Llave foránea RESTRICT a documento_legal
aceptado_at timestamptz no Instante declarado de la aceptación

Constraints

CONSTRAINT chk_aceptacionterminos_aceptadoat
CHECK (aceptado_at > created_at)

Que el instante declarado sea posterior a created_at (el instante en que la fila realmente se guardó) descarta una aceptación cuyo reloj declarado adelanta al del servidor.

Unicidad

Nombre Definición
unq_aceptacionterminos_ambito (usuario_id, documento_id)

Índices

Nombre Definición
idx_aceptacionterminos_aceptadoat (aceptado_at)

Triggers

Nombre Evento Momento Nivel Regla Origen
trg_aceptacionterminos_vigencia INSERT BEFORE ROW El documento aceptado debe estar vigente en aceptado_at RF-U-05

Sin él, nada impediría registrar la aceptación de una versión retirada hace un año, y la tabla existe para poder demostrar lo contrario: RF-U-05 y RF-C-08 piden constancia de qué versión aceptó cada quien, y una constancia que admite versiones imposibles no prueba nada. El trigger compara aceptado_at::date contra el daterange del documento.

Notas de diseño

Cualquier ApiUser puede aceptar, no solo quienes tienen perfil de consumidor: el personal de comercio también acepta los términos comerciales. Registrar una aceptación es idempotente por construcción: la unicidad (usuario_id, documento_id) hace que un segundo intento sobre el mismo par no duplique fila.


alerta_especificacion

Especificación de cada disparador válido de alerta: mismo problema que resolvía parametro_sistema antes de su diseño actual: alerta.datos es un jsonb de forma libre, y sin esta tabla nada fuera de la aplicación impide un datos incompleto o con las claves equivocadas.

Columnas

Campo Tipo Nulo Descripción
codigo varchar(40) no Código del disparador concreto, único
tipo varchar(16) no moderacion
cumplimiento
seguridad
entidad_tipo varchar(20) no publicacion
comercio
usuario
reserva
parametro
exportacion
claves_requeridas text[] no Claves que alerta.datos debe traer para este codigo

Constraints

CONSTRAINT chk_alertaespecificacion_codigo_format
CHECK (codigo ~ '^[a-z][a-z0-9_]*$')

CONSTRAINT chk_alertaespecificacion_tipo
CHECK (tipo IN ('moderacion', 'cumplimiento', 'seguridad'))

CONSTRAINT chk_alertaespecificacion_entidad_tipo
CHECK (entidad_tipo IN ('publicacion', 'comercio', 'usuario', 'reserva', 'parametro', 'exportacion'))

Unicidad

Nombre Definición
unq_alertaespecificacion_codigo (codigo)

Notas de diseño

claves_requeridas es text[] nativo, no un segundo jsonb describiendo la forma de otro: el mismo error de diseño que tenía parametro_sistema antes de fijar una sola columna valor. El trigger que consume esta tabla vive en alerta, documentado en la ficha de esa tabla.

Retirar un codigo es marcarlo inactivo, no un DELETE: alerta.codigo no lleva llave foránea (es una referencia polimórfica), así que las alertas históricas con ese código siguen existiendo sin problema.