Estado: NORMATIVO
Referencias de código: Consecutivos (nebula-masters), Embargos (nebula-treasury)
Este documento define cómo se modelan y aplican los datos de una feature backend
del ERP Nebula: el DDL Oracle (tabla, secuencia, vista plana, GRANTs) y la
internacionalización (i18n) en el simappe_admin de Postgres. Todo se cita de
scripts reales en repositorio.
Punto crítico: Flyway NO corre en el arranque de los servicios en DEV (no
existeflyway_schema_history). Por tanto, las migraciones DDL e i18n se
aplican manualmente sobre la infraestructura del nodo-01. Ver
§4 Aplicación manual.
| Regla | Descripción |
|---|---|
| Secuencia, no IDENTITY | El PK se alimenta con una CREATE SEQUENCE, NUNCA GENERATED ALWAYS AS IDENTITY (causó problemas previos). |
Vista plana @Immutable |
Cada feature publica una vista v_<entidad> solo-lectura que aplana la entidad (+ columnas derivadas). La consume una entidad-vista @Immutable para get-record / page-response. |
| GRANTs al usuario de aplicación | Tanto la tabla como la vista reciben GRANT al usuario transversal de aplicación nebula_user_dev_01 (el dueño del esquema es otro: MASTERS / TREASURY). |
SET SQLBLANKLINES ON |
Primera línea del script cuando se aplica por sqlplus; sin ella, sqlplus rompe los CREATE TABLE al encontrar líneas en blanco. |
| Estado a dos niveles | status interno (ACTIVE/INACTIVE/BLOCKED, inglés) + estado de negocio propio del dominio en español (estado_embargo, estado_consecutivo). status JAMÁS es negocio. |
| Multitenant | Columnas client_id, company_id, subsidiary_id; UNIQUE keys e índices con prefijo client_id, company_id. |
Fragmento real de
backend/nebula-treasury/src/main/resources/db/migration/oracle/V2026_06_010__embargos.sql:
SET SQLBLANKLINES ON;
CREATE TABLE treasury.embargos (
id NUMBER(19) NOT NULL,
consecutivo NUMBER(9) NOT NULL,
tipo_embargo VARCHAR2(30) NOT NULL,
...
estado_embargo VARCHAR2(12) DEFAULT 'ACTIVO' NOT NULL,
status VARCHAR2(20) DEFAULT 'ACTIVE' NOT NULL,
created_by VARCHAR2(255),
updated_by VARCHAR2(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP,
version NUMBER(19) DEFAULT 0 NOT NULL,
client_id NUMBER(19) NOT NULL,
company_id NUMBER(19) NOT NULL,
subsidiary_id NUMBER(19),
CONSTRAINT pk_embargos PRIMARY KEY (id) USING INDEX TABLESPACE treasury_index,
CONSTRAINT uk_embargos_consecutivo UNIQUE (client_id, company_id, consecutivo) USING INDEX TABLESPACE treasury_index,
CONSTRAINT ck_embargos_status CHECK (status IN ('ACTIVE','INACTIVE','BLOCKED')),
CONSTRAINT ck_embargos_estado CHECK (estado_embargo IN ('ACTIVO','INACTIVO','LEVANTADO')),
...
) TABLESPACE treasury_data;
CREATE SEQUENCE treasury.embargos_seq START WITH 1000 INCREMENT BY 25 NOCYCLE;
CREATE INDEX idx_embargos_tenant ON treasury.embargos(client_id, company_id, subsidiary_id) TABLESPACE treasury_index;
CREATE INDEX idx_embargos_estado ON treasury.embargos(client_id, company_id, estado_embargo) TABLESPACE treasury_index;
COMMENT ON TABLE treasury.embargos IS 'Maestro de Embargos (HT_TES_002). estado_embargo = estado de negocio; status interno. cuenta_contable_id es referencia para Egresos (no genera asiento, INV-06).';
Observa: secuencia (embargos_seq START WITH 1000 INCREMENT BY 25), status y
estado_embargo separados con sus CHECK, y COMMENT ON TABLE documentando
convenciones implícitas (patrón obligatorio para que features que consuman la
tabla detecten las reglas heredadas).
get-record / page-responseFragmento real del mismo V2026_06_010__embargos.sql (Embargos):
CREATE OR REPLACE VIEW treasury.v_embargos AS
SELECT
e.id, e.consecutivo, e.tipo_embargo, e.orden_judicial,
e.deudor_tercero_id, e.acreedor_tercero_id, e.mayoria_edad, e.fecha_inicio,
e.valor_embargo, e.ordenado_por, e.por_porcentaje, e.por_valor_fijo,
e.cuenta_contable_id, e.estado_embargo, e.orden_judicial_levantamiento, e.fecha_levantamiento,
CAST(0 AS NUMBER(20,2)) AS valor_embargado,
CASE WHEN e.tipo_embargo = 'PENSION_ALIMENTICIA' THEN 0 ELSE e.valor_embargo END AS saldo_embargo,
e.status, e.created_by, e.updated_by, e.created_at, e.updated_at, e.version,
e.client_id, e.company_id, e.subsidiary_id
FROM treasury.embargos e;
Misma técnica en Consecutivos
(backend/nebula-masters/src/main/resources/db/migration/oracle/V2026_06_008__consecutivo_vistas.sql),
con dos vistas (una multitenant que hereda SequenceBusinessBaseEntity, otra
catálogo global que hereda SequenceBaseEntity):
CREATE OR REPLACE VIEW masters.v_consecutivo_def AS
SELECT
d.id, d.codigo, d.nombre, d.descripcion,
d.modo, d.alcance, d.tipo_valor,
...
d.tipo_vigencia, d.anio, d.fecha_desde, d.fecha_hasta, d.estado_consecutivo,
d.status, d.created_by, d.updated_by, d.created_at, d.updated_at, d.version,
d.client_id, d.company_id, d.subsidiary_id
FROM masters.consecutivo_def d;
CREATE OR REPLACE VIEW masters.v_consecutivo_tipo AS
SELECT
t.id, t.codigo, t.nombre, t.descripcion, t.modulo,
t.status, t.created_by, t.updated_by, t.created_at, t.updated_at, t.version
FROM masters.consecutivo_tipo t;
La vista es solo-lectura: la entidad-vista
@Immutablehereda de su base las
columnas de tenant (client_id/company_id/subsidiary_id), pero el aislamiento
en la lectura es explícito —findByIdAndTenantnativo paraget-record; el
page-responsese apoya en la conexión por tenant (ver 09 §2). Las columnas derivadas (saldo_embargo,
valor_embargado) se calculan en la vista, no en el servicio.
El dueño del esquema (TREASURY / MASTERS) crea los objetos; el servicio se
conecta como nebula_user_dev_01, que necesita GRANTs explícitos.
Embargos (V2026_06_010__embargos.sql):
GRANT SELECT, INSERT, UPDATE, DELETE ON treasury.embargos TO nebula_user_dev_01;
GRANT SELECT ON treasury.v_embargos TO nebula_user_dev_01;
Consecutivos (V2026_06_008__consecutivo_vistas.sql) — solo SELECT sobre vistas:
GRANT SELECT ON masters.v_consecutivo_def TO nebula_user_dev_01;
GRANT SELECT ON masters.v_consecutivo_tipo TO nebula_user_dev_01;
Regla: tabla → SELECT/INSERT/UPDATE/DELETE; vista → solo SELECT.
| Regla | Descripción |
|---|---|
| Destino | Postgres simappe_admin, schema admin, tablas translation_keys + translations. |
| Idiomas | SIEMPRE ES y EN para cada clave, sin excepción. language_id: EN=1, ES=2. |
| Idempotente | ON CONFLICT DO NOTHING en claves y traducciones. |
| Estructura | (1) insertar claves en translation_keys; (2) traducciones ES (language_id=2); (3) traducciones EN (language_id=1), uniendo por key_code. |
Fragmento real de
backend/nebula-treasury/src/main/resources/db/i18n/V001__treasury_i18n_embargos.sql:
SET search_path TO admin;
BEGIN;
-- 1. CLAVES
INSERT INTO admin.translation_keys (key_code, module, key_type, description, default_value, created_date, updated_date) VALUES
('treasury.embargo.error.tercero', 'treasury', 'ERROR', 'Tercero invalido', 'El tercero ingresado no existe en el Maestro de Terceros o no se encuentra Activo', NOW(), NOW()),
...
('treasury.embargo.estado.format', 'treasury', 'VALIDATION', 'Estado invalido', 'Estado invalido (ACTIVO, INACTIVO)', NOW(), NOW())
ON CONFLICT (key_code) DO NOTHING;
-- 2. TRADUCCIONES ES (language_id = 2)
INSERT INTO admin.translations (language_id, translation_key_id, value, is_verified, created_date, updated_date)
SELECT 2, tk.id, t.value, TRUE, NOW(), NOW()
FROM (VALUES
('treasury.embargo.error.tercero', 'El tercero ingresado no existe en el Maestro de Terceros o no se encuentra Activo'),
...
) AS t(key_code, value)
JOIN admin.translation_keys tk ON tk.key_code = t.key_code
ON CONFLICT (translation_key_id, language_id) DO NOTHING;
-- 3. TRADUCCIONES EN (language_id = 1)
INSERT INTO admin.translations (language_id, translation_key_id, value, is_verified, created_date, updated_date)
SELECT 1, tk.id, t.value, TRUE, NOW(), NOW()
FROM (VALUES
('treasury.embargo.error.tercero', 'The party does not exist in the Parties master or is not Active'),
...
) AS t(key_code, value)
JOIN admin.translation_keys tk ON tk.key_code = t.key_code
ON CONFLICT (translation_key_id, language_id) DO NOTHING;
COMMIT;
Cada clave aparece tres veces: una vez en
translation_keys, una en ES y una
en EN. No se permite una clave con menos de 2 traducciones. La verificación
de control está en §4.3.
Junto a ERROR y VALIDATION, el i18n carga también las etiquetas legibles de
enums y códigos que las respuestas exponen como campos *Nombre (aplanado descriptivo
aditivo, ver 04 §3.1). Misma estructura de 3 bloques
(claves + ES + EN), misma idempotencia; cambia la convención de key_code:
<MODULO>.<GRUPO>.<VALOR> en mayúsculas (no se rompe nunca: si falta la clave, el
mapper cae al código crudo).
-- treasury: V002__treasury_i18n_embargo_enums.sql (extracto de claves)
INSERT INTO admin.translation_keys (key_code, module, key_type, description, default_value, created_date, updated_date) VALUES
('TREASURY.EMBARGO.ESTADO.ACTIVO', 'treasury', 'LABEL', 'Estado embargo activo', 'Activo', NOW(), NOW()),
('TREASURY.EMBARGO.TIPO.CIVILES_BANCARIOS', 'treasury', 'LABEL', 'Tipo civiles/bancarios', 'Civiles y bancarios', NOW(), NOW()),
('TREASURY.EMBARGO.ORDENADO_POR.PORCENTAJE','treasury', 'LABEL', 'Ordenado por porcentaje', 'Porcentaje', NOW(), NOW())
ON CONFLICT (key_code) DO NOTHING;
-- masters: V005__masters_i18n_consecutivo_enums.sql usa el prefijo CONSECUTIVO.<ENUM>.<VALOR>
-- (MODO / SCOPE / TIPO_VALOR / TIPO_VIGENCIA / ESTADO), igual en ES (2) y EN (1).
Booleanos → clave COMÚN reutilizable. Las banderas Sí/No NO se traducen con una clave
por campo ni por módulo: usan la clave transversal COMMON.BOOLEAN.TRUE /
COMMON.BOOLEAN.FALSE (módulo common), cargada una sola vez para todo el ERP. Así un
nuevo feature con banderas booleanas no duplica claves.
-- accounting: V019__common_i18n_boolean_labels.sql (clave comun, modulo 'common')
('COMMON.BOOLEAN.TRUE', 'LABEL', 'Sí', 'Yes'),
('COMMON.BOOLEAN.FALSE', 'LABEL', 'No', 'No')
En DEV no hay auto-migración: no existe flyway_schema_history y el servicio no
ejecuta migraciones al arrancar. Los scripts de
src/main/resources/db/migration/oracle/ (Oracle) y src/main/resources/db/i18n/
(Postgres) se aplican a mano sobre la infraestructura del nodo-01 ANTES del
deploy del servicio.
oracle-xe)El owner del esquema crea los objetos (MASTERS / TREASURY); el app user
nebula_user_dev_01 solo recibe los GRANTs.
# Aplicar el DDL como owner del esquema (ej. TREASURY)
ssh nodo-01 "docker exec -i oracle-xe sqlplus -S /nolog" <<'EOF'
CONNECT TREASURY/<pass>@//localhost:1521/XEPDB1
SET SQLBLANKLINES ON;
@/ruta/V2026_06_010__embargos.sql
EOF
En la práctica el .sql ya trae SET SQLBLANKLINES ON; en la primera línea; al
pasarlo por docker exec -i ... < archivo.sql se respeta. Para el módulo Masters
el owner es MASTERS:
ssh nodo-01 "docker exec -i oracle-xe sqlplus -S /nolog" \
< backend/nebula-masters/src/main/resources/db/migration/oracle/V2026_06_008__consecutivo_vistas.sql
# (con CONNECT MASTERS/<pass>@//localhost:1521/XEPDB1 al inicio del script o en heredoc)
ssh nodo-01 "docker exec -i oracle-xe sqlplus -S /nolog" <<'EOF'
CONNECT TREASURY/<pass>@//localhost:1521/XEPDB1
SET PAGESIZE 100 LINESIZE 200;
-- Tabla creada
SELECT table_name FROM user_tables WHERE table_name = 'EMBARGOS';
-- Vista creada
SELECT view_name FROM all_views WHERE owner = 'TREASURY' AND view_name = 'V_EMBARGOS';
-- Indices
SELECT index_name FROM user_indexes WHERE table_name = 'EMBARGOS';
EOF
Verificación de los GRANTs efectivos (que nebula_user_dev_01 ve los objetos):
ssh nodo-01 "docker exec -i oracle-xe sqlplus -S /nolog" <<'EOF'
CONNECT nebula_user_dev_01/<pass>@//localhost:1521/XEPDB1
SELECT * FROM treasury.v_embargos WHERE ROWNUM <= 1;
EOF
postgres-dev)ssh nodo-01 "docker exec -i -e PGPASSWORD=Simappe postgres-dev psql -U simappe -d simappe_admin -tA" \
< backend/nebula-treasury/src/main/resources/db/i18n/V001__treasury_i18n_embargos.sql
La invariante es: toda clave del módulo debe tener al menos 2 traducciones
(ES + EN). La query siguiente debe devolver 0 filas:
ssh nodo-01 "docker exec -i -e PGPASSWORD=Simappe postgres-dev psql -U simappe -d simappe_admin -tA" <<'EOF'
SET search_path TO admin;
SELECT tk.key_code, COUNT(t.language_id) AS n_trad
FROM admin.translation_keys tk
LEFT JOIN admin.translations t ON t.translation_key_id = tk.id
WHERE tk.module = 'treasury'
GROUP BY tk.key_code
HAVING COUNT(t.language_id) < 2;
EOF
Si devuelve filas → falta ES o EN para esas claves; corregir antes del deploy.
MEMORY → reference_oracle_nodo01_topologia.md.MEMORY → feedback_i18n_es_en_siempre.md.