Versión: 1.0
Fecha: 8 de Marzo, 2026
Arquitecto: Carlos Alberto Torres Camargo
Audiencia: Desarrolladores Backend (Senior y Junior)
SimappeModel y SimappeCommons instalados localmente (mvn clean install -DskipTests)nebula-models, nebula-commons, nebula-shared instalados localmente# Desde el repo del microservicio
cd nebula-accounting-core
git checkout develop
git pull origin develop
git checkout -b feature/NEB-15-crud-centro-costos
Archivo: nebula-models/src/main/java/com/centrica/nebula/model/accounting/CentroCostosDto.java
package com.centrica.nebula.model.accounting;
import co.simappe.model.base.BusinessBaseDto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class CentroCostosDto extends BusinessBaseDto {
private Long id;
@NotBlank(message = "El código es obligatorio")
@Size(max = 20, message = "El código no puede exceder 20 caracteres")
private String codigo;
@NotBlank(message = "El nombre es obligatorio")
@Size(max = 200, message = "El nombre no puede exceder 200 caracteres")
private String nombre;
private String descripcion;
private Long tipoCentroCostosId;
private String tipoCentroCostosNombre; // Para display (LightLoader)
private Boolean activo;
}
Instalar nebula-models:
cd nebula-models
mvn clean install -DskipTests
Archivo: nebula-accounting-core/src/main/java/com/centrica/nebula/accounting/core/v1/centrocostos/entity/CentroCostosEntity.java
package com.centrica.nebula.accounting.core.v1.centrocostos.entity;
import co.simappe.commons.jpa.entity.BusinessBaseEntity;
import co.simappe.commons.jpa.entity.LongId;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "centro_costos", schema = "accounting")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class CentroCostosEntity extends BusinessBaseEntity<LongId> {
@Column(name = "codigo", nullable = false, length = 20, unique = true)
private String codigo;
@Column(name = "nombre", nullable = false, length = 200)
private String nombre;
@Column(name = "descripcion", length = 500)
private String descripcion;
@Column(name = "tipo_centro_costos_id")
private Long tipoCentroCostosId;
@Column(name = "activo")
private Boolean activo = true;
}
Archivo: .../centrocostos/repository/CentroCostosRepository.java
package com.centrica.nebula.accounting.core.v1.centrocostos.repository;
import co.simappe.commons.jpa.entity.LongId;
import co.simappe.commons.jpa.repository.SimappeRepository;
import com.centrica.nebula.accounting.core.v1.centrocostos.entity.CentroCostosEntity;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface CentroCostosRepository extends SimappeRepository<CentroCostosEntity, LongId> {
Optional<CentroCostosEntity> findByCodigo(String codigo);
boolean existsByCodigo(String codigo);
List<CentroCostosEntity> findByActivoTrue();
List<CentroCostosEntity> findByTipoCentroCostosId(Long tipoCentroCostosId);
}
Archivo: .../centrocostos/component/CentroCostosComponent.java
package com.centrica.nebula.accounting.core.v1.centrocostos.component;
import co.simappe.commons.annotation.ConnectionContext;
import co.simappe.commons.annotation.EntityIdSupport;
import co.simappe.commons.jpa.service.SimappeService;
import co.simappe.commons.component.CrudHtppServletRequestComponent;
import co.simappe.commons.exception.SimappeException;
import co.simappe.commons.mapper.SimappeModelMapper;
import com.centrica.nebula.model.accounting.CentroCostosDto;
import com.centrica.nebula.accounting.core.v1.centrocostos.entity.CentroCostosEntity;
import com.centrica.nebula.accounting.core.v1.centrocostos.repository.CentroCostosRepository;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Component
@ConnectionContext
@EntityIdSupport
@RequiredArgsConstructor
public class CentroCostosComponent extends SimappeService<
CentroCostosDto, Long, CentroCostosEntity, CentroCostosRepository
> implements CrudHtppServletRequestComponent<CentroCostosDto, Long> {
private final CentroCostosRepository repository;
private final SimappeModelMapper mapper;
@Override
@Transactional(readOnly = true)
public List<CentroCostosDto> read(HttpServletRequest request) {
log.info("Leyendo centros de costos");
return repository.findAll().stream()
.map(entity -> mapper.map(entity, CentroCostosDto.class))
.collect(Collectors.toList());
}
@Override
@Transactional(readOnly = true)
public CentroCostosDto get(Long id, HttpServletRequest request) {
log.info("Obteniendo centro de costos: id={}", id);
CentroCostosEntity entity = repository.findById(new co.simappe.commons.jpa.entity.LongId(id))
.orElseThrow(() -> new SimappeException("Centro de costos no encontrado: " + id));
return mapper.map(entity, CentroCostosDto.class);
}
@Override
@Transactional
public CentroCostosDto create(CentroCostosDto dto, HttpServletRequest request) {
log.info("Creando centro de costos: codigo={}", dto.getCodigo());
// Validación de negocio
if (repository.existsByCodigo(dto.getCodigo())) {
throw new SimappeException("Ya existe un centro de costos con código: " + dto.getCodigo());
}
CentroCostosEntity entity = mapper.map(dto, CentroCostosEntity.class);
entity.setActivo(true);
CentroCostosEntity saved = repository.save(entity);
log.info("Centro de costos creado: id={}", saved.getId());
return mapper.map(saved, CentroCostosDto.class);
}
@Override
@Transactional
public CentroCostosDto update(CentroCostosDto dto, HttpServletRequest request) {
log.info("Actualizando centro de costos: id={}", dto.getId());
CentroCostosEntity existing = repository.findById(new co.simappe.commons.jpa.entity.LongId(dto.getId()))
.orElseThrow(() -> new SimappeException("Centro de costos no encontrado: " + dto.getId()));
// Validar código único (si cambió)
if (!existing.getCodigo().equals(dto.getCodigo()) && repository.existsByCodigo(dto.getCodigo())) {
throw new SimappeException("Ya existe un centro de costos con código: " + dto.getCodigo());
}
mapper.map(dto, existing);
CentroCostosEntity saved = repository.save(existing);
return mapper.map(saved, CentroCostosDto.class);
}
@Override
@Transactional
public void delete(Long id, HttpServletRequest request) {
log.info("Eliminando centro de costos: id={}", id);
CentroCostosEntity entity = repository.findById(new co.simappe.commons.jpa.entity.LongId(id))
.orElseThrow(() -> new SimappeException("Centro de costos no encontrado: " + id));
// Soft delete
entity.setActivo(false);
repository.save(entity);
log.info("Centro de costos desactivado: id={}", id);
}
}
Archivo: .../centrocostos/service/CentroCostosService.java
package com.centrica.nebula.accounting.core.v1.centrocostos.service;
import com.centrica.nebula.model.accounting.CentroCostosDto;
import com.centrica.nebula.accounting.core.v1.centrocostos.component.CentroCostosComponent;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class CentroCostosService {
private final CentroCostosComponent component;
public List<CentroCostosDto> read(HttpServletRequest request) {
return component.read(request);
}
public CentroCostosDto get(Long id, HttpServletRequest request) {
return component.get(id, request);
}
public CentroCostosDto create(CentroCostosDto dto, HttpServletRequest request) {
return component.create(dto, request);
}
public CentroCostosDto update(CentroCostosDto dto, HttpServletRequest request) {
return component.update(dto, request);
}
public void delete(Long id, HttpServletRequest request) {
component.delete(id, request);
}
}
Archivo: .../centrocostos/controller/CentroCostosController.java
package com.centrica.nebula.accounting.core.v1.centrocostos.controller;
import com.centrica.nebula.model.accounting.CentroCostosDto;
import com.centrica.nebula.accounting.core.v1.centrocostos.service.CentroCostosService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/v1/accounting/centro-costos")
@RequiredArgsConstructor
@Tag(name = "Centro de Costos", description = "Gestión de centros de costos contables")
public class CentroCostosController {
private final CentroCostosService service;
@GetMapping("/read")
@Operation(summary = "Listar todos los centros de costos")
public ResponseEntity<List<CentroCostosDto>> read(HttpServletRequest request) {
return ResponseEntity.ok(service.read(request));
}
@GetMapping("/get/{id}")
@Operation(summary = "Obtener un centro de costos por ID")
public ResponseEntity<CentroCostosDto> get(
@PathVariable Long id,
HttpServletRequest request) {
return ResponseEntity.ok(service.get(id, request));
}
@PostMapping("/create")
@Operation(summary = "Crear un nuevo centro de costos")
public ResponseEntity<CentroCostosDto> create(
@Valid @RequestBody CentroCostosDto dto,
HttpServletRequest request) {
return ResponseEntity.ok(service.create(dto, request));
}
@PutMapping("/update")
@Operation(summary = "Actualizar un centro de costos existente")
public ResponseEntity<CentroCostosDto> update(
@Valid @RequestBody CentroCostosDto dto,
HttpServletRequest request) {
return ResponseEntity.ok(service.update(dto, request));
}
@DeleteMapping("/delete/{id}")
@Operation(summary = "Eliminar (desactivar) un centro de costos")
public ResponseEntity<Void> delete(
@PathVariable Long id,
HttpServletRequest request) {
service.delete(id, request);
return ResponseEntity.ok().build();
}
}
Archivo: src/main/resources/db/migration/oracle/V2__create_table_centro_costos.sql
CREATE TABLE centro_costos (
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
codigo VARCHAR2(20) NOT NULL UNIQUE,
nombre VARCHAR2(200) NOT NULL,
descripcion VARCHAR2(500),
tipo_centro_costos_id NUMBER,
activo NUMBER(1) DEFAULT 1,
-- Audit fields (BusinessBaseEntity)
status VARCHAR2(20) DEFAULT 'ACTIVE',
created_by VARCHAR2(100),
updated_by VARCHAR2(100),
created_at TIMESTAMP DEFAULT SYSTIMESTAMP,
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP,
version NUMBER DEFAULT 0,
client_id VARCHAR2(50),
company_id NUMBER,
subsidiary_id NUMBER,
CONSTRAINT fk_tipo_centro_costos
FOREIGN KEY (tipo_centro_costos_id)
REFERENCES tipo_centro_costos(id)
);
CREATE INDEX idx_centro_costos_codigo ON centro_costos(codigo);
CREATE INDEX idx_centro_costos_tipo ON centro_costos(tipo_centro_costos_id);
CREATE INDEX idx_centro_costos_activo ON centro_costos(activo);
# 1. Compilar el microservicio
cd nebula-accounting-core
mvn clean package -DskipTests
# 2. Ejecutar tests
mvn test
# 3. Verificar que compila con el perfil localhost
./mvnw spring-boot:run -Dspring-boot.run.profiles=localhost
# 4. Verificar Swagger
# Abrir: http://localhost:8080/swagger-ui.html
# Deben aparecer los 5 endpoints de centro-costos
1. Login → POST /oauth/token → obtener JWT
2. Listar → GET /api/v1/accounting/centro-costos/read
Header: Authorization: Bearer {JWT}
Respuesta esperada: [] (lista vacía)
3. Crear → POST /api/v1/accounting/centro-costos/create
Header: Authorization: Bearer {JWT}
Body:
{
"codigo": "CC-001",
"nombre": "Administración Central",
"descripcion": "Centro de costos principal",
"tipoCentroCostosId": 1
}
Respuesta esperada: 200 con el objeto creado
4. Obtener → GET /api/v1/accounting/centro-costos/get/1
Respuesta esperada: 200 con el objeto
5. Actualizar → PUT /api/v1/accounting/centro-costos/update
Body: { "id": 1, "codigo": "CC-001", "nombre": "Admin Central (editado)" }
6. Eliminar → DELETE /api/v1/accounting/centro-costos/delete/1
Respuesta esperada: 200 (soft delete)
7. Verificar soft delete → GET /read → el registro tiene activo=false
# Commit en nebula-models (si se creó DTO nuevo)
cd nebula-models
git add src/main/java/com/centrica/nebula/model/accounting/CentroCostosDto.java
git commit -m "feat(models): add CentroCostosDto for accounting domain"
git push origin feature/NEB-15-crud-centro-costos
# Commit en microservicio
cd nebula-accounting-core
git add .
git commit -m "feat(accounting): implement CRUD for centro de costos
- Entity, Repository, Component, Service, Controller
- Flyway migration V2 for centro_costos table
- Business validation: unique codigo
- Soft delete pattern
- Swagger documentation"
git push origin feature/NEB-15-crud-centro-costos
# Crear PR en GitLab
# Asignar a: Sr. Backend (Gatekeeper)
# Labels: domain:accounting, stack:backend, type:feature
# Si tocó nebula-models → también asignar al Arquitecto
Pipeline Jenkins:
1. Build ─────── mvn clean package -DskipTests ✅
2. Test ──────── mvn test ✅
3. SonarQube ─── mvn sonar:sonar ✅ (coverage ≥ 70%)
4. Docker Build ─ docker build ✅
5. Docker Push ── docker push to registry ✅
6. Deploy DEV ─── docker-compose up (auto en develop) ✅
Gatekeeper revisa PR → Aprueba → Merge a develop → Deploy automático a DEV
HU asignada
│
├── 1. git checkout -b feature/NEB-XX
│
├── 2. Crear DTO en nebula-models
│ └── mvn clean install -DskipTests
│
├── 3. En microservicio crear:
│ ├── Entity (@Entity, extends BusinessBaseEntity)
│ ├── Repository (extends SimappeRepository)
│ ├── Component (@ConnectionContext, extends SimappeService) ← LÓGICA
│ ├── Service (facade, delega a Component)
│ └── Controller (@RestController, endpoints REST)
│
├── 4. Crear migración Flyway (SQL)
│
├── 5. mvn clean package -DskipTests (compilar)
│
├── 6. mvn test (tests unitarios)
│
├── 7. Probar con Postman (5 endpoints CRUD)
│
├── 8. git commit -m "feat(domain): description"
│
├── 9. git push → Crear PR → Asignar a Gatekeeper
│
└── 10. Pipeline verde → Review → Merge → Deploy DEV
| Version | Fecha | Autor | Descripcion |
|---|---|---|---|
| 1.0.0 | 2026-03-08 | Carlos Torres | Creación de la guía paso a paso para features backend |