All files / src/domains/helpdesk/services helpdesk-sla.service.ts

0% Statements 0/70
0% Branches 0/38
0% Functions 0/6
0% Lines 0/63

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140                                                                                                                                                                                                                                                                                       
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { Ticket, TicketSlaInstance, SlaPolicy } from '../schemas';
import { EventBusService } from '../../../platform/events/event-bus.service';
 
@Injectable()
export class HelpdeskSlaService {
  constructor(
    @InjectModel(Ticket.name)
    private readonly ticketModel: Model<Ticket>,
    @InjectModel(TicketSlaInstance.name)
    private readonly slaInstanceModel: Model<TicketSlaInstance>,
    @InjectModel(SlaPolicy.name)
    private readonly policyModel: Model<SlaPolicy>,
    private readonly eventBus: EventBusService
  ) {}
 
  async createPolicy(tenantId: string, data: any): Promise<SlaPolicy> {
    const policy = new this.policyModel({
      ...data,
      tenantId
    });
    return policy.save();
  }
 
  async bindSlaPolicy(tenantId: string, ticketId: string): Promise<TicketSlaInstance> {
    const ticket = await this.ticketModel.findOne({ _id: ticketId, tenantId }).exec();
    if (!ticket) throw new BadRequestException('Ticket not found');
 
    const policy = await this.policyModel.findOne({ tenantId, priorityScope: ticket.priority }).exec();
    const defaultPolicy = policy || { firstResponseMinTarget: 120, resolutionMinTarget: 1440, _id: new Types.ObjectId() };
 
    const firstResponseDueAt = new Date();
    firstResponseDueAt.setMinutes(firstResponseDueAt.getMinutes() + defaultPolicy.firstResponseMinTarget);
 
    const resolutionDueAt = new Date();
    resolutionDueAt.setMinutes(resolutionDueAt.getMinutes() + defaultPolicy.resolutionMinTarget);
 
    const instance = new this.slaInstanceModel({
      tenantId,
      ticketId: ticket._id,
      slaPolicyId: defaultPolicy._id,
      firstResponseDueAt,
      resolutionDueAt,
      firstResponseBreached: false,
      resolutionBreached: false,
      isPaused: false,
      accumulatedPauseMs: 0
    });
 
    ticket.slaPolicyId = defaultPolicy._id.toString();
    ticket.slaDueAt = resolutionDueAt;
    await ticket.save();
 
    return instance.save();
  }
 
  async pauseSla(tenantId: string, ticketId: string): Promise<TicketSlaInstance> {
    const instance = await this.slaInstanceModel.findOne({ ticketId: new Types.ObjectId(ticketId), tenantId }).exec();
    if (!instance) throw new BadRequestException('SLA instance not found');
 
    if (instance.isPaused) return instance;
 
    instance.isPaused = true;
    instance.lastPausedAt = new Date();
    return instance.save();
  }
 
  async resumeSla(tenantId: string, ticketId: string): Promise<TicketSlaInstance> {
    const instance = await this.slaInstanceModel.findOne({ ticketId: new Types.ObjectId(ticketId), tenantId }).exec();
    if (!instance) throw new BadRequestException('SLA instance not found');
 
    if (!instance.isPaused) return instance;
 
    const pauseEnd = new Date();
    const pauseStart = instance.lastPausedAt || pauseEnd;
    const diffMs = pauseEnd.getTime() - pauseStart.getTime();
 
    instance.isPaused = false;
    instance.accumulatedPauseMs += diffMs;
 
    // Shift due dates out by paused duration
    instance.firstResponseDueAt = new Date(instance.firstResponseDueAt.getTime() + diffMs);
    instance.resolutionDueAt = new Date(instance.resolutionDueAt.getTime() + diffMs);
    instance.lastPausedAt = undefined;
    
    await instance.save();
 
    await this.ticketModel.updateOne({ _id: ticketId }, { slaDueAt: instance.resolutionDueAt });
 
    return instance;
  }
 
  async checkBreaches(tenantId: string): Promise<any> {
    const now = new Date();
 
    // Find active SLA instances not yet resolved or breached
    const activeInstances = await this.slaInstanceModel.find({
      tenantId,
      isPaused: false
    }).exec();
 
    const warnings: string[] = [];
    const breaches: string[] = [];
 
    for (const inst of activeInstances) {
      if (!inst.firstResponseBreached && now.getTime() > inst.firstResponseDueAt.getTime()) {
        inst.firstResponseBreached = true;
        await inst.save();
        breaches.push(inst.ticketId.toString());
 
        await this.ticketModel.updateOne({ _id: inst.ticketId }, { slaBreached: true });
 
        await this.eventBus.publish('helpdesk.sla.breached.v1', {
          ticketId: inst.ticketId.toString(),
          tenantId,
          breachType: 'first_response'
        }, tenantId);
      }
 
      if (!inst.resolutionBreached && now.getTime() > inst.resolutionDueAt.getTime()) {
        inst.resolutionBreached = true;
        await inst.save();
        breaches.push(inst.ticketId.toString());
 
        await this.ticketModel.updateOne({ _id: inst.ticketId }, { slaBreached: true });
 
        await this.eventBus.publish('helpdesk.sla.breached.v1', {
          ticketId: inst.ticketId.toString(),
          tenantId,
          breachType: 'resolution'
        }, tenantId);
      }
    }
 
    return { warnings, breaches };
  }
}