import { Injectable } from '@nestjs/common'; import { Anomaly, Prisma, SolutionTarget, SolutionTargetStep, Tour, } from '@prisma/client'; import { Interval } from 'date-fns'; import { TraceTypes } from 'src/core/base/trace.types'; import { PrismaService } from 'src/core/database/prisma.service'; import { AnomalyDetectorService } from './anomaly-detectors/anomaly-detector.service'; import { AnomalyTypes } from './anomaly-detectors/anomaly-types.enum'; import { DriverCertificateDetector } from './anomaly-detectors/driver-certificate.detector'; import { DriverCodriverSameDetector } from './anomaly-detectors/driver-codriver-same.detector'; import { EmployeesLoggedInDetector } from './anomaly-detectors/employees-logged-in.detector'; import { OneTourAtATimeDetector } from './anomaly-detectors/one-tour-at-a-time.detector'; import { WorkingEmployeeNotInDistributionSystemDetector } from './anomaly-detectors/working-employee-not-in-distribution-system.detector'; @Injectable() export class AnomaliesService { private detectors = [ new EmployeesLoggedInDetector(this.anomalyDetectorService), new DriverCertificateDetector(this.anomalyDetectorService), new DriverCodriverSameDetector(this.anomalyDetectorService), new OneTourAtATimeDetector(this.anomalyDetectorService), new WorkingEmployeeNotInDistributionSystemDetector( this.anomalyDetectorService, ), ]; constructor( private readonly prisma: PrismaService, private readonly anomalyDetectorService: AnomalyDetectorService, ) {} detectAnomalies(interval: Interval): Promise { return Promise.all( this.detectors.map((detector) => detector.detect(interval)), ).then(async (anomaliesPerDetector) => { const anomalies = anomaliesPerDetector.flat(); const anomalyEntities: Anomaly[] = []; for (const anomaly of anomalies) { if (anomaly) { anomalyEntities.push( await this.prisma.anomaly.upsert({ where: { id: anomaly.id, }, create: { ...anomaly, }, update: {}, }), ); } } return anomalyEntities; }); } async validateAnomalies(): Promise { const anomalies = await this.prisma.anomaly.findMany({ where: { resolvedAt: null, }, }); return Promise.all( anomalies.map((anomaly) => this.detectors .find((detector) => detector.type === anomaly.type) .validate(anomaly), ), ).then(async (anomalies) => { const validatedAnomalies = anomalies.flat(); const updatedAnomalies: Anomaly[] = []; for (const anomaly of validatedAnomalies) { updatedAnomalies.push( await this.prisma.anomaly.update({ where: { id: anomaly.id, }, data: { resolvedAt: anomaly.resolvedAt, manuallyResolved: false, }, }), ); } return updatedAnomalies; }); } countActiveAnomalies(types: AnomalyTypes[] = []): Promise { return this.prisma.anomaly.count({ where: this.getWhereActiveAnomalyQuery(types), }); } findActiveAnomalies( skip?: number, take?: number, types: AnomalyTypes[] = [], ): Promise { return this.prisma.anomaly.findMany({ ...{ ...(skip ? { skip, take: 10 } : {}), ...(take ? { take } : {}), ...(take && !skip ? { skip: 0 } : {}), }, where: this.getWhereActiveAnomalyQuery(types), orderBy: { groupKey: 'asc', }, include: { solution: true, }, }); } async findActiveAnomaliesGroupedByGroupKey( skip = 0, take = 10, ): Promise<{ totalCount: number; anomalies: { key: string; anomalies: Anomaly[] }[]; }> { const activeAnomalies = await this.findActiveAnomalies( undefined, undefined, [ AnomalyTypes.DriverCertificate, AnomalyTypes.EmployeesLoggedIn, AnomalyTypes.OneTourAtATime, ], ); const groupedAnomalies = this.groupAnomaliesByGroupKey(activeAnomalies); return { totalCount: Object.keys(groupedAnomalies).length, anomalies: Object.entries(groupedAnomalies) .map(([key, anomalies]) => ({ key, anomalies, })) .slice(skip, skip + take), }; } findById(id: string): Promise { return this.prisma.anomaly.findUnique({ where: { id, }, }); } findSolutionTargetsByAnomalyId(anomalyId: string): Promise { return this.prisma.solutionTarget.findMany({ where: { anomalyId, }, }); } findSolutionTargetStepsBySolutionTargetId( solutionTargetId: string, ): Promise { return this.prisma.solutionTargetStep.findMany({ where: { solutionTargetId, }, }); } findTourByOperationId(operationId: string): Promise { return this.prisma.tour.findUnique({ where: { operationId } }); } async setAnomalyToSleep(anomalyId: string): Promise { await this.prisma.trace.create({ data: { entityKey: anomalyId, type: TraceTypes.AnomalyIgnored, }, }); return this.prisma.anomaly.update({ where: { id: anomalyId, }, data: { sleepSince: new Date(), }, }); } async acceptAnomaly(anomalyId: string): Promise { await this.prisma.trace.create({ data: { entityKey: anomalyId, type: TraceTypes.AnomalyAccepted, }, }); return this.prisma.anomaly.update({ where: { id: anomalyId, }, data: { resolvedAt: new Date(), manuallyResolved: true, }, }); } private getWhereActiveAnomalyQuery( types: AnomalyTypes[] = [], ): Prisma.AnomalyWhereInput { return { ...(types.length ? { type: { in: types } } : {}), resolvedAt: null, OR: [ { sleepSince: null, }, { sleepSince: { lt: new Date(new Date().getTime() - 60 * 60 * 1000), }, }, ], }; } private groupAnomaliesByGroupKey( anomalies: Anomaly[], ): Record { return anomalies.reduce( (grouped, anomaly) => ({ ...grouped, [anomaly.groupKey]: [...(grouped[anomaly.groupKey] ?? []), anomaly], }), {} as Record, ); } }