Saltar a contenido

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:

  1. Añade columna schoolCourseId a hours_bank
  2. Añade columna schoolCourseId a hours_transactions
  3. Registros con actividad después del 2024-09-01 → curso actual
  4. Registros anteriores → null

Próximos Pasos

Use Cases Pendientes

  1. AddCompletedHoursUseCase - Agregar horas realizadas
  2. AddPendingHoursUseCase - Agregar horas pendientes
  3. EditTransactionUseCase - Editar transacción existente
  4. 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