Estado: NORMATIVO
Referencias de código: Consecutivos (nebula-masters), Embargos (nebula-treasury)
Este documento recoge las decisiones transversales que toda feature backend del
ERP Nebula aplica por igual: estado de negocio propio, auditoría, i18n, caché
selectiva, Kafka y Javadoc. Cada regla viene con su matiz y su cita de código
real en repositorio.
status)El campo status heredado de SequenceBusinessBaseEntity es interno
(enum ACTIVE/INACTIVE/BLOCKED, en inglés). Jamás representa estado de
negocio. Cada entidad declara su propio campo estado_x en español, con su
propio universo de valores.
| Feature | Campo de negocio | Valores |
|---|---|---|
| Embargos | estado_embargo |
ACTIVO / INACTIVO / LEVANTADO |
| Consecutivos | estado_consecutivo |
VIGENTE / SUSPENDIDO / EXPIRADO / AGOTADO |
Embargos — EmbargoEntity (nebula-treasury/.../v1/embargo/entity/EmbargoEntity.java):
/** Estado de NEGOCIO: ACTIVO / INACTIVO / LEVANTADO. */
@Column(name = "estado_embargo", nullable = false, length = 12)
private String estadoEmbargo;
Consecutivos — ConsecutivoDefEntity (nebula-masters/.../v1/consecutivo/entity/ConsecutivoDefEntity.java):
/** Estado de negocio de la definición (VIGENTE/SUSPENDIDO/EXPIRADO/AGOTADO); nunca el status interno. */
@Enumerated(EnumType.STRING)
@Column(name = "estado_consecutivo", nullable = false, length = 12)
private EstadoConsecutivo estadoConsecutivo;
Matiz — tipado: el estado de negocio puede ser String (embargos, valores
controlados por validador) o un @Enumerated(EnumType.STRING) (consecutivos,
tipado fuerte). Ambos son válidos; lo que NO se admite es reutilizar status.
Matiz — validación cruzada de "activo/vigente": cuando una feature consume
otra entidad y necesita saber si está "activa", consulta el estado de NEGOCIO
ajeno, nunca su status. En embargos, la vigencia de la cuenta contable se
valida vía validarMovimientoActiva(...) y la del tercero vía
validarActivo(...) (WebClients de nebula-shared); el Javadoc del componente
lo deja explícito: "estado de NEGOCIO, nunca status".
Toda operación de un componente — incluidas las lecturas — emite un mensaje
de auditoría mediante auditLogService + SimappeUtilities.createAuditLogMessage(...).
El patrón canónico encapsula la llamada en un helper privado audit(...).
Embargos — helper real (EmbargoComponent.java):
/**
* Auditoria estandar (mensaje por operacion + EventType + detalle) en cada
* operacion.
*/
private void audit(String op, String msg, EventType tipo, HttpServletRequest request, Object detalle) {
auditLogService.sendMessage(SimappeUtilities.createAuditLogMessage(op, msg, tipo,
this.getClass().getCanonicalName(), request.getHeader(SimappeConstant.AUTHORIZATION), detalle));
}
Se invoca incluso en operaciones de solo lectura, por ejemplo en read:
@Override
@Transactional(readOnly = true)
public List<EmbargoDto> read(HttpServletRequest request) throws SimappeException {
List<EmbargoDto> list = lightloader.loadForListEntity(embargoRepository.findAll());
audit("read", SimappeConstant.SUCCESSFULLY_GENERETED_LIST, EventType.READ, request, null);
return list;
}
En consecutivos el helper se llama auditar(...) (ConsecutivoTipoComponent.java)
y sigue idéntica firma y semántica. El nombre del helper es indiferente; la regla
canónica es una llamada de auditoría por operación, con EventType,
getClass().getCanonicalName() como origen, el token y el detalle.
| Operación | EventType típico |
|---|---|
| listar/leer | READ |
| obtener por id | GET |
| crear | CREATE |
| actualizar / levantar | UPDATE |
| eliminar | DELETE |
| paginar | READ_PAGE |
Todas las claves de mensaje (errores y validaciones) se internacionalizan en
español Y inglés, sin excepción. Viven en Postgres simappe_admin, schema
admin, en dos tablas: translation_keys (la clave) y translations (los
valores por idioma). El language_id es EN = 1, ES = 2. Los scripts son
idempotentes (ON CONFLICT DO NOTHING).
Fragmento real — nebula-treasury/.../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()),
...
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;
Reglas de aplicación:
key_type distingue ERROR y VALIDATION.key_code con namespace por módulo (treasury.embargo.error.*).Detalle de aplicación manual (Flyway no corre en DEV) en
06_DATOS_DDL_I18N.md.
*Nombre)Además de errores y validaciones, el i18n se usa para mostrar etiquetas legibles de
enums y códigos en las respuestas de lectura. La regla es aditiva: el record
conserva el código/enum crudo y agrega un campo *Nombre con su texto resuelto por
el idioma del token. Así el front recibe el valor de máquina (para lógica/filtros) y el
de presentación (para pintar), sin hardcodear textos.
toViewResponse(View) y entoResponse(Entity, context). Nunca el front, nunca el controller.messages.getOrDefault("<PREFIX>.<VALOR>", <default>). Si la clave noname() crudo (degrada, no rompe). Para enums la clave usavalue.name().<MODULO>.<GRUPO>.<VALOR> en mayúsculas.Convención de nombres de campo (OBLIGATORIA). El estándar es estricto para que el
front pueda filtrar/ordenar por el campo crudo y pintar con el *Nombre:
estadoEmbargo (col estado_embargo), estadoConsecutivo, estadoTipoPlanestado_tipo_plan), principal, tipoEmbargo, ordenadoPor. Prohibido unestado) distinto de la columna: el front filtra por ese campo y, si<campoCrudo>Nombre: estadoEmbargoNombre,estadoTipoPlanNombre, principalNombre.Long, Boolean); el *Nombre esString. El front nunca filtra por el *Nombre.| Feature | Campo crudo (= columna, filtrable) | Etiqueta descriptiva | Prefijo de clave |
|---|---|---|---|
| Embargos | tipoEmbargo / ordenadoPor / estadoEmbargo |
…Nombre |
TREASURY.EMBARGO.TIPO. / .ORDENADO_POR. / .ESTADO. |
| Consecutivos (def) | modo / scope / tipoValor / tipoVigencia / estadoConsecutivo |
…Nombre |
CONSECUTIVO.MODO. / .SCOPE. / .TIPO_VALOR. / .TIPO_VIGENCIA. / .ESTADO. |
| Tipos de consecutivo | modulo |
moduloNombre |
MODULES.<codigo>_NAME |
| Tipo plan contable | estadoTipoPlan (enum) / principal,multiplanContable,tieneCuentasActivas (bool) |
estadoTipoPlanNombre / …Nombre |
ACCOUNTING.TIPO_PLAN.ESTADO.* (enum) / COMMON.BOOLEAN.TRUE/FALSE (booleanos) |
Booleanos → clave COMÚN. Las banderas Sí/No usan la clave reutilizable
COMMON.BOOLEAN.TRUE / COMMON.BOOLEAN.FALSE (módulo common), no una por campo ni
por módulo: evita duplicar "Sí/No" en cada feature. El campo crudo es Boolean y el front
filtra por true/false (o 1/0 contra la vista).
// EmbargoMapper — etiqueta i18n del codigo; si no hay clave, devuelve el codigo crudo.
private String nombre(String prefix, String codigo) {
return (codigo == null || codigo.isBlank()) ? null : messages.getOrDefault(prefix + codigo, codigo);
}
Estas claves de etiqueta se cargan en el mismo i18n de Postgres (ES+EN, idempotente);
ver 06 §2.
NebulaEnrichedMapper)El conversor entidad→record implementa NebulaEnrichedMapper<E, R> (de
nebula-models), con el contrato:
R toResponse(E entity, Map<String, Map<Long, Object>> context);
El context transporta enriquecimientos que el component resolvió aparte (no el
mapper): se indexan por una clave lógica (p. ej. "VALOR_ACTUAL") y dentro por id.
Así el mapper queda puro (sin WebClients ni repos) y el enriquecimiento N+1 lo orquesta
el component una sola vez. Ejemplo en consecutivos: context.getOrDefault("VALOR_ACTUAL", Map.of()) aporta el contador vigente de cada definición.
@Cacheable (selectivo)La caché se aplica solo a catálogos/configuración estables (lecturas
frecuentes, escrituras raras). Nunca a datos transaccionales ni a contadores.
consecutivo-tipos yconsecutivo-defs).Código real — ConsecutivoTipoComponent.java:
@Transactional(readOnly = true)
@Cacheable(value = "consecutivo-tipos", key = "'all'")
public List<ConsecutivoTipoDto> read(HttpServletRequest request) throws SimappeException { ... }
@Transactional(readOnly = true)
@Cacheable(value = "consecutivo-tipos", key = "#id")
public ConsecutivoTipoDto get(Long id, HttpServletRequest request) throws SimappeException { ... }
@Transactional
@CacheEvict(value = "consecutivo-tipos", allEntries = true)
public ConsecutivoTipoDto create(ConsecutivoTipoDto dto, HttpServletRequest request) throws SimappeException { ... }
Toda escritura (create/update/delete) lleva
@CacheEvict(value = "consecutivo-tipos", allEntries = true): ante cualquier
cambio se invalida el caché completo (no invalidación selectiva).
Configuración — OptimizedCacheConfiguration.java:
@Configuration
@EnableCaching
public class OptimizedCacheConfiguration {
@Bean
public CacheManager cacheManager() {
ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager(
"entities", "configurations", "tenantData", "queryResults", "systemSettings",
// Componente Consecutivos: catalogos estables (lecturas frecuentes, escrituras raras).
// Se invalidan por completo (allEntries) ante cualquier escritura. NO cachean el
// contador/emision/reserva, que son transaccionales y exigen consistencia fuerte.
"consecutivo-tipos",
"consecutivo-defs"
);
cacheManager.setAllowNullValues(false);
return cacheManager;
}
}
Matices:
consecutivo-tipos): catálogo GLOBAL, no multitenant,'all' o #id).@CacheEvict(allEntries = true); se priorizacacheManager: es transaccional por diseño.Auditoría (transversal, SIEMPRE): todo servicio tiene el canal Kafka de
audit-log. Tanto nebula-masters como nebula-treasury declaran
KafkaAdapterConfig (expone el KafkaTemplate) y nebula-treasury además un
AuditLogListener:
nebula-treasury/.../config/KafkaAdapterConfig.javanebula-treasury/.../v1/kafka/AuditLogListener.java —@KafkaListener(topics = "user-events", ...)nebula-masters/.../config/KafkaAdapterConfig.javaEventos de dominio (selectivo):
| Feature | ¿Evento de dominio Kafka? | Motivo |
|---|---|---|
| Consecutivos | NO | Servicio síncrono request/response (emitir/reservar/confirmar/anular devuelven al instante). No hay consumidor asíncrono. |
| Embargos | Candidato futuro | Eventos EmbargoCreado/EmbargoLevantado cuando exista el módulo de egresos que los consuma. Hoy no se publica (no hay consumidor). |
Regla: no se publica un evento de dominio si no existe un consumidor real.
La existencia del canal de auditoría es independiente de esta decisión.
El Javadoc completo es criterio de aceptación, no opcional. Se documenta:
@param, @return, @throws (con elEl Javadoc debe reflejar la implementación real, no ser relleno. Ejemplos
canónicos:
EmbargoComponent describe el contrato, las validacionescreate(...) enumera literalmente las reglas que validaRN-ALC2-02/03/04, RN-ALC2-05, RN-ALC2-06, RN-ALC2-01).@Column de las entidades lleva un comentario que ata el campo a su RN04_PATRONES_TRANSVERSALES.md — Patrón canónico de feature backend — Nebula ERP