Reglas normativas y literales de toda feature de backend Nebula. Cada regla está validada contra el código real de las referencias (
consecutivosydepartamentoennebula-masters,embargosennebula-treasury). No se inventan: se extraen del código; si el código se desvía de la regla, se registra como deuda, no se silencia.
Estado: alineadas al barrido multitenant nativo (jun-2026), incluida la paginación (NativeTenantPaginator) y el aplanado descriptivo i18n (*Nombre). Referencia viva: nebula-masters 1.3.0 y nebula-treasury 1.2.0 (sobre SimappeCommons 4.13.0, nebula-models 0.4.4).
| Capa | Debe extender / implementar |
|---|---|
| Entity de negocio | SequenceBusinessBaseEntity (por secuencia; multitenant + auditoría) |
| Vista de lectura | SequenceBusinessBaseEntity + @Immutable + @Table(name="v_...") |
| DTO | BusinessBaseDto |
| Repository | SimappeRepository<Entity, Long> |
| Component | SimappeService + CrudHtppServletRequestComponent<Dto,Long> (+ PageResponseSupport<Response,Long>) |
| Controller | SimappeController |
| LightLoader | AbstractLoadModels<Long, Dto, Entity> |
SequenceBaseEntity (global) y el viejo BusinessBaseEntity. Los catálogos perpetuos/globales se tratan aparte (no son negocio multitenant).@Filter de Hibernate NO se aplica: el aislamiento se logra SOLO con consultas nativas que filtran:AND client_id = :clientId AND company_id = :companyId
AND NVL(subsidiary_id, -1) = NVL(:subsidiaryId, -1)
SimappeJwt: getUserSession(jwt.extractToken(request.getHeader(AUTHORIZATION))) → getCustomerId() (= client_id), getCompanyId(), getSubsidiaryId().@Query(nativeQuery=true) con el filtro de tenant. Convención de nombre: sufijo ...AndTenant / findAllByTenant....save/saveAll/delete) NO se tocan: el tenant lo asigna @PrePersist desde el TenantContext.findAll, findById, findAllById, existsById) NO se usan en negocio (fugan cross-tenant): se reemplazan por su variante nativa con tenant (findByIdAndTenant, findAllByTenant, findAllByIdInAndTenant...).getPageResponseQuery/getPageDtoQuery se resuelven con NativeTenantPaginator (getPageResponseNative/getPageDtoNative), que inyecta SIEMPRE el filtro de tenant en SQL nativo. El default de PageResponseSupport opera sobre findAll(Specification, Pageable) y NO aísla tenant (la vista extiende SequenceBusinessBaseEntity, que no es TenantAware) → fuga cross-tenant (AP-1.5). Sin ordersBy del cliente se aplica un orden estable por defecto (aplicarOrden…PorDefecto).... FOR UPDATE (findForUpdateByTenant), no @Lock.HttpServletRequest (métodos @Override de firma fija): obtienen el tenant del hilo con RequestContextHolder.currentRequestAttributes() + SimappeJwt.TenantIterationSupport.forEachActiveTenant; el tenant es explícito por iteración (no hay token).Object[] (Oracle COUNT → BigDecimal): castear con ((Number) r[i]).longValue().status (y todo campo de SequenceBusinessBaseEntity) JAMÁS es negocio — es interno. mapDtoToEntity no debe tomar status del DTO.estado_<entidad>, p.ej. estado_embargo, estado_consecutivo) en español y con enumeración dedicada en nebula-models/.../enums/.status heredado en inglés (ACTIVE/INACTIVE/BLOCKED) + estado de negocio propio en español (ACTIVO/LEVANTADO/...).loadForEntity/loadForDto/loadForId/loadForListEntity/loadForListDto), enriquecimiento anti-N+1. Lo usan read/get/create/update (devuelven DTO).@Immutable → consultas de lectura (get-record / page-response). El Mapper implementa NebulaEnrichedMapper<E,R> (toResponse(entity, context)) y aplana Entity(View) → Response; el context transporta enriquecimientos resueltos por el component. Aquí no se usa LightLoader.NebulaEnrichedMapper) sobre Vista.get-record (unitario) y page-response (paginado), implementadas con PageResponseSupport. No se expone el DTO interno en consultas....Nombre, ...Codigo, derivados como saldo/cantidad). El aplanado lo hace el Mapper sobre la Vista (+ contexto), nunca el front.*Nombre (etiqueta legible resuelta por idioma del token vía messages.getOrDefault("<PREFIX>.<VALOR>", default)); se conserva el código crudo. Sin texto hardcodeado; si falta la clave, degrada al código. Ver 04 §3.1.estadoEmbargo, estadoTipoPlan, principal), NUNCA un genérico (estado); la etiqueta es <campoCrudo>Nombre. El front filtra/ordena por el campo crudo (tipo real: enum/Long/Boolean) y pinta el *Nombre (String); jamás filtra por el *Nombre. Un nombre que no coincide con la columna rompe el filtro nativo (ver AP-4.6). Booleanos Sí/No → clave común COMMON.BOOLEAN.TRUE/FALSE.GENERATED ... AS IDENTITY. SET SQLBLANKLINES ON; en scripts sqlplus.db/migration/oracle/V<AAAA_MM_DD>_NNN__<desc>.sql; GRANT ... TO nebula_user_dev_01; vistas CREATE OR REPLACE. Flyway NO corre en dev: se aplican manualmente.simappe_admin).index, read, get, create, update, delete, page, page-response, get-record + operaciones de dominio propias.byte[]/descarga (csv/xlsx/pdf): anotar con @IgnoreResponseBinding (commons) o el advice global los envuelve en SuccessResponse y rompe (cannot be cast to [B).tenant()), queries y campos de negocio; cobertura 100%, que refleje la implementación (no relleno). En repositorios el Javadoc (@param/@return) va sobre el método aunque lleve @Query/@Param.pom.xml del módulo; no se mergea sin bump.simappeconfig): el application.yml solo lleva bootstrap.mockStatic(SimappeJwt) + UserSession con getCustomerId/getCompanyId/getSubsidiaryId para el tenant; LightLoaders en test: RequestContextHolder + MockHttpServletRequest; gate JaCoCo.Antes de declarar una feature alineada, barrido exhaustivo (no solo lo tocado):
client_id (contar nativeQuery = client_id).findAll/findById/findAllById/existsById) en componentes, services y lightloaders.ConsecutivoTipoDto sin BusinessBaseDto, a corregir en nebula-models).