181 lines
7.0 KiB
TypeScript
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 } },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
}
|