Arquitectura - Bolsa de Horas v2¶
Documentación técnica de la implementación de Bolsa de Horas siguiendo la arquitectura v2 (Clean Architecture).
Capas de la Arquitectura¶
┌─────────────────────────────────────────────────────────────┐
│ Infrastructure │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Controllers │ │ Routes │ │ RepositoryImpl │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Application │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Use Cases │ │
│ │ - GetEmployeeHoursBankUseCase │ │
│ │ - AddCompletedHoursUseCase (pendiente) │ │
│ │ - AddPendingHoursUseCase (pendiente) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Domain │
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
│ │ Entities │ │ Repository Interfaces │ │
│ │ - HoursBank │ │ - DomainHoursBankRepository │ │
│ └─────────────────┘ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Entidad de Dominio¶
HoursBank¶
// src/domain/entities/HoursBank.ts
export interface HoursTransactionData {
id: number;
transactionDate: Date;
type: "COMPLETED" | "PENDING" | "REVERSAL";
totalHours: number;
pendingAffected: number;
completedAffected: number;
reason?: string;
hoursBankId: number;
schoolCourseId?: number;
isActive: boolean;
}
export class HoursBank {
private id: number;
private employeeId: number;
private schoolCourseId?: number;
private totalPendingHours: number;
private totalCompletedHours: number;
private transactions: HoursTransactionData[];
// Getters...
// toJSON()...
}
Interface del Repositorio¶
DomainHoursBankRepository¶
// src/domain/repositories/DomainHoursBankRepository.ts
export interface DomainHoursBankRepository {
getByEmployeeAndCourse(employeeId: number, schoolCourseId: number): Promise<HoursBank | null>;
findOrCreate(employeeId: number, schoolCourseId: number): Promise<HoursBank>;
}
Caso de Uso¶
GetEmployeeHoursBankUseCase¶
// src/application/use-cases/hours-bank/GetEmployeeHoursBankUseCase.ts
export class GetEmployeeHoursBankUseCase {
constructor(
private readonly hoursBankRepository: DomainHoursBankRepository,
private readonly schoolCourseRepository: DomainSchoolCourseRepository
) {}
async execute(employeeId: number, schoolCourseId?: number): Promise<GetEmployeeHoursBankResult> {
// 1. Si no hay schoolCourseId, obtener el curso actual
// 2. Buscar la bolsa de horas
// 3. Formatear y retornar resultado
}
}
Responsabilidades: - Validar parámetros de entrada - Obtener curso escolar actual si no se proporciona - Formatear transacciones con resúmenes legibles - Agrupar transacciones por tipo
Implementación del Repositorio¶
HoursBankRepositoryImpl¶
// src/infrastructure/adapters/persistence/HoursBankRepositoryImpl.ts
export class HoursBankRepositoryImpl implements DomainHoursBankRepository {
async getByEmployeeAndCourse(employeeId: number, schoolCourseId: number): Promise<HoursBank | null> {
const hoursBank = await HoursBankModel.findOne({
where: { employeeId, schoolCourseId },
include: [{ model: HoursTransaction, as: "transactions", where: { isActive: true }, required: false }],
});
return hoursBank ? this.toDomain(hoursBank) : null;
}
private toDomain(model: HoursBankModel): HoursBank {
// Mapear modelo Sequelize a entidad de dominio
}
}
Controlador¶
HoursBankController¶
// src/infrastructure/controllers/hoursBank.controller.ts
export class HoursBankController {
constructor(
private readonly getEmployeeHoursBankUseCase: GetEmployeeHoursBankUseCase
) {}
getByEmployee = catchAsync(async (req: Request, res: Response) => {
const employeeId = parseInt(req.params.employeeId, 10);
const schoolCourseId = req.query.schoolCourseId
? parseInt(req.query.schoolCourseId as string, 10)
: undefined;
const result = await this.getEmployeeHoursBankUseCase.execute(employeeId, schoolCourseId);
res.status(200).json({ message: "ok", data: result });
});
}
Rutas¶
hoursBank.routes.ts¶
// src/infrastructure/http/routes/hoursBank.routes.ts
export class HoursBankRoutes {
constructor(private readonly controller: HoursBankController) {
this.router = Router();
this.initializeRoutes();
}
private initializeRoutes(): void {
this.router.get("/:employeeId", protectToken, this.controller.getByEmployee);
}
}
Wiring en app.ts¶
// src/app.ts
// Repositorio
const hoursBankRepository = new HoursBankRepositoryImpl();
// Use Case
const getEmployeeHoursBankUseCase = new GetEmployeeHoursBankUseCase(
hoursBankRepository,
schoolCourseRepository
);
// Controller
const hoursBankController = new HoursBankController(getEmployeeHoursBankUseCase);
// Ruta
apiRouterV2.use("/hours-bank", routerHoursBank(hoursBankController));
Migración de Datos¶
20241204200000-add-schoolCourseId-to-hours-bank.ts¶
La migración añade schoolCourseId a las tablas existentes y migra datos:
- Añade columna
schoolCourseIdahours_bank - Añade columna
schoolCourseIdahours_transactions - Registros con actividad después del 2024-09-01 → curso actual
- Registros anteriores →
null
Próximos Pasos¶
Use Cases Pendientes¶
- AddCompletedHoursUseCase - Agregar horas realizadas
- AddPendingHoursUseCase - Agregar horas pendientes
- EditTransactionUseCase - Editar transacción existente
- DeleteTransactionUseCase - Eliminar/revertir transacción
Consideraciones¶
- Mantener lógica de compensación automática
- Validar que no se puedan tener valores negativos
- Registrar todas las operaciones como transacciones
- Soportar reversiones sin eliminar historial
Última actualización: 2025-12-04