✅ FASE 6 PARCIAL: Extras y Diferenciadores (base implementada)
Implementados módulos base de Fase 6: 1. WALL OF FAME (base) - Modelo de base de datos - Servicio CRUD - Controladores - Endpoints: GET /wall-of-fame/* 2. ACHIEVEMENTS/LOGROS (base) - Modelo de logros desbloqueables - Servicio de progreso - Controladores base - Endpoints: GET /achievements/* 3. QR CHECK-IN (completo) - Generación de códigos QR - Validación y procesamiento - Check-in/check-out - Endpoints: /checkin/* 4. BASE DE DATOS - Tablas: wall_of_fame, achievements, qr_codes, check_ins - Tablas preparadas: equipment, orders, notifications, activities Estructura lista para: - Equipment/Material rental - Orders/Servicios del club - Wearables integration - Challenges/Retos Nota: Algunos módulos avanzados requieren ajustes finales.
This commit is contained in:
753
backend/src/services/equipmentRental.service.ts
Normal file
753
backend/src/services/equipmentRental.service.ts
Normal file
@@ -0,0 +1,753 @@
|
||||
import prisma from '../config/database';
|
||||
import { ApiError } from '../middleware/errorHandler';
|
||||
import logger from '../config/logger';
|
||||
import { PaymentService } from './payment.service';
|
||||
import { EquipmentService } from './equipment.service';
|
||||
|
||||
// Estados del alquiler
|
||||
export const RentalStatus = {
|
||||
RESERVED: 'RESERVED',
|
||||
PICKED_UP: 'PICKED_UP',
|
||||
RETURNED: 'RETURNED',
|
||||
LATE: 'LATE',
|
||||
DAMAGED: 'DAMAGED',
|
||||
CANCELLED: 'CANCELLED',
|
||||
} as const;
|
||||
|
||||
export type RentalStatusType = typeof RentalStatus[keyof typeof RentalStatus];
|
||||
|
||||
// Interfaces
|
||||
export interface RentalItemInput {
|
||||
itemId: string;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface CreateRentalInput {
|
||||
items: RentalItemInput[];
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
bookingId?: string;
|
||||
}
|
||||
|
||||
export class EquipmentRentalService {
|
||||
/**
|
||||
* Crear un nuevo alquiler
|
||||
*/
|
||||
static async createRental(userId: string, data: CreateRentalInput) {
|
||||
const { items, startDate, endDate, bookingId } = data;
|
||||
|
||||
// Validar fechas
|
||||
const now = new Date();
|
||||
if (new Date(startDate) < now) {
|
||||
throw new ApiError('La fecha de inicio debe ser futura', 400);
|
||||
}
|
||||
|
||||
if (new Date(endDate) <= new Date(startDate)) {
|
||||
throw new ApiError('La fecha de fin debe ser posterior a la de inicio', 400);
|
||||
}
|
||||
|
||||
// Validar que hay items
|
||||
if (!items || items.length === 0) {
|
||||
throw new ApiError('Debe seleccionar al menos un item', 400);
|
||||
}
|
||||
|
||||
// Verificar booking si se proporciona
|
||||
if (bookingId) {
|
||||
const booking = await prisma.booking.findFirst({
|
||||
where: {
|
||||
id: bookingId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
throw new ApiError('Reserva no encontrada', 404);
|
||||
}
|
||||
|
||||
// Verificar que las fechas del alquiler coincidan con la reserva
|
||||
const bookingDate = new Date(booking.date);
|
||||
const rentalStart = new Date(startDate);
|
||||
|
||||
if (
|
||||
bookingDate.getDate() !== rentalStart.getDate() ||
|
||||
bookingDate.getMonth() !== rentalStart.getMonth() ||
|
||||
bookingDate.getFullYear() !== rentalStart.getFullYear()
|
||||
) {
|
||||
throw new ApiError(
|
||||
'Las fechas del alquiler deben coincidir con la fecha de la reserva',
|
||||
400
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar disponibilidad de cada item
|
||||
const rentalItems = [];
|
||||
let totalCost = 0;
|
||||
let totalDeposit = 0;
|
||||
|
||||
for (const itemInput of items) {
|
||||
const equipment = await prisma.equipmentItem.findUnique({
|
||||
where: {
|
||||
id: itemInput.itemId,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!equipment) {
|
||||
throw new ApiError(
|
||||
`Equipamiento no encontrado: ${itemInput.itemId}`,
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar disponibilidad
|
||||
const availability = await EquipmentService.checkAvailability(
|
||||
itemInput.itemId,
|
||||
new Date(startDate),
|
||||
new Date(endDate)
|
||||
);
|
||||
|
||||
if (!availability.available || availability.quantityAvailable < itemInput.quantity) {
|
||||
throw new ApiError(
|
||||
`No hay suficiente stock disponible para: ${equipment.name}`,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
// Calcular duración en horas y días
|
||||
const durationMs = new Date(endDate).getTime() - new Date(startDate).getTime();
|
||||
const durationHours = Math.ceil(durationMs / (1000 * 60 * 60));
|
||||
const durationDays = Math.ceil(durationHours / 24);
|
||||
|
||||
// Calcular costo
|
||||
let itemCost = 0;
|
||||
if (equipment.dailyRate && durationDays >= 1) {
|
||||
itemCost = equipment.dailyRate * durationDays * itemInput.quantity;
|
||||
} else if (equipment.hourlyRate) {
|
||||
itemCost = equipment.hourlyRate * durationHours * itemInput.quantity;
|
||||
}
|
||||
|
||||
totalCost += itemCost;
|
||||
totalDeposit += equipment.depositRequired * itemInput.quantity;
|
||||
|
||||
rentalItems.push({
|
||||
itemId: itemInput.itemId,
|
||||
quantity: itemInput.quantity,
|
||||
hourlyRate: equipment.hourlyRate,
|
||||
dailyRate: equipment.dailyRate,
|
||||
equipment,
|
||||
});
|
||||
}
|
||||
|
||||
// Crear el alquiler
|
||||
const rental = await prisma.equipmentRental.create({
|
||||
data: {
|
||||
userId,
|
||||
bookingId: bookingId || null,
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
totalCost,
|
||||
depositAmount: totalDeposit,
|
||||
status: RentalStatus.RESERVED,
|
||||
items: {
|
||||
create: rentalItems.map((ri) => ({
|
||||
itemId: ri.itemId,
|
||||
quantity: ri.quantity,
|
||||
hourlyRate: ri.hourlyRate,
|
||||
dailyRate: ri.dailyRate,
|
||||
})),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
item: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Actualizar cantidades disponibles
|
||||
for (const item of rentalItems) {
|
||||
await prisma.equipmentItem.update({
|
||||
where: { id: item.itemId },
|
||||
data: {
|
||||
quantityAvailable: {
|
||||
decrement: item.quantity,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Crear preferencia de pago en MercadoPago
|
||||
let paymentPreference = null;
|
||||
try {
|
||||
const payment = await PaymentService.createPreference(userId, {
|
||||
type: 'EQUIPMENT_RENTAL',
|
||||
referenceId: rental.id,
|
||||
title: `Alquiler de equipamiento - ${rental.items.length} items`,
|
||||
description: `Alquiler de material deportivo desde ${startDate.toLocaleDateString()} hasta ${endDate.toLocaleDateString()}`,
|
||||
amount: totalCost,
|
||||
metadata: {
|
||||
rentalId: rental.id,
|
||||
items: rentalItems.map((ri) => ({
|
||||
name: ri.equipment.name,
|
||||
quantity: ri.quantity,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
paymentPreference = {
|
||||
id: payment.id,
|
||||
initPoint: payment.initPoint,
|
||||
sandboxInitPoint: payment.sandboxInitPoint,
|
||||
};
|
||||
|
||||
// Actualizar rental con el paymentId
|
||||
await prisma.equipmentRental.update({
|
||||
where: { id: rental.id },
|
||||
data: {
|
||||
paymentId: payment.paymentId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error creando preferencia de pago para alquiler:', error);
|
||||
// No fallar el alquiler si el pago falla, pero informar
|
||||
}
|
||||
|
||||
logger.info(`Alquiler creado: ${rental.id} para usuario ${userId}`);
|
||||
|
||||
return {
|
||||
rental: {
|
||||
id: rental.id,
|
||||
startDate: rental.startDate,
|
||||
endDate: rental.endDate,
|
||||
totalCost: rental.totalCost,
|
||||
depositAmount: rental.depositAmount,
|
||||
status: rental.status,
|
||||
items: rental.items.map((ri) => ({
|
||||
id: ri.id,
|
||||
quantity: ri.quantity,
|
||||
item: {
|
||||
id: ri.item.id,
|
||||
name: ri.item.name,
|
||||
category: ri.item.category,
|
||||
brand: ri.item.brand,
|
||||
imageUrl: ri.item.imageUrl,
|
||||
},
|
||||
})),
|
||||
},
|
||||
user: rental.user,
|
||||
payment: paymentPreference,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesar webhook de pago de MercadoPago
|
||||
*/
|
||||
static async processPaymentWebhook(payload: any) {
|
||||
// El webhook es procesado por PaymentService
|
||||
// Aquí podemos hacer acciones adicionales si es necesario
|
||||
logger.info('Webhook de pago para alquiler procesado', { payload });
|
||||
return { processed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualizar estado del alquiler después del pago
|
||||
*/
|
||||
static async confirmRentalPayment(rentalId: string) {
|
||||
const rental = await prisma.equipmentRental.findUnique({
|
||||
where: { id: rentalId },
|
||||
});
|
||||
|
||||
if (!rental) {
|
||||
throw new ApiError('Alquiler no encontrado', 404);
|
||||
}
|
||||
|
||||
if (rental.status !== RentalStatus.RESERVED) {
|
||||
throw new ApiError('El alquiler ya no está en estado reservado', 400);
|
||||
}
|
||||
|
||||
// El alquiler se mantiene en RESERVED hasta que se retire el material
|
||||
logger.info(`Pago confirmado para alquiler: ${rentalId}`);
|
||||
|
||||
return rental;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener mis alquileres
|
||||
*/
|
||||
static async getMyRentals(userId: string) {
|
||||
const rentals = await prisma.equipmentRental.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
item: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
category: true,
|
||||
brand: true,
|
||||
imageUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
booking: {
|
||||
select: {
|
||||
id: true,
|
||||
date: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
court: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
return rentals.map((rental) => ({
|
||||
id: rental.id,
|
||||
startDate: rental.startDate,
|
||||
endDate: rental.endDate,
|
||||
totalCost: rental.totalCost,
|
||||
depositAmount: rental.depositAmount,
|
||||
depositReturned: rental.depositReturned,
|
||||
status: rental.status,
|
||||
pickedUpAt: rental.pickedUpAt,
|
||||
returnedAt: rental.returnedAt,
|
||||
items: rental.items.map((ri) => ({
|
||||
id: ri.id,
|
||||
quantity: ri.quantity,
|
||||
item: ri.item,
|
||||
})),
|
||||
booking: rental.booking,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener detalle de un alquiler
|
||||
*/
|
||||
static async getRentalById(id: string, userId?: string) {
|
||||
const rental = await prisma.equipmentRental.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
item: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
phone: true,
|
||||
},
|
||||
},
|
||||
booking: {
|
||||
select: {
|
||||
id: true,
|
||||
date: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
court: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!rental) {
|
||||
throw new ApiError('Alquiler no encontrado', 404);
|
||||
}
|
||||
|
||||
// Si se proporciona userId, verificar que sea el dueño
|
||||
if (userId && rental.userId !== userId) {
|
||||
throw new ApiError('No tienes permiso para ver este alquiler', 403);
|
||||
}
|
||||
|
||||
return rental;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entregar material (pickup) - Admin
|
||||
*/
|
||||
static async pickUpRental(rentalId: string, adminId: string) {
|
||||
const rental = await prisma.equipmentRental.findUnique({
|
||||
where: { id: rentalId },
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
item: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!rental) {
|
||||
throw new ApiError('Alquiler no encontrado', 404);
|
||||
}
|
||||
|
||||
if (rental.status !== RentalStatus.RESERVED) {
|
||||
throw new ApiError(
|
||||
`No se puede entregar el material. Estado actual: ${rental.status}`,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const updated = await prisma.equipmentRental.update({
|
||||
where: { id: rentalId },
|
||||
data: {
|
||||
status: RentalStatus.PICKED_UP,
|
||||
pickedUpAt: new Date(),
|
||||
},
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
item: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
category: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`Material entregado para alquiler: ${rentalId} por admin ${adminId}`);
|
||||
|
||||
return {
|
||||
rental: updated,
|
||||
user: rental.user,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Devolver material - Admin
|
||||
*/
|
||||
static async returnRental(
|
||||
rentalId: string,
|
||||
adminId: string,
|
||||
condition?: string,
|
||||
depositReturned?: number
|
||||
) {
|
||||
const rental = await prisma.equipmentRental.findUnique({
|
||||
where: { id: rentalId },
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
item: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!rental) {
|
||||
throw new ApiError('Alquiler no encontrado', 404);
|
||||
}
|
||||
|
||||
if (rental.status !== RentalStatus.PICKED_UP && rental.status !== RentalStatus.LATE) {
|
||||
throw new ApiError(
|
||||
`No se puede devolver el material. Estado actual: ${rental.status}`,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
// Determinar nuevo estado
|
||||
let newStatus: RentalStatusType = RentalStatus.RETURNED;
|
||||
const notes = [];
|
||||
|
||||
if (condition) {
|
||||
notes.push(`Condición al devolver: ${condition}`);
|
||||
if (condition === 'DAMAGED') {
|
||||
newStatus = RentalStatus.DAMAGED;
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar si está vencido
|
||||
const now = new Date();
|
||||
if (now > new Date(rental.endDate) && newStatus !== RentalStatus.DAMAGED) {
|
||||
newStatus = RentalStatus.LATE;
|
||||
}
|
||||
|
||||
const depositReturnAmount = depositReturned ?? rental.depositAmount;
|
||||
|
||||
const updated = await prisma.equipmentRental.update({
|
||||
where: { id: rentalId },
|
||||
data: {
|
||||
status: newStatus,
|
||||
returnedAt: now,
|
||||
depositReturned: depositReturnAmount,
|
||||
notes: notes.length > 0 ? notes.join(' | ') : undefined,
|
||||
},
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
item: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Restaurar cantidades disponibles
|
||||
for (const item of rental.items) {
|
||||
await prisma.equipmentItem.update({
|
||||
where: { id: item.itemId },
|
||||
data: {
|
||||
quantityAvailable: {
|
||||
increment: item.quantity,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`Material devuelto para alquiler: ${rentalId} por admin ${adminId}`);
|
||||
|
||||
return {
|
||||
rental: updated,
|
||||
user: updated.user,
|
||||
depositReturned: depositReturnAmount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancelar alquiler
|
||||
*/
|
||||
static async cancelRental(rentalId: string, userId: string) {
|
||||
const rental = await prisma.equipmentRental.findUnique({
|
||||
where: { id: rentalId },
|
||||
include: {
|
||||
items: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!rental) {
|
||||
throw new ApiError('Alquiler no encontrado', 404);
|
||||
}
|
||||
|
||||
// Verificar que sea el dueño
|
||||
if (rental.userId !== userId) {
|
||||
throw new ApiError('No tienes permiso para cancelar este alquiler', 403);
|
||||
}
|
||||
|
||||
// Solo se puede cancelar si está RESERVED
|
||||
if (rental.status !== RentalStatus.RESERVED) {
|
||||
throw new ApiError(
|
||||
`No se puede cancelar el alquiler. Estado actual: ${rental.status}`,
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const updated = await prisma.equipmentRental.update({
|
||||
where: { id: rentalId },
|
||||
data: {
|
||||
status: RentalStatus.CANCELLED,
|
||||
},
|
||||
});
|
||||
|
||||
// Restaurar cantidades disponibles
|
||||
for (const item of rental.items) {
|
||||
await prisma.equipmentItem.update({
|
||||
where: { id: item.itemId },
|
||||
data: {
|
||||
quantityAvailable: {
|
||||
increment: item.quantity,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`Alquiler cancelado: ${rentalId} por usuario ${userId}`);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener alquileres vencidos (admin)
|
||||
*/
|
||||
static async getOverdueRentals() {
|
||||
const now = new Date();
|
||||
|
||||
const overdue = await prisma.equipmentRental.findMany({
|
||||
where: {
|
||||
endDate: {
|
||||
lt: now,
|
||||
},
|
||||
status: {
|
||||
in: [RentalStatus.RESERVED, RentalStatus.PICKED_UP],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
item: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
category: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
phone: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
endDate: 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
// Actualizar estado a LATE para los que están PICKED_UP
|
||||
for (const rental of overdue) {
|
||||
if (rental.status === RentalStatus.PICKED_UP) {
|
||||
await prisma.equipmentRental.update({
|
||||
where: { id: rental.id },
|
||||
data: { status: RentalStatus.LATE },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return overdue.map((rental) => ({
|
||||
...rental,
|
||||
overdueHours: Math.floor(
|
||||
(now.getTime() - new Date(rental.endDate).getTime()) / (1000 * 60 * 60)
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener todos los alquileres (admin)
|
||||
*/
|
||||
static async getAllRentals(filters?: { status?: RentalStatusType; userId?: string }) {
|
||||
const where: any = {};
|
||||
|
||||
if (filters?.status) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
|
||||
if (filters?.userId) {
|
||||
where.userId = filters.userId;
|
||||
}
|
||||
|
||||
const rentals = await prisma.equipmentRental.findMany({
|
||||
where,
|
||||
include: {
|
||||
items: {
|
||||
include: {
|
||||
item: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
category: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
return rentals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener estadísticas de alquileres
|
||||
*/
|
||||
static async getRentalStats() {
|
||||
const [
|
||||
totalRentals,
|
||||
activeRentals,
|
||||
reservedRentals,
|
||||
returnedRentals,
|
||||
lateRentals,
|
||||
damagedRentals,
|
||||
totalRevenue,
|
||||
] = await Promise.all([
|
||||
prisma.equipmentRental.count(),
|
||||
prisma.equipmentRental.count({ where: { status: RentalStatus.PICKED_UP } }),
|
||||
prisma.equipmentRental.count({ where: { status: RentalStatus.RESERVED } }),
|
||||
prisma.equipmentRental.count({ where: { status: RentalStatus.RETURNED } }),
|
||||
prisma.equipmentRental.count({ where: { status: RentalStatus.LATE } }),
|
||||
prisma.equipmentRental.count({ where: { status: RentalStatus.DAMAGED } }),
|
||||
prisma.equipmentRental.aggregate({
|
||||
_sum: {
|
||||
totalCost: true,
|
||||
},
|
||||
where: {
|
||||
status: {
|
||||
in: [RentalStatus.PICKED_UP, RentalStatus.RETURNED],
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
total: totalRentals,
|
||||
byStatus: {
|
||||
reserved: reservedRentals,
|
||||
active: activeRentals,
|
||||
returned: returnedRentals,
|
||||
late: lateRentals,
|
||||
damaged: damagedRentals,
|
||||
},
|
||||
totalRevenue: totalRevenue._sum.totalCost || 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default EquipmentRentalService;
|
||||
Reference in New Issue
Block a user