erp-transportistas-backend-v2/src/modules/combustible-gastos/services/carga-combustible.service.ts
Adrian Flores Cortes 0ff4089b71 feat: P2+P3 - Tests y servicios faltantes (TASK-006)
P2 - Tests unitarios creados (5 archivos):
- carta-porte/__tests__/carta-porte.service.spec.ts
- auth/__tests__/roles.service.spec.ts
- auth/__tests__/permissions.service.spec.ts
- tarifas-transporte/__tests__/tarifas.service.spec.ts
- tarifas-transporte/__tests__/lanes.service.spec.ts

P3 - Servicios implementados (19 servicios):
combustible-gastos (5):
- CargaCombustibleService, CrucePeajeService, GastoViajeService
- AnticipoViaticoService, ControlRendimientoService

hr (7 + DTOs):
- EmployeesService, DepartmentsService, PuestosService
- ContractsService, LeaveTypesService, LeaveAllocationsService, LeavesService

reports (7):
- ReportDefinitionService, ReportExecutionService, ReportScheduleService
- DashboardService, KpiSnapshotService, CustomReportService, DataModelService

Config: Excluir tests del build TypeScript (tsconfig.json)

Total: ~8,200 líneas de código

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 12:17:13 -06:00

294 lines
7.0 KiB
TypeScript

