Ofertas¶
La distinción conceptual más importante del modelo:
Plantilla y publicación no son lo mismo:
- Lo que el comercio configura una vez y reutiliza.
- No tiene stock, ni fecha, ni estado de publicación.
- La instancia concreta de un día y una ventana.
- Tiene stock propio, precio congelado y estado propio.
- Es lo que el usuario ve y reserva.
Confundirlas haría imposible cambiar el precio de mañana sin reescribir el comprobante de ayer, que es justamente lo que el snapshot obligatorio evita.
También viven aquí plantilla_programacion y excepcion_calendario: ambas
dependen de plantilla_oferta, así que documentarlas junto a
Horarios formaría una dependencia circular entre módulos.
Requerimientos cubiertos¶
RF-U-16RF-U-17RF-U-18RF-U-19RF-U-20RF-U-21RF-U-22RF-U-23RF-C-19RF-C-21RF-C-25RF-C-26RF-C-27RF-C-28RF-C-29RF-C-30RF-C-31RF-C-32RF-C-33RF-C-34RF-C-35RF-A-23RF-A-24RF-A-25RF-A-40RF-A-41
Alérgenos
Este módulo no almacena alérgenos como dato estructurado. La declaración corresponde al comercio dentro de la descripción de la oferta, que es donde el requerimiento de la oferta específica la exige.
plantilla_oferta¶
Configuración reutilizable de una oferta. Máximo cinco activas por comercio.
- Régimen: Mutable rastreada
- Volumen estimado: 150-400 filas
- Origen:
Columnas¶
| Campo | Tipo | Nulo | Predeterminado | Descripción |
|---|---|---|---|---|
comercio_id |
uuid |
no | Llave foránea RESTRICT |
|
nombre |
varchar(60) |
no | ||
tipo |
varchar(20) |
no | pack_sorpresaoferta_especifica |
|
categoria_comida_id |
uuid |
no | Obligatoria incluso en pack sorpresa | |
descripcion |
text |
no | '' |
Vacía en pack sorpresa |
foto_id |
uuid |
sí | Obligatoria en oferta específica | |
precio |
numeric(10,2) |
no | ||
valor_referencia |
numeric(10,2) |
no | ||
cantidad_default |
smallint |
no | 1-50 | |
is_active |
boolean |
no | true |
Nunca se borra: hay publicaciones que la referencian |
creada_by |
uuid |
no | Llave foránea RESTRICT a usuario |
Llaves foráneas¶
| Columna | Referencia | ON DELETE |
Notas |
|---|---|---|---|
comercio_id |
comercio |
RESTRICT |
|
categoria_comida_id |
categoria_comida |
RESTRICT |
Catálogo administrable |
foto_id |
media |
RESTRICT |
Las publicaciones la congelan |
Constraints¶
CONSTRAINT chk_plantillaoferta_nombre_longitud CHECK (length(nombre) >= 1)
CONSTRAINT chk_plantillaoferta_variante
CHECK (
(tipo = 'pack_sorpresa' AND descripcion = '' AND foto_id IS NULL)
OR
(
tipo = 'oferta_especifica'
AND length(descripcion) BETWEEN 20 AND 200
AND foto_id IS NOT NULL
)
)
CONSTRAINT chk_plantillaoferta_precio CHECK (precio > 0)
CONSTRAINT chk_plantillaoferta_valor_referencia CHECK (valor_referencia > precio)
CONSTRAINT chk_plantillaoferta_cantidad_default CHECK (cantidad_default BETWEEN 1 AND 50)
chk_plantillaoferta_variante reúne en una sola restricción las reglas del
pack sorpresa y de la oferta específica. Está expresada como disyunción de
dos variantes completas, y no como una lista de CHECK condicionales, para
que el mensaje de error identifique la variante violada en lugar de una
regla suelta.
Unicidad¶
| Nombre | Definición | Propósito |
|---|---|---|
unq_plantillaoferta_comercio_nombre_isactive |
(comercio_id, lower(nombre)) WHERE is_active |
Dos plantillas activas homónimas confundirían al operador |
Índices¶
| Nombre | Definición | Propósito |
|---|---|---|
idx_plantillaoferta_comercio |
(comercio_id) WHERE is_active |
Panel de plantillas y conteo del tope |
Es el índice de apoyo de trg_plantillaoferta_maximo_activas.
Triggers¶
| Nombre | Evento | Momento | Nivel | Regla | Origen |
|---|---|---|---|---|---|
trg_plantillaoferta_maximo_activas |
INSERTUPDATE |
BEFORE |
ROW |
Plantillas activas del comercio dentro de plantilla.max_activas |
RF-C-27 |
trg_plantillaoferta_precio |
INSERTUPDATE |
BEFORE |
ROW |
precio >= precio.minimo y descuento sobre precio.descuento_minimo_pct |
RF-C-30 |
trg_plantillaoferta_items |
UPDATE |
BEFORE |
ROW |
Un pack sorpresa activo tiene entre 3 y 8 ítems; una oferta específica, ninguno | RF-C-28 |
chk_plantillaoferta_valor_referencia ya exige que el valor de referencia
supere al precio, pero no cuánto. El descuento mínimo del treinta por ciento
que fija RF-C-30 es configurable por RF-A-41, así
que no cabe en un CHECK: lo lee trg_plantillaoferta_precio desde
parametro_sistema, junto con el precio mínimo.
trg_plantillaoferta_items es el que traduce "una plantilla de este tipo no
puede activarse con menos de tres elementos en su lista"
(RF-C-28). Cuenta filas hijas, así que es trigger, y corre al
activar y no al crear: una plantilla en construcción puede estar incompleta
mientras is_active sea falso.
plantilla_oferta_item¶
Lista "puede contener" del pack sorpresa.
- Régimen: Mutable rastreada
- Origen:
Columnas¶
| Campo | Tipo | Nulo | Descripción |
|---|---|---|---|
plantilla_oferta_id |
uuid |
no | Llave foránea CASCADE |
texto |
varchar(60) |
no | Elemento que el pack contiene |
orden |
smallint |
no | 0 por defecto; el orden que eligió el comercio |
Llaves foráneas¶
| Columna | Referencia | ON DELETE |
Notas |
|---|---|---|---|
plantilla_oferta_id |
plantilla_oferta |
CASCADE |
Un elemento no significa nada fuera de su plantilla |
Constraints¶
CONSTRAINT chk_plantillaofertaitem_texto_longitud CHECK (length(texto) >= 1)
Unicidad¶
| Nombre | Definición | Propósito |
|---|---|---|
unq_plantillaofertaitem_plantilla_orden |
(plantilla_oferta_id, orden) |
El orden que el comercio eligió, no el de inserción |
plantilla_programacion¶
Programación semanal de cada plantilla de oferta. Es lo que alimenta la generación automática de publicaciones diarias.
- Régimen: Mutable rastreada
- Volumen estimado: siete filas por plantilla
- Origen:
Columnas¶
| Campo | Tipo | Nulo | Descripción |
|---|---|---|---|
plantilla_oferta_id |
uuid |
no | Llave foránea CASCADE |
dia_semana |
smallint |
no | 0-6 |
ventana_id |
uuid |
no | Llave foránea RESTRICT |
cantidad_default |
smallint |
no | 1-50 |
is_active |
boolean |
no | Si es falso, ese día no se genera publicación |
ventana_id y cantidad_default son siempre obligatorias, incluso cuando
is_active es falso: desactivar un día no borra su configuración, solo
suspende la generación.
Llaves foráneas¶
| Columna | Referencia | ON DELETE |
Notas |
|---|---|---|---|
plantilla_oferta_id |
plantilla_oferta |
CASCADE |
Es un fragmento de la plantilla, sin identidad propia |
ventana_id |
ventana_recogida |
RESTRICT |
La ventana tiene vida propia y la comparten varias plantillas |
Constraints¶
CONSTRAINT chk_plantillaprogramacion_diasemana CHECK (dia_semana BETWEEN 0 AND 6)
CONSTRAINT chk_plantillaprogramacion_cantidad_default
CHECK (cantidad_default BETWEEN 1 AND 50)
Unicidad¶
| Nombre | Definición |
|---|---|
unq_plantillaprogramacion_plantilla_dia_ventana |
(plantilla_oferta_id, dia_semana, ventana_id) |
Índices¶
| Nombre | Definición |
|---|---|
idx_plantillaprogramacion_diasemana |
(dia_semana) WHERE is_active |
Lo recorre la generación diaria de publicaciones.
Triggers¶
| Nombre | Evento | Momento | Nivel | Regla | Origen |
|---|---|---|---|---|---|
trg_plantillaprogramacion_ventana |
INSERTUPDATE |
BEFORE |
ROW |
La ventana debe ser del mismo comercio que la plantilla y del mismo dia_semana |
RF-C-19 |
RF-C-19 lo enuncia palabra por
palabra: "la ventana asignada debe pertenecer al mismo comercio y al mismo
día de la semana". Son dos columnas de ventana_recogida y una de
plantilla_oferta, tres tablas en juego, así que no hay CHECK ni llave
foránea compuesta que lo exprese sin inventar índices redundantes en los dos
padres.
Sin él, la generación diaria produce publicaciones cuya ventana pertenece a
otro comercio o a otro día, y los cinco instantes que
trg_publicacion_instantes deriva de esa ventana quedan resueltos contra
horas que no corresponden a nada.
excepcion_calendario¶
Excepciones por fecha concreta, sin alterar la programación semanal.
- Régimen: Mutable rastreada
- Volumen estimado: decenas de filas por comercio y año
- Origen:
Columnas¶
| Campo | Tipo | Nulo | Descripción |
|---|---|---|---|
plantilla_oferta_id |
uuid |
no | Llave foránea CASCADE |
fecha |
date |
no | Fecha operativa afectada |
tipo |
varchar(20) |
no | cerradomodifica_ventanamodifica_cantidad |
ventana_id |
uuid |
sí | Requerida solo si modifica_ventana |
cantidad |
smallint |
sí | Requerida solo si modifica_cantidad |
Llaves foráneas¶
| Columna | Referencia | ON DELETE |
Notas |
|---|---|---|---|
plantilla_oferta_id |
plantilla_oferta |
CASCADE |
|
ventana_id |
ventana_recogida |
RESTRICT |
Solo si modifica_ventana |
Constraints¶
CONSTRAINT chk_excepcioncalendario_variante
CHECK (
(tipo = 'cerrado' AND ventana_id IS NULL AND cantidad IS NULL)
OR
(tipo = 'modifica_ventana' AND ventana_id IS NOT NULL AND cantidad IS NULL)
OR
(tipo = 'modifica_cantidad' AND ventana_id IS NULL AND cantidad IS NOT NULL)
)
CONSTRAINT chk_excepcioncalendario_cantidad
CHECK (cantidad IS NULL OR cantidad BETWEEN 1 AND 50)
Unicidad¶
| Nombre | Definición | Propósito |
|---|---|---|
unq_excepcioncalendario_plantilla_fecha_tipo |
(plantilla_oferta_id, fecha, tipo) |
Sin dos excepciones del mismo tipo por fecha |
Puede haber más de una excepción para la misma fecha si son de tipo
distinto (una modifica_ventana y otra modifica_cantidad el mismo día),
pero no dos del mismo tipo.
Triggers¶
| Nombre | Evento | Momento | Nivel | Regla | Origen |
|---|---|---|---|---|---|
trg_excepcioncalendario_horizonte |
INSERTUPDATE |
BEFORE |
ROW |
fecha entre hoy y noventa días hacia adelante |
RF-C-21 |
trg_excepcioncalendario_cierre |
INSERTUPDATE |
BEFORE |
ROW |
Un cerrado excluye cualquier otra excepción de la misma fecha |
RF-C-21 |
trg_excepcioncalendario_ventana |
INSERTUPDATE |
BEFORE |
ROW |
La ventana sustituta debe ser del comercio y del día de la semana de fecha |
RF-C-21 |
trg_excepcioncalendario_cierre es la mitad de RF-C-21 que la
unicidad no alcanza: "un cierre excluye cualquier otra excepción del mismo
día". unq_excepcioncalendario_plantilla_fecha_tipo admite hoy una
cerrado y una modifica_cantidad sobre la misma fecha, que es una
contradicción, porque un día cerrado no tiene cantidad que modificar. La
regla mira filas hermanas, así que es trigger, y corre en las dos
direcciones: rechaza un cerrado si ya hay otras excepciones ese día, y
rechaza cualquier otra si ya hay un cerrado.
trg_excepcioncalendario_horizonte no puede ser CHECK porque compara
contra hz_hoy(), que no es inmutable. El límite de noventa días es el que
RF-C-21 fija de forma fija, no configurable, así que no es regla
parametrizada.
publicacion¶
La instancia diaria. Es la tabla más leída del sistema.
- Régimen: Mutable protegida
- Volumen estimado: 100-300 filas diarias
- Origen:
Columnas¶
| Campo | Tipo | Nulo | Descripción |
|---|---|---|---|
comercio_id |
uuid |
no | Denormalizado: evita una unión en cada consulta del mapa |
plantilla_oferta_id |
uuid |
sí | Nulo si la publicación fue manual |
ventana_id |
uuid |
no | |
fecha_operativa |
date |
no | Fecha local |
estado_id |
uuid |
no | Llave foránea a estado_publicacion |
origen |
varchar(12) |
no | automaticamanualduplicada |
tipo |
varchar(20) |
no | Snapshot de la plantilla |
nombre |
varchar(60) |
no | Snapshot |
descripcion |
text |
no | Snapshot |
categoria_comida_id |
uuid |
no | Snapshot |
foto_id |
uuid |
sí | Snapshot |
precio |
numeric(10,2) |
no | Snapshot |
valor_referencia |
numeric(10,2) |
no | Snapshot |
descuento_pct |
numeric(6,2) |
no | Generada almacenada |
cantidad_total |
smallint |
no | 0-50 |
cantidad_reservada |
smallint |
no | default 0, mutada por ajustar_stock |
cantidad_entregada |
smallint |
no | default 0, mutada por registrar_entrega |
cantidad_disponible |
smallint |
no | Generada virtual |
reservable_desde_at |
timestamptz |
no | |
reservas_cierran_at |
timestamptz |
no | |
ventana_inicio_at |
timestamptz |
no | |
ventana_fin_at |
timestamptz |
no | |
validacion_hasta_at |
timestamptz |
no | |
publicada_at |
timestamptz |
sí | Marca de apertura de reservas |
agotado_at |
timestamptz |
sí | |
cerrada_at |
timestamptz |
sí | Marca de cierre |
aptitud_declarada_at |
timestamptz |
sí | RF-C-35 |
aptitud_declarada_by |
uuid |
sí | Llave foránea RESTRICT a usuario_comercio |
duplicada_de_id |
uuid |
sí | Autorreferencia RESTRICT |
creada_by |
uuid |
sí | Nulo si la generó el proceso programado |
Columnas generadas¶
descuento_pct numeric(6,2)
GENERATED ALWAYS AS (100 * (1 - precio / valor_referencia)) STORED
cantidad_disponible smallint
GENERATED ALWAYS AS (cantidad_total - cantidad_reservada) VIRTUAL
descuento_pct es almacenada porque la moderación de contenido y la
alerta de descuento excesivo la consultan por umbral. cantidad_disponible
es virtual porque nunca se indexa: cualquier consulta que la necesite usa
cantidad_reservada < cantidad_total directamente.
Llaves foráneas¶
| Columna | Referencia | ON DELETE |
Notas |
|---|---|---|---|
estado_id |
estado_publicacion |
RESTRICT |
|
comercio_id |
comercio |
RESTRICT |
Denormalizado, escrito una sola vez |
plantilla_oferta_id |
plantilla_oferta |
RESTRICT |
|
ventana_id |
ventana_recogida |
RESTRICT |
|
categoria_comida_id |
categoria_comida |
RESTRICT |
|
foto_id |
media |
RESTRICT |
La foto congelada no puede desaparecer |
duplicada_de_id |
publicacion |
RESTRICT |
|
aptitud_declarada_by |
usuario_comercio |
RESTRICT |
Constraints¶
CONSTRAINT chk_publicacion_stock_coherente
CHECK (cantidad_reservada >= 0 AND cantidad_reservada <= cantidad_total)
CONSTRAINT chk_publicacion_entregado_coherente
CHECK (cantidad_entregada >= 0 AND cantidad_entregada <= cantidad_reservada)
CONSTRAINT chk_publicacion_orden_temporal
CHECK (
reservable_desde_at <= reservas_cierran_at
AND reservas_cierran_at <= ventana_inicio_at
AND ventana_inicio_at <= ventana_fin_at
AND ventana_fin_at <= validacion_hasta_at
)
CONSTRAINT chk_publicacion_publicada_con_aptitud
CHECK (publicada_at IS NULL OR aptitud_declarada_at IS NOT NULL)
CONSTRAINT chk_publicacion_variante
CHECK (
(tipo = 'pack_sorpresa' AND descripcion = '' AND foto_id IS NULL)
OR
(
tipo = 'oferta_especifica'
AND length(descripcion) BETWEEN 20 AND 200
AND foto_id IS NOT NULL
)
)
CONSTRAINT chk_publicacion_precio_positivo CHECK (precio > 0)
CONSTRAINT chk_publicacion_valor_referencia_mayor CHECK (valor_referencia > precio)
CONSTRAINT chk_publicacion_cantidad_total CHECK (cantidad_total BETWEEN 0 AND 50)
CONSTRAINT chk_publicacion_origen CHECK (origen IN ('automatica', 'manual', 'duplicada'))
CONSTRAINT chk_publicacion_origen_autor
CHECK ((origen = 'automatica') = (creada_by IS NULL))
CONSTRAINT chk_publicacion_origen_duplicada
CHECK ((origen = 'duplicada') = (duplicada_de_id IS NOT NULL))
CONSTRAINT chk_publicacion_orden_marcas
CHECK (
(agotado_at IS NULL OR (publicada_at IS NOT NULL AND agotado_at >= publicada_at))
AND (cerrada_at IS NULL OR (publicada_at IS NOT NULL AND cerrada_at >= publicada_at))
)
chk_publicacion_origen_autor traduce la única diferencia real entre los
tres orígenes: RF-C-23 genera desde un proceso, que no tiene
usuario, y RF-C-24 y RF-C-34 los crea una persona.
Hoy creada_by admite nulo sin condición, de modo que una publicación manual
puede quedar sin autor y la bitácora de RF-C-14 no tiene a quién
nombrar.
chk_publicacion_stock_coherente es una de las invariantes críticas
del sistema. Combinada con que trg_reserva_ajustar_stock mueve
cantidad_reservada dentro de la misma transacción que crea la reserva,
convierte la sobreventa en un error de transacción sin SELECT ... FOR UPDATE
explícito y sin ventana de carrera entre la lectura y la escritura.
chk_publicacion_variante/_precio_positivo/_valor_referencia_mayor se
repiten aquí, ya declaradas también en plantilla_oferta, porque una
publicación manual (plantilla_oferta_id IS NULL) puede traer estos
valores propios sin haber pasado nunca por una plantilla, y el ReadOnly
de abajo solo impide modificarlos después de creados, no valida su forma
al insertar.
Unicidad¶
| Nombre | Definición | Propósito |
|---|---|---|
unq_publicacion_dia |
(comercio_id, plantilla_oferta_id, fecha_operativa, ventana_id), NULLS NOT DISTINCT |
Idempotencia del generador diario |
NULLS NOT DISTINCT es lo que hace idempotente a
generar_publicaciones_del_dia incluso cuando plantilla_oferta_id es
nulo (publicación manual): sin él, dos publicaciones manuales del mismo
comercio, fecha y ventana no colisionarían. Es también lo que cumple el "no
puede existir más de una publicación de la misma plantilla para la misma
fecha y ventana" de RF-C-24 sin ninguna comprobación previa.
Triggers¶
| Nombre | Evento | Momento | Nivel | Regla | Origen |
|---|---|---|---|---|---|
trg_readonly_snapshot |
UPDATE de los snapshot |
BEFORE |
ROW |
Los campos congelados son inmutables tras la creación | D-15 |
trg_publicacion_comercio_apto |
INSERT |
BEFORE |
ROW |
El comercio puede publicar y no tiene una pausa vigente | RF-C-22 |
trg_publicacion_ventana |
INSERT |
BEFORE |
ROW |
La ventana es del comercio, está activa y su dia_semana coincide con fecha_operativa |
RF-C-19 |
trg_publicacion_snapshot |
INSERT |
BEFORE |
ROW |
Copia tipo, nombre, descripcion, categoria_comida_id, foto_id, precio y valor_referencia desde la plantilla |
D-15 |
trg_publicacion_instantes |
INSERT |
BEFORE |
ROW |
Deriva los cinco instantes de la ventana y la configuración | RF-C-25 |
trg_publicacion_horizonte |
INSERT |
BEFORE |
ROW |
fecha_operativa dentro de publicacion.horizonte_horas |
RF-C-24 |
trg_publicacion_precio |
INSERTUPDATE |
BEFORE |
ROW |
Precio, descuento y cantidad contra parametro_sistema |
RF-C-30 |
trg_publicacion_aptitud |
UPDATE OF aptitud_declarada_by |
BEFORE |
ROW |
Quien declara la aptitud es miembro activo del comercio | RF-C-35 |
trg_publicacion_agotado |
UPDATE OF cantidad_* |
BEFORE |
ROW |
Mueve el estado entre publicada y agotada según la disponibilidad |
RF-C-32 |
trg_publicacion_alerta_moderacion |
INSERTUPDATE |
AFTER |
ROW |
Levanta alerta de moderación al cruzar un umbral de contenido | RF-A-25 |
trg_readonly_snapshot cubre comercio_id, plantilla_oferta_id,
ventana_id, fecha_operativa, origen, tipo, nombre, descripcion,
categoria_comida_id, foto_id, precio, valor_referencia y los cinco
instantes. Los campos mutables son estado_id, cantidad_total,
cantidad_reservada, cantidad_entregada, publicada_at, agotado_at,
cerrada_at y aptitud_declarada_at/_by.
cantidad_total queda deliberadamente fuera de esa lista: dejarla de solo
lectura habría hecho imposibles tres requerimientos a la vez —
RF-C-31 ("el sistema impide reducir la cantidad total por
debajo de las unidades ya reservadas", que presupone que se puede reducir),
RF-C-32 ("el comercio puede aumentar o reducir la cantidad
durante la ventana") y RF-C-33 (marcar como agotado "iguala la
cantidad total a las unidades ya reservadas"). Lo que la protege no es
ReadOnly: chk_publicacion_stock_coherente ya impide bajarla por debajo
de cantidad_reservada, que es exactamente el límite de
RF-C-31, y trg_publicacion_agotado se encarga del efecto en
el estado.
trg_publicacion_instantes¶
La pieza que hace honesto el snapshot temporal. Los cinco instantes son hoy
datos de entrada: chk_publicacion_orden_temporal comprueba que estén
ordenados entre sí, no que correspondan a la ventana que la fila referencia.
El trigger los deriva y sobrescribe lo que haya mandado el llamador:
| Columna | Se resuelve como |
|---|---|
ventana_inicio_at |
fecha_operativa + ventana.hora_inicio, en America/Managua |
ventana_fin_at |
fecha_operativa + ventana.hora_fin, en America/Managua |
validacion_hasta_at |
ventana_fin_at + validacion.tolerancia_min |
reservas_cierran_at |
ventana_fin_at − publicacion.corte_reservas_min |
reservable_desde_at |
now() si comercio.reservable_desde_generacion; si no, ventana_inicio_at − comercio.horas_antes_reservable |
Es también la única instancia de la familia de derivación que consume
parámetros, y donde D-16 se paga: la tolerancia y el corte se leen
una vez, en la escritura, y quedan congelados en la fila. Un cambio
posterior del parámetro no mueve las publicaciones ya creadas, que es lo que
las notas de diseño de abajo describen como "el contrato con quien ya
reservó".
trg_publicacion_agotado¶
RF-C-32 describe una transición que el sistema hace solo: "si la
disponibilidad llega a cero, la publicación pasa a agotada; si vuelve a ser
positiva, regresa a publicada". Los dos pares están declarados en
transicion_publicacion con actor sistema, así que el trigger no inventa
nada: calcula cantidad_total - cantidad_reservada y mueve estado_id, y
trg_publicacion_transicion_valida lo valida como a cualquier otro cambio.
Corre BEFORE y en la misma sentencia que mueve el contador, que es lo que
permite cumplir el "treinta segundos o menos" de RF-C-32 sin
ningún proceso que barra publicaciones buscando ceros.
trg_publicacion_alerta_moderacion¶
RF-A-25 enumera cinco disparadores y cuatro de ellos dependen
solo de esta fila y de parametro_sistema: descuento sobre
precio.descuento_revision_pct, precio bajo precio.minimo, oferta
específica sin foto y cantidad mayor a treinta. El quinto, la primera
publicación de un comercio recién aprobado, se resuelve contando las
publicaciones anteriores del comercio.
Es AFTER INSERT OR UPDATE e inserta en alerta con
ON CONFLICT ON CONSTRAINT unq_alerta_abierta DO NOTHING, que es lo que
traduce el "no se acumulan alertas idénticas sin atender sobre la misma
publicación" del requerimiento a control de flujo en vez de a un error.
Índices¶
| Nombre | Definición | Propósito |
|---|---|---|
idx_publicacion_directorio |
(estado_id, comercio_id, fecha_operativa) |
Directorio y exploración de ofertas |
idx_publicacion_cierre |
(estado_id, reservas_cierran_at) |
Barrido del proceso de cierre de ventana |
Notas de diseño¶
Los cinco instantes se resuelven al crear la publicación y no se recalculan: si el comercio edita la ventana de recogida mañana, las publicaciones de hoy conservan los suyos, son el contrato con quien ya reservó.
comercio_id está denormalizado desde la plantilla aunque sea derivable.
La consulta del directorio filtra y agrupa por comercio en cada carga, y
trg_readonly_snapshot garantiza que la copia no pueda contradecir al
original.
publicacion_item¶
Copia congelada de plantilla_oferta_item al publicar. Copia y no
referencia porque el comprobante que el usuario consulta meses después
debe mostrar lo que decía la oferta ese día.
- Régimen:
append-only - Origen:
Columnas¶
| Campo | Tipo | Nulo | Descripción |
|---|---|---|---|
publicacion_id |
uuid |
no | Llave foránea CASCADE |
texto |
varchar(60) |
no | Copia congelada |
orden |
smallint |
no | 0 por defecto; el orden en que el usuario vio la lista al reservar |
Constraints¶
CONSTRAINT chk_publicacionitem_texto_longitud CHECK (length(texto) >= 1)
Unicidad¶
Ninguna, además de la llave primaria, y por la misma razón que
plantilla_oferta_item: una unq_publicacionitem_publicacion limitaría a un
ítem por publicación y haría imposible copiar la lista de tres a ocho
elementos que la plantilla declara. La ficha anterior la documentaba; no
existe en el código y no debe crearse.
orden importa aquí más que en la plantilla: el comprobante que el usuario
consulta meses después debe mostrar la lista en el mismo orden en que la
vio al reservar.
despublicacion¶
Retiro administrativo de una publicación. Relación 1:1.
- Régimen:
append-only - Volumen estimado: unidades por semana
- Origen:
Columnas¶
| Campo | Tipo | Nulo | Descripción |
|---|---|---|---|
publicacion_id |
uuid |
no | Llave foránea RESTRICT, única |
motivo_id |
uuid |
no | Llave foránea RESTRICT a motivo |
nota |
text |
no | '' |
resolucion_reservas |
varchar(12) |
no | mantenercancelar |
reservas_afectadas |
smallint |
no | Contadas por un trigger |
administrador_id |
uuid |
no | Llave foránea RESTRICT a usuario |
notificado_at |
timestamptz |
sí | Aviso al comercio |
Constraints¶
CONSTRAINT chk_despublicacion_resolucion
CHECK (resolucion_reservas IN ('mantener', 'cancelar'))
CONSTRAINT chk_despublicacion_reservas_afectadas CHECK (reservas_afectadas >= 0)
Unicidad¶
| Nombre | Definición | Propósito |
|---|---|---|
unq_despublicacion_publicacion |
(publicacion_id) |
Una publicación no se retira dos veces |
Es la unicidad que cumple el "una publicación no puede despublicarse dos
veces" de RF-A-24, sin ninguna comprobación previa.
Triggers¶
| Nombre | Evento | Momento | Nivel | Regla | Origen |
|---|---|---|---|---|---|
trg_despublicacion_reservas |
INSERT |
BEFORE |
ROW |
Deriva reservas_afectadas contando las reservas vivas de la publicación |
RF-A-24 |
trg_despublicacion_estado |
INSERT |
AFTER |
ROW |
Mueve la publicación al estado despublicada |
RF-A-24 |
trg_despublicacion_reservas es lo que la ficha de columnas ya prometía al
describir reservas_afectadas como "contadas por un trigger". Como toda la
familia de derivación, sobrescribe en vez de comparar:
el número tiene que ser el que la base contó en el instante de la
despublicación, no el que declaró quien la ejecutó, porque es la evidencia de
cuánta gente quedó afectada por una decisión administrativa.
trg_despublicacion_estado cierra el bucle: insertar la fila de
despublicación es despublicar. Dejar el cambio de estado en manos del
llamador permitiría una despublicación registrada que nunca retiró la oferta
del mapa, que es lo contrario de lo que RF-A-24 llama "de forma
inmediata".