Files
moraworld/apps/api/src/consolidations/consolidations.service.ts
T

181 lines
7.0 KiB
TypeScript

import {
Injectable,
NotFoundException,
BadRequestException,
Logger,
} from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service";
import { NotificationsService } from "../notifications/notifications.service";
function genCode(): string {
const date = new Date().toISOString().slice(0, 10).replace(/-/g, "");
const rand = Math.random().toString(36).substring(2, 8).toUpperCase();
return `CON-${date}-${rand}`;
}
@Injectable()
export class ConsolidationsService {
private readonly logger = new Logger(ConsolidationsService.name);
constructor(
private prisma: PrismaService,
private notifications: NotificationsService,
) {}
/** Lista consolidaciones del tenant. Cliente solo ve las suyas. */
async list(tenantId: string, userId?: string): Promise<any[]> {
return this.prisma.client.consolidation.findMany({
where: { tenantId, ...(userId ? { userId } : {}) },
include: {
packages: {
include: {
package: { select: { id: true, trackingId: true, description: true, declaredValue: true, actualWeight: true, status: true } },
},
},
},
orderBy: { createdAt: "desc" },
});
}
/** Obtiene una consolidación por ID */
async findOne(id: string, tenantId: string): Promise<any> {
const c = await this.prisma.client.consolidation.findFirst({
where: { id, tenantId },
include: {
packages: {
include: {
package: { select: { id: true, trackingId: true, description: true, declaredValue: true, actualWeight: true, status: true } },
},
},
},
});
if (!c) throw new NotFoundException("Consolidación no encontrada");
return c;
}
/** Crea una consolidación vacía para un cliente */
async create(tenantId: string, userId: string, notes?: string): Promise<any> {
return this.prisma.client.consolidation.create({
data: { tenantId, userId, code: genCode(), notes, status: "ABIERTA" },
});
}
/** Agrega un paquete a la consolidación (§21 — solo paquetes VERIFICADOS) */
async addPackage(id: string, packageId: string, tenantId: string): Promise<any> {
const c = await this.prisma.client.consolidation.findFirst({ where: { id, tenantId } });
if (!c) throw new NotFoundException("Consolidación no encontrada");
if (c.status !== "ABIERTA") throw new BadRequestException("Solo se pueden agregar paquetes a consolidaciones ABIERTAS");
const pkg = await this.prisma.client.package.findFirst({ where: { id: packageId, tenantId } });
if (!pkg) throw new NotFoundException("Paquete no encontrado");
if (!["VERIFICADO", "RECIBIDO_BODEGA"].includes(pkg.status)) {
throw new BadRequestException(`El paquete debe estar en estado VERIFICADO o RECIBIDO_BODEGA. Estado actual: ${pkg.status}`);
}
// Verificar que no esté ya en otra consolidación
const existing = await this.prisma.client.consolidationPackage.findUnique({ where: { packageId } });
if (existing) throw new BadRequestException("El paquete ya está en una consolidación");
await this.prisma.client.consolidationPackage.create({
data: { consolidationId: id, packageId },
});
return this.recalcTotals(id, tenantId);
}
/** Quita un paquete de la consolidación */
async removePackage(id: string, packageId: string, tenantId: string): Promise<any> {
const c = await this.prisma.client.consolidation.findFirst({ where: { id, tenantId } });
if (!c) throw new NotFoundException("Consolidación no encontrada");
if (c.status !== "ABIERTA") throw new BadRequestException("No se pueden quitar paquetes de una consolidación cerrada");
await this.prisma.client.consolidationPackage.deleteMany({
where: { consolidationId: id, packageId },
});
return this.recalcTotals(id, tenantId);
}
/** Cierra la consolidación y la marca lista para despacho */
async close(id: string, tenantId: string, courierTracking?: string): Promise<any> {
const c = await this.prisma.client.consolidation.findFirst({
where: { id, tenantId },
include: { packages: true },
});
if (!c) throw new NotFoundException("Consolidación no encontrada");
if (c.status !== "ABIERTA") throw new BadRequestException("La consolidación ya está cerrada");
if (c.packages.length === 0) throw new BadRequestException("No se puede cerrar una consolidación vacía");
return this.prisma.client.consolidation.update({
where: { id },
data: { status: "CERRADA", courierTracking: courierTracking ?? null },
});
}
/** Marca como despachada (courier recogió el paquete) */
async dispatch(id: string, tenantId: string, courierTracking: string): Promise<any> {
const c = await this.prisma.client.consolidation.findFirst({ where: { id, tenantId } });
if (!c) throw new NotFoundException("Consolidación no encontrada");
if (c.status !== "CERRADA") throw new BadRequestException("La consolidación debe estar CERRADA para despachar");
// Actualizar todos los paquetes a EN_TRANSITO_ECUADOR
const pkgLinks = await this.prisma.client.consolidationPackage.findMany({
where: { consolidationId: id },
select: { packageId: true },
});
await this.prisma.client.package.updateMany({
where: { id: { in: pkgLinks.map(p => p.packageId) } },
data: { status: "EN_TRANSITO_ECUADOR" },
});
// Notificar a cada cliente cuyo paquete fue despachado
for (const { packageId } of pkgLinks) {
try {
const pkg = await this.prisma.client.package.findUnique({
where: { id: packageId },
include: { user: true },
});
if (pkg) {
await this.notifications.notifyStatusChange(pkg, pkg.user);
}
} catch (err: any) {
this.logger.warn(`[CONSOLIDATION] Notif failed for pkg ${packageId}: ${err.message}`);
}
}
this.logger.log(`[CONSOLIDATION] Dispatched ${id}${pkgLinks.length} packages → EN_TRANSITO_ECUADOR`);
return this.prisma.client.consolidation.update({
where: { id },
data: { status: "DESPACHADA", courierTracking },
});
}
/** Recalcula totales de peso y valor */
private async recalcTotals(id: string, _tenantId: string): Promise<any> {
const cp = await this.prisma.client.consolidationPackage.findMany({
where: { consolidationId: id },
include: { package: { select: { declaredValue: true, actualWeight: true, declaredWeight: true } } },
});
const totalWeightLb = cp.reduce((s, cp) =>
s + Number(cp.package.actualWeight ?? cp.package.declaredWeight ?? 0), 0);
const totalValue = cp.reduce((s, cp) => s + Number(cp.package.declaredValue ?? 0), 0);
return this.prisma.client.consolidation.update({
where: { id },
data: {
totalWeightLb: Math.round(totalWeightLb * 100) / 100,
totalValue: Math.round(totalValue * 100) / 100,
},
include: {
packages: {
include: {
package: { select: { id: true, trackingId: true, description: true, declaredValue: true, actualWeight: true, status: true } },
},
},
},
});
}
}