import { Repository, FindOptionsWhere, ILike, Between, In } from 'typeorm';
import { AppDataSource } from '../../../config/typeorm.js';
import { CargaCombustible, TipoCargaCombustible, EstadoGasto } from '../entities/carga-combustible.entity.js';
/**
* Search parameters for fuel loads
*/
export interface CargaCombustibleSearchParams {
tenantId: string;
search?: string;
unidadId?: string;
viajeId?: string;
operadorId?: string;
tipoCarga?: TipoCargaCombustible;
estado?: EstadoGasto;
estados?: EstadoGasto[];
fechaDesde?: Date;
fechaHasta?: Date;
limit?: number;
offset?: number;
}
/**
* DTO for creating a fuel load
*/
export interface CreateCargaCombustibleDto {
unidadId: string;
viajeId?: string;
operadorId: string;
tipoCarga: TipoCargaCombustible;
tipoCombustible: string;
litros: number;
precioLitro: number;
total: number;
odometroCarga?: number;
estacionId?: string;
estacionNombre?: string;
estacionDireccion?: string;
latitud?: number;
longitud?: number;
numeroVale?: string;
numeroFactura?: string;
folioTicket?: string;
fechaCarga: Date;
fotoTicketUrl?: string;
}
/**
* DTO for updating a fuel load
*/
export interface UpdateCargaCombustibleDto extends Partial<CreateCargaCombustibleDto> {
estado?: EstadoGasto;
rendimientoCalculado?: number;
}
/**
* Service for managing fuel loads (Cargas de Combustible)
*/
export class CargaCombustibleService {
private repository: Repository<CargaCombustible>;
constructor() {
this.repository = AppDataSource.getRepository(CargaCombustible);
}
/**
* Find all fuel loads with filters
*/
async findAll(params: CargaCombustibleSearchParams): Promise<{ data: CargaCombustible[]; total: number }> {
const {
tenantId,
search,
unidadId,
viajeId,
operadorId,
tipoCarga,
estado,
estados,
fechaDesde,
fechaHasta,
limit = 50,
offset = 0,
} = params;
const where: FindOptionsWhere<CargaCombustible>[] = [];
const baseWhere: FindOptionsWhere<CargaCombustible> = { tenantId };
if (unidadId) {
baseWhere.unidadId = unidadId;
}
if (viajeId) {
baseWhere.viajeId = viajeId;
}
if (operadorId) {
baseWhere.operadorId = operadorId;
}
if (tipoCarga) {
baseWhere.tipoCarga = tipoCarga;
}
if (estado) {
baseWhere.estado = estado;
}
if (estados && estados.length > 0) {
baseWhere.estado = In(estados);
}
if (fechaDesde && fechaHasta) {
baseWhere.fechaCarga = Between(fechaDesde, fechaHasta);
}
if (search) {
where.push(
{ ...baseWhere, estacionNombre: ILike(`%${search}%`) },
{ ...baseWhere, numeroVale: ILike(`%${search}%`) },
{ ...baseWhere, folioTicket: ILike(`%${search}%`) }
);
} else {
where.push(baseWhere);
}
const [data, total] = await this.repository.findAndCount({
where,
take: limit,
skip: offset,
order: { fechaCarga: 'DESC' },
});
return { data, total };
}
/**
* Find a single fuel load by ID
*/
async findOne(tenantId: string, id: string): Promise<CargaCombustible | null> {
return this.repository.findOne({
where: { id, tenantId },
});
}
/**
* Create a new fuel load
*/
async create(tenantId: string, dto: CreateCargaCombustibleDto, createdBy: string): Promise<CargaCombustible> {
const carga = this.repository.create({
...dto,
tenantId,
estado: EstadoGasto.PENDIENTE,
createdById: createdBy,
});
return this.repository.save(carga);
}
/**
* Update an existing fuel load
*/
async update(
tenantId: string,
id: string,
dto: UpdateCargaCombustibleDto
): Promise<CargaCombustible | null> {
const carga = await this.findOne(tenantId, id);
if (!carga) return null;
Object.assign(carga, dto);
return this.repository.save(carga);
}
/**
* Approve a fuel load
*/
async aprobar(tenantId: string, id: string, aprobadoPor: string): Promise<CargaCombustible | null> {
const carga = await this.findOne(tenantId, id);
if (!carga) return null;
if (carga.estado !== EstadoGasto.PENDIENTE) {
throw new Error('Solo se pueden aprobar cargas pendientes');
}
carga.estado = EstadoGasto.APROBADO;
carga.aprobadoPor = aprobadoPor;
carga.aprobadoFecha = new Date();
return this.repository.save(carga);
}
/**
* Reject a fuel load
*/
async rechazar(tenantId: string, id: string, aprobadoPor: string): Promise<CargaCombustible | null> {
const carga = await this.findOne(tenantId, id);
if (!carga) return null;
if (carga.estado !== EstadoGasto.PENDIENTE) {
throw new Error('Solo se pueden rechazar cargas pendientes');
}
carga.estado = EstadoGasto.RECHAZADO;
carga.aprobadoPor = aprobadoPor;
carga.aprobadoFecha = new Date();
return this.repository.save(carga);
}
/**
* Delete a fuel load (soft or hard)
*/
async delete(tenantId: string, id: string): Promise<boolean> {
const carga = await this.findOne(tenantId, id);
if (!carga) return false;
const result = await this.repository.delete(id);
return (result.affected ?? 0) > 0;
}
/**
* Get fuel loads by unit
*/
async getCargasPorUnidad(unidadId: string, tenantId: string): Promise<CargaCombustible[]> {
return this.repository.find({
where: { unidadId, tenantId },
order: { fechaCarga: 'DESC' },
take: 50,
});
}
/**
* Get fuel loads by trip
*/
async getCargasPorViaje(viajeId: string, tenantId: string): Promise<CargaCombustible[]> {
return this.repository.find({
where: { viajeId, tenantId },
order: { fechaCarga: 'ASC' },
});
}
/**
* Get pending fuel loads
*/
async getCargasPendientes(tenantId: string): Promise<CargaCombustible[]> {
return this.repository.find({
where: { tenantId, estado: EstadoGasto.PENDIENTE },
order: { fechaCarga: 'DESC' },
});
}
/**
* Calculate fuel performance for a unit in a period
*/
async calcularRendimiento(
unidadId: string,
tenantId: string,
fechaInicio: Date,
fechaFin: Date
): Promise<{ totalLitros: number; totalKm: number; rendimiento: number }> {
const cargas = await this.repository.find({
where: {
unidadId,
tenantId,
estado: EstadoGasto.APROBADO,
fechaCarga: Between(fechaInicio, fechaFin),
},
order: { fechaCarga: 'ASC' },
});
if (cargas.length < 2) {
return { totalLitros: 0, totalKm: 0, rendimiento: 0 };
}
let totalLitros = 0;
let kmInicial = cargas[0].odometroCarga || 0;
let kmFinal = kmInicial;
for (const carga of cargas) {
totalLitros += Number(carga.litros);
if (carga.odometroCarga) {
kmFinal = carga.odometroCarga;
}
}
const totalKm = kmFinal - kmInicial;
const rendimiento = totalLitros > 0 ? totalKm / totalLitros : 0;
return { totalLitros, totalKm, rendimiento };
}
}
export const cargaCombustibleService = new CargaCombustibleService();