Cómo un módulo de Nebula ofrece una importación masiva de datos (TEXT/CSV/XLSX/XML/JSON) sobre la capa genérica de
SimappeCommons, con un solo contrato de endpoint, una sola estructura de respuesta y un flujo atómico que el desarrollador no puede divergir. Referencia de implementación: Dependencias y Depreciación (nebula-accounting-core), validadas en dev.
Versión: 2.0 · Fecha: 30 de Julio, 2026 · Arquitecto: Carlos Alberto Torres Camargo
Librerías: SimappeCommons ≥ 4.28.1 · SimappeModel ≥ 3.11.0 · nebula-commons ≥ 0.5.0 · ADR de arquitectura: SimappeCommons/docs/ADR-0001
Cambio de la v2.0 — el endpoint es agnóstico. La v1.0 mostraba el
ImportControlfijado en el controlador del módulo, y eso amarraba cada endpoint a un escenario. Queda derogado: los nueve ejes del control son parámetros de la petición, y el caso de uso que motivó el endpoint (la HU) no restringe su firma. Quien consume elige el escenario —validar sin escribir, cortar al primer error, abortar, todo-o-nada, bloques, codificación, modo—; el módulo solo aporta su proceso de dominio. Ver §4.1, §6 y §12.
Todo maestro o entidad que deba cargarse en lote desde un archivo usa esta capa. Sustituye las importaciones ad-hoc de los primeros sprints (cada módulo reimplementaba parseo, validación y persistencia con contratos incompatibles). El frontend deja de tratar cada carga como un caso distinto: un solo endpoint, una sola respuesta.
SimappeCommons no es una librería utilitaria: es una librería de arquitectura. Define reglas, interfaces y procesos atómicos — procesos que se ejecutan siempre de una sola forma, sin divergencias. El modelo canónico es la paginación nativa: todo lo variable se resuelve arriba y el componente de commons ejecuta el paso final determinista. Esta capa se diseña bajo esa misma ley.
ImportControl son parámetros de la petición. El objetivo del endpoint lo fija este patrón —si dice importar, importa— y nada más: la HU, la pantalla o el caso de uso que lo motivó no recortan la firma ni fijan políticas.ImportControl) y lo ejecuta el framework. El proceso la honra.400: nunca se degrada en silencio al canónico, porque correría un comportamiento distinto del que pidió el consumidor.Eje 1 — QUÉ (dominio): AbstractImportProcess<T> (clase abstracta única)
el módulo la extiende 1 vez por caso de uso
aporta SOLO: validate(registro) + persist(lote)
Eje 2 — CÓMO (modo): Executors: síncrono / asíncrono / streaming
resuelven origen de ingesta + publican el resultado
el proceso del Eje 1 queda ciego al modo y al formato
Con N casos de uso y 3 modos el costo es N + 3, no 3N. Una importación se escribe una sola vez y corre en síncrono, asíncrono o streaming sin tocarla.
ImportControlLa carga declara el comportamiento completo. El proceso lo honra, no lo decide. Es un record de SimappeModel con nueve ejes:
| Campo | Valores | Efecto |
|---|---|---|
format |
TEXT / CSV / XLSX / XML / JSON |
Lector que normaliza el recurso |
mode |
SYNC / ASYNC / STREAMING |
Executor: de dónde entra la ingesta y dónde se publica |
validation |
COMPLETA / AL_PRIMER_ERROR / FLUIDA |
Estrategia de validación de entrada |
failurePolicy |
ABORTAR / PARCIAL |
Ante errores: no persistir nada, o persistir lo válido |
tx |
ALL_OR_NOTHING / PER_BLOCK |
Frontera transaccional de la persistencia |
readBlockSize |
int |
Tamaño del bloque de ingestión (streaming) |
persistBlockSize |
int |
Persiste cada N registros; un bloque por transacción según tx |
charset |
Charset |
Codificación exigida al recurso de texto: los lectores la verifican de forma estricta y rechazan el archivo que no la cumpla, en vez de sustituir bytes inválidos y admitir datos corruptos. Los formatos binarios la ignoran |
persistencia |
PERSIST / VALIDATE |
PERSIST escribe; VALIDATE corre la validación completa y no escribe nada. ImportRunner corta antes de persistir en un solo punto, cubriendo los tres executors |
Definición (SimappeModel · com.catcsoft.simappe.model.core.imports):
public record ImportControl(
ImportFormat format,
ImportMode mode,
ValidationStrategy validation,
FailurePolicy failurePolicy,
Transactionality tx,
int readBlockSize,
int persistBlockSize,
Charset charset, // desde 3.11.0
PersistencePolicy persistencia) { // desde 3.11.0
// El constructor de 7 argumentos asume UTF-8 + PERSIST (comportamiento histórico);
// el canónico normaliza los dos opcionales nulos a esos mismos valores.
}
public enum ImportFormat { TEXT, CSV, XLSX, XML, JSON }
public enum ImportMode { SYNC, ASYNC, STREAMING }
public enum ValidationStrategy { COMPLETA, AL_PRIMER_ERROR, FLUIDA }
public enum FailurePolicy { ABORTAR, PARCIAL }
public enum Transactionality { ALL_OR_NOTHING, PER_BLOCK }
public enum PersistencePolicy { PERSIST, VALIDATE }
La carga se arma con lo que trae la petición. El módulo no fija políticas; ni siquiera las suyas. El armado es transversal y vive en nebula-commons:
// com.centrica.nebula.commons.imports.ImportControlFactory
ImportControl control = ImportControlFactory.desdeParametros(
parametros, // los @RequestParam de la peticion
archivo.getOriginalFilename(), // solo para deducir el formato si no se declara
mensajesDeControl()); // titulos i18n resueltos por el modulo
| Eje | Parámetro | Valor canónico si no se declara |
|---|---|---|
| Formato | formato |
se deduce de la extensión (csv, xlsx/xls, txt, xml, json) |
| Modo | modo |
SYNC |
| Validación | validacion |
COMPLETA |
| Falla | falla |
PARCIAL |
| Transaccionalidad | transaccionalidad |
PER_BLOCK |
| Bloque de lectura | bloqueLectura |
0 |
| Bloque de persistencia | bloquePersistencia |
0 |
| Codificación | codificacion |
UTF-8 |
| Persistencia | persistencia |
PERSIST |
Reglas del armado, todas verificadas por prueba:
validate es VALIDATE): el consumidor no tiene que conocer la grafía interna.400.formato declarado tiene precedencia sobre la extensión del archivo.periodo, observacion, lo que el dominio necesite) se ignoran en el armado y siguen disponibles para el proceso.ImportControlMessages), y como algunas claves interpolan el valor rechazado y otras no, cada título es una función que recibe ese valor.ValidationStrategy
COMPLETA — valida TODOS los registros y recolecta TODOS los errores antes de decidir persistencia. Default sano.AL_PRIMER_ERROR — único modo que corta temprano. Elección explícita del dev, no un accidente.FLUIDA — valida y persiste bloque a bloque (streaming).FailurePolicy (ortogonal a la validación)
ABORTAR — si hubo cualquier error, no persiste nada y devuelve todos los errores.PARCIAL — persiste los válidos y reporta los inválidos (best-effort).El caso de uso extiende AbstractImportProcess<T> y aporta dos piezas de dominio. El framework corre el resto.
public abstract class AbstractImportProcess<T> {
public String importKey(); // clave del proceso (async)
protected abstract List<ImportError> validate(T record, int line); // 1 registro
protected abstract PersistOutcome persist(List<T> validBatch); // 1 lote válido
}
Lo que devuelve persist — PersistOutcome (SimappeCommons · com.catcsoft.simappe.commons.api.v1.core.imports):
public record PersistOutcome(
long created, // creados en el lote
long updated, // actualizados en el lote
long skipped, // válidos no aplicados (p. ej. duplicado)
List<ImportError> errors) // errores de persistencia por fila; vacío si ninguno
{
// fábrica de conveniencia sin errores de persistencia
public static PersistOutcome of(long created, long updated, long skipped);
}
Implementación de referencia — Dependencias (nebula-accounting-core):
@Component
@RequiredArgsConstructor
public class DependenciaImportProcess extends AbstractImportProcess<DependenciaImportRow> {
private final DependenciasService dependenciasService;
private final DependenciasRepository dependenciasRepository; // consulta nativa/tenant
private final Validator validator; // Bean Validation
@Override public String importKey() { return "DEPENDENCIAS"; }
/** Valida UNA fila (negocio); el framework la corre sobre todas, honrando la estrategia. */
@Override
protected List<ImportError> validate(DependenciaImportRow record, int line) {
Set<ConstraintViolation<DependenciaImportRow>> v = validator.validate(record);
if (v.isEmpty()) return List.of();
List<ImportError> errors = new ArrayList<>(v.size());
for (ConstraintViolation<DependenciaImportRow> c : v) {
errors.add(new ImportError(line, c.getPropertyPath().toString(),
"IMPORT_VALIDATION", c.getMessage(), String.valueOf(c.getInvalidValue())));
}
return errors;
}
/** Persiste un lote ya válido: omite duplicados por código (tenant) y crea el resto. */
@Override
protected PersistOutcome persist(List<DependenciaImportRow> validBatch) throws SimappeException {
UserSession session = session(currentRequest());
long created = 0, skipped = 0;
for (DependenciaImportRow row : validBatch) {
boolean existe = dependenciasRepository.findByCodigoAndTenant(row.codigo(),
session.getCustomerId(), session.getCompanyId(), session.getSubsidiaryId()).isPresent();
if (existe) { skipped++; continue; }
dependenciasService.create(DependenciaDto.builder()
.codigoDependencia(row.codigo()).nombreDependencia(row.nombre())
.estadoDependencia(row.estado()).build(), currentRequest());
created++;
}
return PersistOutcome.of(created, 0, skipped);
}
}
El dev no abre transacciones, no trocea lotes, no decide abortar: eso lo gobierna
ImportControly lo ejecuta elImportRunner. Elpersistescribe con repositorios nativos/tenant y reporta cuántos creó/actualizó/omitió.
@ImportColumn en nebula-modelsTodo modelo va en nebula-models. El registro enlaza cada columna por su encabezado (insensible a mayúsculas, con trim); el lector convierte tipos y reporta errores estructurales. Las reglas de negocio se declaran con Bean Validation:
public record DependenciaImportRow(
@ImportColumn(header = "codigo")
@NotBlank(message = "{masters.validation.dependencia.codigo.required}")
@Size(max = 5, message = "{masters.validation.dependencia.codigo.max}")
String codigo,
@ImportColumn(header = "nombre")
@NotBlank(message = "{masters.validation.dependencia.nombre.required}")
@Size(max = 50, message = "{masters.validation.dependencia.nombre.max}")
String nombre,
@ImportColumn(header = "estado")
@NotNull(message = "{masters.validation.dependencia.estado.required}")
EstadoDependenciaEnum estado) {}
/import (contrato del controlador)El estándar es POST /api/v1/<recurso>/import (multipart, parámetro archivo). El controlador implementa ImportEndpoint<T> —la interface de SimappeCommons que impone el I/O para que ningún módulo diverja la firma— y aporta tres cosas: su proceso, el tipo de fila y el orquestador. La ejecución la hace handleImport(source, control), que el módulo no altera. Devuelve siempre ImportResponse.
public class DependenciasController extends SimappeController
implements ImportEndpoint<DependenciaImportRow> {
@Override public AbstractImportProcess<DependenciaImportRow> importProcess() { return dependenciaImportProcess; }
@Override public Class<DependenciaImportRow> rowType() { return DependenciaImportRow.class; }
@Override public ImportOrchestrator importOrchestrator() { return importOrchestrator; }
@PostMapping(value = "/import", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ImportResponse> importDependencias(
@RequestParam MultipartFile archivo,
@RequestParam Map<String, String> parametros, // los nueve ejes, todos opcionales
HttpServletRequest request) throws SimappeException {
// El controlador NO decide el comportamiento: lo arma con lo que declaro quien llama.
ImportControl control = ImportControlFactory.desdeParametros(
parametros, archivo.getOriginalFilename(), mensajesDeControl());
try (InputStream contenido = archivo.getInputStream()) {
return ResponseEntity.ok(handleImport(contenido, control));
} catch (IOException e) {
throw new SimappeException(accountingMessages.get(MSG_ERROR_IMPORT_LECTURA), ...);
}
}
}
El backend recibe el archivo (como
InputStream, nobyte[]niList) y corre el proceso de validación de entrada. El frontend no valida el contenido del archivo.
Lo que el controlador tiene prohibido: construir el ImportControl con constantes, recortar los formatos que admite, imponer VALIDATE o PERSIST porque su pantalla trabaje de una forma, o vetar un valor del enum por criterio de un cliente. Nada de eso es del endpoint; es del consumidor.
El binding por Map no publica los parámetros por sí solo: los nueve van declarados en @Operation(parameters = ...) con su dominio, su valor canónico y un @ExampleObject. Lo que no está en el contrato publicado, para el consumidor no existe.
Volver agnóstico un endpoint ya consumido es un cambio aditivo: los parámetros que ya existían conservan nombre, tipo y significado, y una llamada que solo manda el archivo debe seguir produciendo exactamente la misma carga que antes. Se prueba con una aserción por eje sobre el ImportControl capturado, no por inspección.
| Modo | Origen del recurso | Publicación del resultado |
|---|---|---|
SYNC |
Viaja en el payload multipart | En la respuesta HTTP (misma llamada). Tope de registros simappe.import.sync.max-records (config-server) |
ASYNC |
Se coloca en un almacén; se responde de inmediato con jobId |
Se consulta después por jobId; notifica al terminar (AWS SES) |
STREAMING |
Se lee por bloques (readBlockSize) |
Por bloque, agregando un resumen final |
Un solo contrato para todas las importaciones. Definición (SimappeModel · com.catcsoft.simappe.model.core.imports):
public record ImportResponse(
ImportSummary summary, // resumen agregado
List<ImportError> errors, // vacío en éxito
String jobId) {} // null en SYNC; presente en ASYNC
public record ImportSummary(
long total, // total de filas leídas
long created, // filas creadas
long updated, // filas actualizadas
long skipped, // filas válidas no aplicadas (p. ej. duplicado)
long invalid) {} // filas descartadas por validación
public record ImportError(
int line, // línea de origen en el archivo (base 1)
String field, // campo/columna con el problema
String rule, // regla violada (IMPORT_REQUIRED_FIELD, IMPORT_TYPE_MISMATCH, IMPORT_VALIDATION, ...)
String message, // mensaje legible
String receivedValue) {} // valor recibido que falló
Campos de ImportError (lo que consume el frontend por fila):
| Campo | Tipo | Significado |
|---|---|---|
line |
int |
Línea exacta del archivo (base 1; el encabezado es la 1) |
field |
String |
Columna/campo con el problema |
rule |
String |
Regla violada — estructural (IMPORT_REQUIRED_FIELD, IMPORT_TYPE_MISMATCH) o de negocio (IMPORT_VALIDATION) |
message |
String |
Mensaje legible |
receivedValue |
String |
Valor que llegó y falló (null si venía vacío) |
Viaja envuelta en el envelope estándar del stack: el contenido útil está en response.content.
La capa distingue tajantemente dos clases de problema, y ese es el corazón de "la validación siempre es completa":
| Clase | Ejemplos | Cómo se maneja |
|---|---|---|
| Por fila / recuperable | campo requerido, tipo inválido, @Size, duplicado por código |
NO lanza excepción: es un dato en la respuesta (ImportError en ImportResponse.errors). La corrida continúa y los recolecta TODOS |
| De lanzamiento / irrecuperable | formato no soportado, archivo ilegible, no hay executor/lector/proceso, fallo de persistencia del lote | Lanza SimappeException (la excepción canónica del stack) y aborta la corrida |
Reglas del modelo de excepciones (inviolables):
SimappeException. No hay excepciones de negocio propias en la capa. El controlador (/import, formatoDeExtension, lectura del archivo), el ImportRunner, los lectores y los executors: todos throws SimappeException.validate(...) devuelve List<ImportError> (nunca lanza por una fila mala) y persist(...) declara throws SimappeException (solo para un fallo real de escritura del lote).ImportError y la corrida sigue.AtomicPersistException es un envoltorio interno (no público) que usa el ImportRunner para forzar el rollback dentro de TransactionTemplate; se desenvuelve a la SimappeException original antes de salir. Nunca escapa: el contrato externo siempre es SimappeException.HttpStatus viaja en la propia SimappeException (constructor SimappeException(mensaje, errores, soluciones, HttpStatus)). Formato no soportado → HttpStatus.BAD_REQUEST (error del cliente); si no se indica estatus, el manejador global responde 500.Base: https://api-dev.centricasoluciones.com · header obligatorio x-simappe-environment: dev.
Paso 1 — login:
curl -s -X POST "$BASE/simappe-oauth2-server/api/v1/login" \
-H "content-type: application/json" -H "x-simappe-environment: dev" \
-d '{"username":"<usuario>","password":"<clave>"}'
# → response.content.accessToken (aún sin compañía)
Paso 2 — selección de compañía (header accesstoken, no Authorization):
curl -s -X POST "$BASE/simappe-oauth2-server/api/v1/jwt-select-company?companyId=<id>" \
-H "accesstoken: <accessToken del paso 1>" -H "x-simappe-environment: dev"
# → nuevo response.content.accessToken (token de negocio)
Importar (multipart; el parámetro es archivo; token en Authorization: Bearer):
curl -s -X POST "$BASE/nebula-accounting-core/api/v1/dependencia/import" \
-H "Authorization: Bearer <token de negocio>" -H "x-simappe-environment: dev" \
-F "archivo=@dependencias.csv"
Formato del archivo (encabezado + filas):
codigo,nombre,estado
DEP01,Contabilidad,ACTIVO
DEP02,Recursos Humanos,ACTIVO
Batería ejecutada 1:1 contra el endpoint en dev. Los cuerpos son reales.
Archivo imp_happy.csv (encabezado + 3 filas nuevas):
codigo,nombre,estado
TQ01,Dep Test QA 01,ACTIVO
TQ02,Dep Test QA 02,ACTIVO
TQ03,Dep Test QA 03,INACTIVO
Respuesta:
"content": {
"summary": { "total": 3, "created": 3, "updated": 0, "skipped": 0, "invalid": 0 },
"errors": [],
"jobId": null
}
HTTP 200. Reimportar el mismo archivo → created:0, skipped:3 (omite duplicados por código/tenant; idempotente).
Archivo imp_mixto.csv = 1 nueva válida + 1 duplicada + 3 malas. La columna de la izquierda es el número de línea del archivo (el encabezado es la línea 1):
# línea
codigo,nombre,estado # 1 (encabezado)
TQ04,Dep Test QA 04,ACTIVO # 2 válida (nueva) → created
TQ01,Dep Dup,ACTIVO # 3 duplicada (TQ01) → skipped
TQLARGO,Codigo Largo,ACTIVO # 4 codigo > 5 (@Size) → invalid (negocio)
,Sin Codigo,ACTIVO # 5 codigo vacío → invalid (estructural)
TQ05,Enum Malo,FOOBAR # 6 estado inexistente → invalid (estructural)
El archivo real NO lleva la columna
# líneani los comentarios; se muestran solo para mapear cada fila a su resultado.
Respuesta — valida TODAS, reporta TODOS los errores, persiste lo bueno:
"content": {
"summary": { "total": 5, "created": 1, "updated": 0, "skipped": 1, "invalid": 3 },
"errors": [
{ "line": 5, "field": "codigo", "rule": "IMPORT_REQUIRED_FIELD", "message": "El codigo es obligatorio", "receivedValue": null },
{ "line": 6, "field": "estado", "rule": "IMPORT_TYPE_MISMATCH", "message": "El valor no es un estado valido", "receivedValue": "FOOBAR" },
{ "line": 4, "field": "codigo", "rule": "IMPORT_VALIDATION", "message": "El codigo no puede exceder 5 caracteres", "receivedValue": "TQLARGO" }
],
"jobId": null
}
HTTP 200 (proceso completado con errores por fila). Cada error trae línea exacta, campo, regla y valor recibido. Persistencia verificada por GET /read: quedan las 4 filas válidas; la fila con estado inválido no se creó.
Reglas visibles:
IMPORT_REQUIRED_FIELD/IMPORT_TYPE_MISMATCHson estructurales (las detecta el lector);IMPORT_VALIDATIONes de negocio (Bean Validation del proceso). La validación no cortó al primer error: reportó los tres.
El mismo AbstractImportProcess corre sin cambios; solo cambia la extensión (el executor resuelve el lector). Archivo imp_text.txt (separado por tabulador, \t):
codigo⇥nombre⇥estado
TQ06⇥Dep Test QA 06⇥ACTIVO
⇥representa un tabulador real. El encabezado enlaza por@ImportColumn(header=...)igual que en CSV.
Respuesta:
"content": {
"summary": { "total": 1, "created": 1, "updated": 0, "skipped": 0, "invalid": 0 },
"errors": [], "jobId": null
}
Archivo imp_bad.pdf — cualquier contenido, lo que importa es la extensión no tabular:
contenido cualquiera
Respuesta:
{
"status": "BAD_REQUEST",
"message": "Formato de importacion no soportado",
"errors": ["Extension no soportada: pdf"],
"solutions": ["Use un archivo xlsx, csv o txt"]
}
HTTP 400 — extensión no soportada es error del cliente. El endpoint la rechaza antes de procesar.
| # | Entrada | created |
skipped |
invalid |
HTTP |
|---|---|---|---|---|---|
| 1 | 3 filas nuevas (CSV) | 3 | 0 | 0 | 200 |
| 2 | Reimportar las mismas | 0 | 3 | 0 | 200 |
| 3 | Mixto (válida+dup+3 malas) | 1 | 1 | 3 | 200 |
| 4 | 1 fila (TEXT/TSV) | 1 | 0 | 0 | 200 |
| 5 | Formato .pdf |
— | — | — | 400 |
nebula-models con @ImportColumn + Bean Validation (obligatorios, tamaños, tipos).AbstractImportProcess<T>; implementa solo validate + persist; importKey estable.persist usa repositorios nativos/tenant y reporta PersistOutcome (created/updated/skipped).ImportEndpoint<T> (importProcess + rowType + importOrchestrator) y ejecuta por handleImport; no reimplementa la llamada al orquestador.POST /api/v1/<recurso>/import (multipart, param archivo) y devuelve ImportResponse.ImportControl se arma con los parámetros de la petición vía ImportControlFactory; el controlador no fija ningún eje ni recorta formatos.AL_PRIMER_ERROR explícito.@Deprecated(forRemoval=true) (no romper); no eliminar los DTO /page.validate/persist + armado del control (canónicos, declarados, rechazos) + batería 1:1 contra el endpoint (éxito, parcial, formato, dedup).SimappeModel/SimappeCommons viajan de Catcsoft support/3.6.x (fuente de verdad) a Centrica por rama release y se publican a Nexus (repository/centrica). SemVer obligatorio.simappe-commons.version) y bumpea su propia versión.ImportAutoConfiguration vive en com.catcsoft.simappe.commons.autoconfigure (fuera del @ComponentScan de los servicios) para cargarse diferida y ver el PlatformTransactionManager del contexto. No moverla a ..api.v1.core...Referencia de arquitectura completa: SimappeCommons/docs/ADR-0001-capa-generica-importacion.md.
Cada afirmación de este documento apunta a un archivo real. Verificado el 30 de Julio de 2026.
| Pieza | Archivo | Qué prueba |
|---|---|---|
| Los nueve ejes | SimappeModel · com/catcsoft/simappe/model/core/imports/ImportControl.java |
record de 9 componentes; el canónico normaliza charset→UTF-8 y persistencia→PERSIST. Javadoc: "el dev decide aquí el comportamiento completo del proceso. El proceso lo honra, no lo decide" |
VALIDATE en un solo punto |
SimappeCommons · commons/api/v1/core/imports/ImportRunner.java |
corta antes de persistir según la política, cubriendo los tres executors |
| La firma impuesta | SimappeCommons · commons/api/v1/core/imports/ImportEndpoint.java |
handleImport(InputStream, ImportControl); el control es parámetro, no constante del módulo |
| Lectores por formato | commons/api/v1/core/imports/{Csv,Text,Xlsx,Xml,Json}ImportReader.java |
los cinco formatos del enum tienen lector: ninguno es teórico |
| Pieza | Archivo |
|---|---|
| Armado del control | nebula-commons · com/centrica/nebula/commons/imports/ImportControlFactory.java |
| Títulos i18n del módulo | nebula-commons · com/centrica/nebula/commons/imports/ImportControlMessages.java |
| Pruebas del armado | nebula-commons · test/.../imports/ImportControlFactoryTest.java — 15 pruebas: canónicos, mapa nulo, nueve ejes declarados, precedencia del formato, minúsculas/espacios, blanco≠declarado, seis extensiones, y los rechazos (extensión, sin extensión, formato inexistente, eje fuera de dominio, codificación, bloques) |
| Pieza | Archivo |
|---|---|
| Depreciación | nebula-accounting-core · v1/depreciacion/controller/DepreciacionController.java — implements ImportEndpoint<DepreciacionImportRow> |
| Dependencias | nebula-accounting-core · v1/dependencia/controller/DependenciasController.java — implements ImportEndpoint<DependenciaImportRow> |
| Firma aditiva probada | test/.../dependencia/controller/DependenciasImportEndpointTest.java — la llamada sin parámetros produce la carga idéntica a la que el controlador fijaba antes, eje por eje |
Compatibilidad de codificacion |
test/.../depreciacion/controller/DepreciacionImportEndpointTest.java — el parámetro de la firma anterior sigue vigente con el mismo efecto |
| Grafo sin ciclo | test/.../depreciacion/controller/DepreciacionImportWiringTest.java — contexto acotado con controlador, proceso, componente y lector histórico |
| Claves i18n del control | main/resources/db/i18n/V042__accounting_i18n_import_control_agnostico.sql — ES + EN |
| Pieza | Estado |
|---|---|
DepreciacionImportLauncher |
@Deprecated(forRemoval = true) — fijaba la carga en el módulo (VALIDATE impuesto). Se remueve en un MAJOR |
DepreciacionService.importarCanonico |
@Deprecated(forRemoval = true) — misma razón |
/importar, /guardar, /export-template, /bulk, /exportar |
intactos. La alineación no toca el flujo histórico ni la exportación |