Eviworx
Docs

Unified SLA System Architecture

Diese Seite beschreibt den Aufbau des SLA-Systems: wie Trackings entstehen, wie Fristen nach Geschäftszeiten berechnet, pausiert und bei Prioritätswechsel neu gesetzt werden, wie der SLA-Monitor eskaliert und wie Abwesenheiten die Zuweisung beeinflussen.

Funktionen
✓ Ein SLA-System für Ticket, Incident, Problem
✓ Geschäftszeiten und Feiertage (je Land/Region)
✓ Pause bei ON_HOLD, Incident-Link, Sub-Ticket
✓ Neue Fristen bei Prioritätswechsel
✓ Mehrstufige Eskalation mit Wiederholung (repeatConfig)
✓ Manager-Eskalation über die Manager-Kette
✓ Response-SLA je Typ (Antwort bzw. Annahme)
✓ Compliance-Report (met/missed, MTTA/MTTR)
✓ Vertretung bei abwesenden Agents
✓ SLA-Events für Workflows und Benachrichtigungen

System-Übersicht

┌────────────────────────────────────────────────────────────────────────────┐
│                      SLA-SYSTEM ARCHITEKTUR                           │
└────────────────────────────────────────────────────────────────────────────┘

┌──────────────────┐
│   Frontend      │  User creates Ticket/Incident/Problem
│   (React 19+)    │  → POST /api/tickets
└────────┬─────────┘
         │
         ▼
┌──────────────────────────────────────────────────────────────────────────┐
│   Backend (Node.js + Express)                                         │
│                                                                          │
│  ┌────────────────────────────────────────────────────────────────────┐ │
│  │  TicketMutationService                                          │ │
│  │  • createTicket()                                                  │ │
│  │  • Calls: slaTrackingService.createTracking()                     │ │
│  └───────────────────────────┬────────────────────────────────────────┘ │
│                              │                                           │
│                              ▼                                           │
│  ┌────────────────────────────────────────────────────────────────────┐ │
│  │  SLATrackingService                                             │ │
│  │  • findMatchingPolicy(entityType, categoryId)                     │ │
│  │    - Category-specific policy (if categoryId)                     │ │
│  │    - Default policy for entity type                               │ │
│  │  • extractTargets(policy, priority)                               │ │
│  │    - targets.HIGH = { responseMin: 60, resolutionMin: 480 }       │ │
│  │  • Calculate deadlines via BusinessHoursCalculator               │ │
│  │  • INSERT INTO SLATracking                                         │ │
│  └───────────────────────────┬────────────────────────────────────────┘ │
│                              │                                           │
│                              ▼                                           │
│  ┌────────────────────────────────────────────────────────────────────┐ │
│  │  BusinessHoursCalculator                                        │ │
│  │  • calculateDeadline(startTime, businessMinutes, businessHoursId) │ │
│  │  • If 24/7 mode: addMinutes(startTime, businessMinutes)           │ │
│  │  • If business hours:                                              │ │
│  │    - Iterate days, skip weekends/holidays                          │ │
│  │    - Accumulate business minutes until target reached              │ │
│  │  • Returns deadline (Date)                                         │ │
│  └────────────────────────────────────────────────────────────────────┘ │
│                                                                          │
└──────────────────────────────┬───────────────────────────────────────────┘
                               │
                               ▼
┌────────────────────────────────────────────────────────────────────────────┐
│   PostgreSQL Database                                                    │
│                                                                            │
│   SLATracking:                                                             │
│   • id, entityType, entityId                                               │
│   • slaPolicyId, targetResponseMin, targetResolutionMin                   │
│   • responseDeadline, resolutionDeadline                                   │
│   • responseMet, resolutionMet, responseAt, resolvedAt, breachAt           │
│   • isPaused, pausedAt, pausedTotalSec, pauseHistory                       │
│   • currentEscalationLevel, lastEscalationAt, escalationHistory            │
│   • calculatedStatus, calculatedPercentUsed  (Monitor-gepflegt)            │
│   • excludeFromReporting                     (aus Compliance-Quoten raus)  │
│                                                                            │
│   SLAPolicy:                                                               │
│   • id, name, entityType, targets (JSONB)                                  │
│   • businessHoursId, escalationPolicyId                                    │
│   • categoryIds[], pauseOnStatus[]                                         │
│   • pauseOnIncidentLink, pauseOnChildTicket                                │
│   • isDefault, isActive                                                    │
│                                                                            │
│   BusinessHours:                                                           │
│   • id, name, schedule (JSONB), timezone                                   │
│   • excludeHolidays, holidayCountry, holidayRegion, isDefault              │
│                                                                            │
│   Holiday:                                                                 │
│   • id, name, date, isRecurring, country, region                           │
│                                                                            │
│   EscalationPolicy:                                                        │
│   • id, name, levels (JSONB), repeatConfig (JSONB), isActive               │
│                                                                            │
│   UserAbsence:                                                             │
│   • userId, type, startDate, endDate, substituteId                         │
│   • autoReassign, status, approvedBy, approvedAt                           │
└────────────────────────────────────────────────────────────────────────────┘

         │
         ▼
┌────────────────────────────────────────────────────────────────────────────┐
│   Job-Worker (Background Jobs)                                           │
│                                                                            │
│   ┌────────────────────────────────────────────────────────────────────┐  │
│   │  SLAMonitorAction (built-in cron, interval 2 min + runOnStartup) │  │
│   │  • Fetch active SLA trackings (resolutionMet = null)               │  │
│   │  • Calculate current status (OK/WARNING/BREACH/CRITICAL)           │  │
│   │  • Evaluate escalation policy levels                               │  │
│   │  • Execute escalation actions (NOTIFY/REASSIGN/ESCALATE_PRIORITY)  │  │
│   │  • Repeat reminders after the last level (repeatConfig)            │  │
│   │  • Record escalation in database                                   │  │
│   │  • Publish SLA events to notification system                       │  │
│   └────────────────────────────────────────────────────────────────────┘  │
│                                                                            │
└────────────────────────────────────────────────────────────────────────────┘

         │
         ▼
┌────────────────────────────────────────────────────────────────────────────┐
│   Notification System                                                    │
│   • SLANotificationService → DomainEventBus                               │
│   • SLANotificationService listens to SLA events                          │
│   • Routes to NotificationOrchestrator                                     │
│   • Multi-channel: WEB, EMAIL, TEAMS, WEBEX                                │
└────────────────────────────────────────────────────────────────────────────┘

         │
         ▼
┌────────────────────────────────────────────────────────────────────────────┐
│   Audit System                                                           │
│   • All SLA events logged via getAuditService()                            │
│   • SHA-256 hash chain (tamper-evident)                                    │
│   • Actions: SLA_TRACKING_CREATED, SLA_WARNING/SLA_BREACH, SLA_RESOLUTION_MISSED │
└────────────────────────────────────────────────────────────────────────────┘

Domain-Struktur

Das SLA-System besteht aus folgenden Bausteinen:

Services

Service Verantwortlichkeit Aufgaben im Detail
SLATrackingService SLA-Lifecycle Management Trackings anlegen, pausieren und fortsetzen, Response/Resolution als erfüllt markieren, bei Prioritätswechsel zurücksetzen
BusinessHoursCalculator Deadline-Berechnung (Business Hours) Fristen in Geschäftszeiten berechnen, Arbeitstage und Feiertage berücksichtigen
UserAbsenceService Abwesenheits-Lifecycle (Domain /api/absences) CRUD + approve/reject (siehe Absences API)
AvailabilityService Agent-Verfügbarkeit (Abwesenheits-aware) Verfügbarkeit einzelner Agents und Gruppen prüfen, abwesende Agents aus der Auswahl filtern
SLAReportService Historisches Reporting (einzige Compliance-Quelle) Report über Zeitraum und entityType: met/missed/compliancePct, MTTA/MTTR, byEntityType, byPriority; ohne viewAll nur über die für den Agenten sichtbaren Tickets. Abgebrochene Trackings (CANCELLED) und per excludeFromReporting ausgenommene zählen nicht in die Quoten; der Report weist sie getrennt aus (cancelled, excluded)
HolidayAutoImportService Berechnet deutsche Feiertage (inkl. beweglicher via Osterformel) und importiert sie idempotent aufgerufen vom Worker-Job holiday_autoimport
SLANotificationService SLA-Events → Notifications Breach-Warnung/-Critical + Escalation an das Notification-System (Empfänger-Auflösung inkl. CUSTOM, Dedupe je Level/Wiederholung)
AbsenceNotificationService Benachrichtigungen der Abwesenheits-Domain

API-Bereiche

Bereich Beschreibung Beispiel-Endpoints
SLA-API SLA-Dashboard und Report (ohne viewAll nur die für den Agenten sichtbaren Tickets) GET /api/sla/trackings, GET /api/sla/stats, GET /api/sla/report
SLA-Admin-API Admin CRUD für Policies, Business Hours, Holidays POST /api/sla/admin/policies, PATCH /api/sla/admin/business-hours/:id
Absences-API User Absence Management POST /api/absences, POST /api/absences/:id/approve

SLA-Creation Flow (detailliert)

┌─────────────────────────────────────────────────────────────────────────┐
│                   SLA-ERSTELLUNG (DETAILLIERT)                       │
└─────────────────────────────────────────────────────────────────────────┘

1. User creates Ticket/Incident/Problem:

POST /api/tickets
{
  "title": "Database connection timeout",
  "priority": "HIGH",
  "categoryId": "database-issues",
  "description": "..."
}

2. TicketMutationService.createTicket():

// Insert ticket into database
const ticket = await prisma.ticket.create({ ... });

// Create SLA tracking
await slaTrackingService.createTracking({
  entityType: 'TICKET',
  entityId: ticket.id,
  priority: ticket.priority,
  categoryId: ticket.categoryId,
  createdAt: ticket.createdAt
});

3. SLATrackingService.createTracking():

Step 3.1: Find Matching SLA Policy
// Try category-specific policy first
let policy = await prisma.slaPolicy.findFirst({
  where: {
    entityType: 'TICKET',
    categoryIds: { has: categoryId },  // Array contains categoryId
    isActive: true
  }
});

// Fallback to default policy
if (!policy) {
  policy = await prisma.slaPolicy.findFirst({
    where: {
      entityType: 'TICKET',
      isDefault: true,
      isActive: true
    }
  });
}

// Throw error if no policy found
if (!policy) {
  throw new Error('No SLA policy found for entity type TICKET');
}

Step 3.2: Extract Targets for Priority
const targets = policy.targets[priority] || policy.targets.default;
// Example: targets = { responseMin: 60, resolutionMin: 480 }

if (!targets) {
  throw new Error(`No SLA targets for priority ${priority}`);
}

Step 3.3: Calculate Deadlines
const responseDeadline = await businessHoursCalculator.calculateDeadline(
  createdAt,
  targets.responseMin,
  policy.businessHoursId
);

const resolutionDeadline = await businessHoursCalculator.calculateDeadline(
  createdAt,
  targets.resolutionMin,
  policy.businessHoursId
);

Step 3.4: Create SLATracking Record
const tracking = await prisma.sLATracking.create({
  data: {
    entityType: 'TICKET',
    entityId: ticket.id,
    slaPolicyId: policy.id,
    targetResponseMin: targets.responseMin,
    targetResolutionMin: targets.resolutionMin,
    responseDeadline,
    resolutionDeadline,
    responseMet: null,       // pending
    resolutionMet: null,     // pending
    responseAt: null,
    resolvedAt: null,
    breachAt: null,
    currentEscalationLevel: 0,
    lastEscalationAt: null,
    escalationHistory: [],
    isPaused: false,
    pausedAt: null,
    pausedTotalSec: 0,
    pauseHistory: []
  }
});

Step 3.5: Audit Log
await auditService.log({
  domain: 'SLA',
  action: 'SLA_TRACKING_CREATED',
  severity: 'INFO',
  entityType: 'TICKET',
  entityId: ticket.id,
  actorId: currentUser.id,
  metadata: {
    policyId: policy.id,
    policyName: policy.name,
    targetResponseMin: targets.responseMin,
    targetResolutionMin: targets.resolutionMin,
    responseDeadline: responseDeadline.toISOString(),
    resolutionDeadline: resolutionDeadline.toISOString()
  }
});

4. Return Response:
{
  "id": "ticket-uuid",
  "title": "Database connection timeout",
  "priority": "HIGH",
  "sla": {
    "id": "tracking-uuid",
    "policyName": "Standard Support SLA",
    "status": "OK",
    "responseDeadline": "2026-01-28T11:00:00Z",
    "resolutionDeadline": "2026-01-28T17:00:00Z"
  }
}

Business Hours Calculator (Implementation)

Algorithm für Deadline-Berechnung

BUSINESS-HOURS CALCULATOR LOGIC:

async calculateDeadline(
  startTime: Date,
  businessMinutes: number,
  businessHoursId: string | null
): Promise<Date> {

  // 24/7 Mode (no business hours restriction)
  if (!businessHoursId) {
    return addMinutes(startTime, businessMinutes);
  }

  // Business Hours Mode
  const businessHours = await this.getBusinessHours(businessHoursId);
  // Config cached in-memory for 5 min (schedule/timezone per id)

  let currentTime = startTime;
  let remainingMinutes = businessMinutes;

  // Iterate until all business minutes accumulated
  while (remainingMinutes > 0) {
    const dayOfWeek = currentTime.getDay(); // 0 = Sunday, 1 = Monday, ...
    const dayName = ['sunday', 'monday', 'tuesday', ...][dayOfWeek];
    const daySchedule = businessHours.schedule[dayName];

    // Skip non-working days
    if (!daySchedule || !daySchedule.start || !daySchedule.end) {
      currentTime = startOfNextDay(currentTime);
      continue;
    }

    // Check if holiday
    if (businessHours.excludeHolidays) {
      const isHoliday = await this.isHoliday(
        currentTime,
        businessHours.holidayCountry,
        businessHours.holidayRegion
      );
      // Cached in Redis: key "sla:holidays:{country}:{region}:{year}", TTL 24h

      if (isHoliday) {
        currentTime = startOfNextDay(currentTime);
        continue;
      }
    }

    // Parse working hours (in businessHours.timezone)
    const workStartTime = parseTime(daySchedule.start, businessHours.timezone);
    const workEndTime = parseTime(daySchedule.end, businessHours.timezone);

    // If before work start, jump to work start
    if (currentTime < workStartTime) {
      currentTime = workStartTime;
    }

    // If after work end, jump to next day
    if (currentTime >= workEndTime) {
      currentTime = startOfNextDay(currentTime);
      continue;
    }

    // Calculate working minutes available today
    const minutesUntilEndOfDay = differenceInMinutes(workEndTime, currentTime);
    const minutesToAdd = Math.min(remainingMinutes, minutesUntilEndOfDay);

    // Add minutes to current time
    currentTime = addMinutes(currentTime, minutesToAdd);
    remainingMinutes -= minutesToAdd;

    // If still minutes remaining, move to next day
    if (remainingMinutes > 0) {
      currentTime = startOfNextDay(currentTime);
    }
  }

  return currentTime;
}

BEISPIEL (mit Visualisierung):

Input:
• startTime: Friday 14:00
• businessMinutes: 480 (8 hours)
• businessHours: Monday-Friday 09:00-17:00 (8h/day)

Calculation:
┌─────────────────────────────────────────────────┐
│ Friday 14:00 → 17:00 = 180 min (3h) ✅           │
│ Remaining: 480 - 180 = 300 min                  │
├─────────────────────────────────────────────────┤
│ Saturday = SKIP (not in schedule)               │
│ Sunday = SKIP (not in schedule)                 │
├─────────────────────────────────────────────────┤
│ Monday 09:00 → 14:00 = 300 min (5h) ✅           │
│ Remaining: 0 min                                │
└─────────────────────────────────────────────────┘

Result: Monday 14:00 (3 calendar days later!)

Pause/Resume Logic (Implementation)

Drei Pause-Gründe

Die Uhr kann aus drei unabhängigen Gründen stehen: der Entity-Status liegt in policy.pauseOnStatus (Default [ON_HOLD]), die Entity hängt an einem offenen Incident-Link (policy.pauseOnIncidentLink, Default true, nur TICKET), oder ein Ticket hat mindestens ein offenes Sub-Ticket (policy.pauseOnChildTicket, Default true, nur TICKET). Die Gründe können gleichzeitig gelten. Die Uhr läuft erst weiter, wenn keiner von ihnen mehr zutrifft:

shouldRemainPaused(tracking, entityStatus, tx):
   entityStatus ∈ policy.pauseOnStatus                      → true
   policy.pauseOnIncidentLink && TICKET mit offenem Incident-Link  → true
   policy.pauseOnChildTicket && TICKET mit offenem Sub-Ticket      → true
   sonst                                                  → false
  • Beim Verlassen eines Pause-Status wird nur fortgesetzt, wenn wirklich kein Grund mehr greift. Bleibt die Uhr wegen des Incident-Links stehen, nennt die Pause-Historie den Incident-Link als Grund.
  • Dasselbe gilt beim Entfernen eines Incident-Links: Ist die Entity zusätzlich ON_HOLD, bleibt sie pausiert.
  • Kommt ein solcher Grund zu einer bereits pausierten Entity hinzu, wird kein zweites Mal pausiert — der Grund wird nur als Marker in der History dokumentiert.
  • Das Elternticket pausiert, sobald ein Sub-Ticket angelegt oder untergeordnet wird, das noch nicht fertig ist. Wird das Sub-Ticket fertig (gelöst, geschlossen oder als Spam markiert), vom Elternticket gelöst oder gelöscht, prüft das System die Wiederaufnahme — ist ein weiteres Sub-Ticket offen oder steht das Elternticket in einem Pause-Status, bleibt die Uhr stehen. Ein wieder geöffnetes Sub-Ticket pausiert erneut.
  • Der Grund der laufenden Pause wird angezeigt: im SLA-Dashboard und in der Seitenleiste des Ticket-Details, etwa als „Wartet auf INC-000042" oder „Wartet auf Sub-Ticket TK-000456". Der Sub-Ticket-Grund ist eine interne Angabe — er erscheint nur für Agenten mit dem Recht tickets.viewInternal.

Pause SLA

PAUSE LOGIC:

// Triggered when status changes to ON_HOLD
async pauseTracking(trackingId: string): Promise<void> {

  const tracking = await prisma.sLATracking.findUnique({
    where: { id: trackingId }
  });

  if (!tracking || tracking.isPaused) {
    return; // Already paused or not found
  }

  const now = new Date();

  // Update tracking record
  await prisma.sLATracking.update({
    where: { id: trackingId },
    data: {
      isPaused: true,
      pausedAt: now,
      pauseHistory: {
        push: {
          action: 'PAUSE',
          timestamp: now.toISOString(),
          reason: 'Status changed to ON_HOLD'
        }
      }
    }
  });

  // Audit log
  await auditService.log({
    domain: 'SLA',
    action: 'SLA_TRACKING_PAUSED',
    severity: 'INFO',
    entityType: tracking.entityType,
    entityId: tracking.entityId,
    metadata: { pausedAt: now.toISOString() }
  });
}

Resume SLA (mit Deadline-Shift)

RESUME LOGIC:

// Triggered when status changes from ON_HOLD to IN_PROGRESS
async resumeTracking(trackingId: string): Promise<void> {

  const tracking = await prisma.sLATracking.findUnique({
    where: { id: trackingId },
    include: { slaPolicy: { include: { businessHours: true } } }
  });

  if (!tracking || !tracking.isPaused) {
    return; // Not paused
  }

  const now = new Date();
  const pausedAt = tracking.pausedAt!;

  // Calculate pause duration
  const pauseDurationSec = differenceInSeconds(now, pausedAt);

  // Shift deadlines
  let newResponseDeadline = tracking.responseDeadline;
  let newResolutionDeadline = tracking.resolutionDeadline;

  if (tracking.slaPolicy.businessHoursId) {
    // Business Hours Mode: Shift by business minutes only
    const businessMinutes = await businessHoursCalculator.calculateBusinessMinutes(
      pausedAt,
      now,
      tracking.slaPolicy.businessHoursId
    );

    newResponseDeadline = await businessHoursCalculator.calculateDeadline(
      tracking.responseDeadline,
      businessMinutes,
      tracking.slaPolicy.businessHoursId
    );

    newResolutionDeadline = await businessHoursCalculator.calculateDeadline(
      tracking.resolutionDeadline,
      businessMinutes,
      tracking.slaPolicy.businessHoursId
    );
  } else {
    // 24/7 Mode: Shift by calendar minutes
    newResponseDeadline = addSeconds(tracking.responseDeadline, pauseDurationSec);
    newResolutionDeadline = addSeconds(tracking.resolutionDeadline, pauseDurationSec);
  }

  // Update tracking record
  await prisma.sLATracking.update({
    where: { id: trackingId },
    data: {
      isPaused: false,
      pausedAt: null,
      pausedTotalSec: tracking.pausedTotalSec + pauseDurationSec,
      responseDeadline: newResponseDeadline,
      resolutionDeadline: newResolutionDeadline,
      pauseHistory: {
        push: {
          action: 'RESUME',
          timestamp: now.toISOString(),
          pauseDurationSec,
          oldDeadline: tracking.resolutionDeadline.toISOString(),
          newDeadline: newResolutionDeadline.toISOString()
        }
      }
    }
  });

  // Audit log
  await auditService.log({
    domain: 'SLA',
    action: 'SLA_TRACKING_RESUMED',
    severity: 'INFO',
    entityType: tracking.entityType,
    entityId: tracking.entityId,
    metadata: {
      pauseDurationSec,
      deadlineShiftSec: differenceInSeconds(newResolutionDeadline, tracking.resolutionDeadline)
    }
  });
}

BEISPIEL:

Initial Deadline: Monday 14:00
Paused: Monday 11:00
Resumed: Tuesday 10:00 (24 hours later)

24/7 Mode:
  → Shift by 24 hours (1440 minutes)
  → New Deadline: Tuesday 14:00

Business Hours Mode (09:00-17:00):
  → Pause duration = 24 calendar hours
  → Business minutes = 7 hours (Monday 11:00-17:00 + Tuesday 09:00-10:00)
  → Shift by 7 business hours
  → New Deadline: Tuesday 14:00 + 7h = Wednesday 13:00

→ Business Hours Mode is FAIRER for the customer!

SLA-Verhalten bei Reopen

Wird eine Entität wiederöffnet, wird das SLA-Verhalten je Entitätstyp in der Lifecycle-Konfiguration festgelegt: slaOnReopenFromResolved (Default CONTINUE) und slaOnReopenFromClosed (Default RESTART).

ModusWirkung
CONTINUESoft-Reset: dieselbe SLA-Periode läuft weiter, nur der Resolution-Status (resolutionMet) wird genullt — KEINE neuen Deadlines/Timer.
RESTARTHard-Reset: frischer SLA-Lifecycle (neue Deadlines, Eskalations-Level 0, Breach-Status gelöscht).

Die Defaults spiegeln ITIL: Reopen aus RESOLVED wartete nur auf Kunden-Bestätigung → CONTINUE; Reopen aus CLOSED/SPAM ist faktisch ein neuer Vorgang → RESTART. Fehlt beim Reopen aus CLOSED/SPAM eine aktive Kategorie-/Default-SLA-Policy, läuft die Entität ohne SLA-Tracking weiter (per WARN-Audit dokumentiert). Governance-Details: Reopen & Lifecycle.

Response-SLA: was als Reaktion zählt

Die Response-SLA misst, wie schnell der Kunde eine echte Reaktion sieht — nicht, wie schnell intern ein Name am Vorgang steht. Deshalb ist die Semantik je Entity-Typ unterschiedlich:

Entity responseMet wird gesetzt durch
TICKET die erste ÖFFENTLICHE Antwort eines Agenten: Web-Nachricht (nicht intern, nicht vom Kunden), Agenten-Antwort per E-Mail in den Thread, oder die ausgehende E-Mail, mit der ein Agent das Ticket überhaupt erst anlegt. Eine Zuweisung — an Agent, Gruppe, Mailbox oder Queue — zählt NICHT.
INCIDENT / PROBLEM die Zuweisung bzw. Annahme (Acknowledge) — ITIL-Semantik, hier ist das Übernehmen die Reaktion.

Folge im Betrieb: Ein E-Mail-Ticket, das automatisch in die Default-Gruppe einer Mailbox läuft, ist erst mit der ersten öffentlichen Agenten-Antwort beantwortet. Die Response-Stufen einer Eskalations-Policy greifen also auch hier. Wird ein Ticket wiederhergestellt, prüft das System, ob bereits eine öffentliche Agenten-Antwort vorliegt.

Priority Reset Logic

Wenn die Priority eines Entity erhöht wird (z.B. LOW → HIGH oder HIGH → CRITICAL):

PRIORITY RESET LOGIC:

// Triggered when priority changes
async resetTracking(trackingId: string, newPriority: string): Promise<void> {

  const tracking = await prisma.sLATracking.findUnique({
    where: { id: trackingId },
    include: { slaPolicy: true }
  });

  if (!tracking) {
    throw new Error('SLA tracking not found');
  }

  // Extract new targets for new priority
  const newTargets = tracking.slaPolicy.targets[newPriority]
    || tracking.slaPolicy.targets.default;

  if (!newTargets) {
    throw new Error(`No SLA targets for priority ${newPriority}`);
  }

  const now = new Date();

  // Calculate NEW deadlines from NOW (fresh start)
  const newResponseDeadline = await businessHoursCalculator.calculateDeadline(
    now,
    newTargets.responseMin,
    tracking.slaPolicy.businessHoursId
  );

  const newResolutionDeadline = await businessHoursCalculator.calculateDeadline(
    now,
    newTargets.resolutionMin,
    tracking.slaPolicy.businessHoursId
  );

  // Reset escalation level
  const resetEscalationLevel = 0;

  // Update tracking record
  await prisma.sLATracking.update({
    where: { id: trackingId },
    data: {
      targetResponseMin: newTargets.responseMin,
      targetResolutionMin: newTargets.resolutionMin,
      responseDeadline: newResponseDeadline,
      resolutionDeadline: newResolutionDeadline,
      currentEscalationLevel: resetEscalationLevel,
      breachAt: null,  // Clear breach if any
      escalationHistory: {
        push: {
          action: 'RESET',
          timestamp: now.toISOString(),
          reason: `Priority changed to ${newPriority}`,
          oldTargets: {
            responseMin: tracking.targetResponseMin,
            resolutionMin: tracking.targetResolutionMin
          },
          newTargets: {
            responseMin: newTargets.responseMin,
            resolutionMin: newTargets.resolutionMin
          },
          oldDeadline: tracking.resolutionDeadline.toISOString(),
          newDeadline: newResolutionDeadline.toISOString()
        }
      }
    }
  });

  // Audit log
  await auditService.log({
    domain: 'SLA',
    action: 'SLA_RECALCULATED_FOR_PRIORITY',
    severity: 'WARNING',
    entityType: tracking.entityType,
    entityId: tracking.entityId,
    metadata: {
      newPriority,
      oldTargetResolutionMin: tracking.targetResolutionMin,
      newTargetResolutionMin: newTargets.resolutionMin,
      oldDeadline: tracking.resolutionDeadline.toISOString(),
      newDeadline: newResolutionDeadline.toISOString()
    }
  });
}

WARUM RESET?

Szenario:
• Ticket created: Monday 09:00, Priority LOW
• Target: 2880 minutes (48 hours)
• Deadline: Wednesday 09:00
• Elapsed: 24 hours (50% used, status WARNING)

Priority escalated to CRITICAL:
• New Target: 120 minutes (2 hours)
• NEW Deadline: Monday 11:00 (from NOW, not inherited)
• Escalation Level: 0 (reset)

→ Prevents inherited "almost breached" status from the old priority!
→ Gives the team a fair chance with stricter targets!

Weil ein Prioritätswechsel die volle Frist neu startet, zeigt die Ticket-Timeline den Wechsel mit alter und neuer Resolution-Deadline. Im Audit-Log stehen beide Deadlines für alle Entitätstypen. Bei Incidents ergibt sich die Priorität aus der Matrix. Auch die Policy-UI weist darauf hin, dass ein Prioritätswechsel die Zielzeiten ab dem Wechselzeitpunkt neu setzt. Pausieren, Fortsetzen, Abbrechen und Eskalationen des SLA erscheinen ebenfalls in der Timeline von Ticket, Incident und Problem; der Text wird in der Sprache des Betrachters angezeigt.

Escalation System (Detailed Flow)

┌─────────────────────────────────────────────────────────────────────────┐
│              SLA ESCALATION WORKFLOW                              │
└─────────────────────────────────────────────────────────────────────────┘

1. SLA Monitor (Background Job) runs every 2 minutes (built-in template):

┌──────────────────────────────────────────────────────────────────────┐
│  job-worker: SLAMonitorAction                                        │
└──────────────────────────────────────────────────────────────────────┘

Step 1.1: Fetch Active SLA Trackings
const trackings = await loadActiveTrackings(); // resolutionMet = null

Returns all SLAs not yet resolved (active)

Step 1.2: Calculate Current Status for Each
For each tracking:

// Calculate elapsed time (excluding pause time)
const elapsed = differenceInMinutes(NOW, tracking.createdAt);
const elapsedNet = elapsed - (tracking.pausedTotalSec / 60);

// For business hours policies
if (tracking.slaPolicy.businessHoursId) {
  elapsedNet = await businessHoursCalculator.calculateBusinessMinutes(
    tracking.createdAt,
    NOW,
    tracking.slaPolicy.businessHoursId
  ) - (tracking.pausedTotalSec / 60);
}

// Calculate percent used
const percentUsed = (elapsedNet / tracking.targetResolutionMin) * 100;

// Determine status
let status = 'OK';
if (percentUsed >= 100) {
  const minutesAfterBreach = elapsedNet - tracking.targetResolutionMin;
  status = minutesAfterBreach >= 60 ? 'CRITICAL' : 'BREACH';
} else if (percentUsed >= 80) {
  status = 'WARNING';
}

Step 1.3: Evaluate Escalation Policy Levels
const escalationPolicy = tracking.slaPolicy.escalationPolicy;

if (!escalationPolicy || !escalationPolicy.isActive) {
  continue; // No escalation policy
}

for (const level of escalationPolicy.levels) {
  // Skip if already escalated to this level
  if (tracking.currentEscalationLevel >= level.level) {
    continue;
  }

  // Check if trigger condition met
  let triggered = false;

  switch (level.triggerType) {
    case 'PERCENTAGE':
      triggered = percentUsed >= level.triggerValue;
      break;

    case 'BREACH':
      triggered = percentUsed >= 100;
      break;

    case 'TIME_AFTER_BREACH':
      if (tracking.breachAt) {
        const minutesAfterBreach = differenceInMinutes(NOW, tracking.breachAt);
        triggered = minutesAfterBreach >= level.triggerValue;
      }
      break;
  }

  if (!triggered) {
    continue;
  }

  // TRIGGER ESCALATION!
  await executeEscalation(tracking, level, status, percentUsed);
}

Step 1.4: Execute Escalation Actions
async function executeEscalation(tracking, level, status, percentUsed) {

  const actionResults = [];

  for (const action of level.actions) {
    try {
      switch (action.type) {
        case 'NOTIFY':
          await executeNotifyAction(tracking, action, status);
          actionResults.push({ type: 'NOTIFY', success: true });
          break;

        case 'REASSIGN':
          await executeReassignAction(tracking, action);
          actionResults.push({ type: 'REASSIGN', success: true });
          break;

        case 'ESCALATE_PRIORITY':
          await executeEscalatePriorityAction(tracking);
          actionResults.push({ type: 'ESCALATE_PRIORITY', success: true });
          break;
      }
    } catch (error) {
      actionResults.push({ type: action.type, success: false, error: error.message });
    }
  }

  // Record escalation in database
  await recordEscalation(tracking.id, {
    level: level.level,
    trigger: level.triggerType,
    percentUsed,
    status,
    actions: actionResults,
    timestamp: NOW
  });

  // Audit log (status-based action: SLA_WARNING | SLA_BREACH | SLA_CRITICAL)
  await auditService.log({
    domain: 'SLA',
    action: status === 'CRITICAL' ? 'SLA_CRITICAL' : (status === 'BREACH' ? 'SLA_BREACH' : 'SLA_WARNING'),
    severity: status === 'CRITICAL' ? 'CRITICAL' : 'WARNING',
    entityType: tracking.entityType,
    entityId: tracking.entityId,
    metadata: {
      escalationLevel: level.level,
      triggerType: level.triggerType,
      percentUsed,
      status,
      actions: actionResults
    }
  });
}

2. Execute NOTIFY Action:

async function executeNotifyAction(tracking, action, status) {

  // Determine recipients based on notifyTargets
  const recipients = [];

  if (action.notifyTargets.includes('ASSIGNEE')) {
    recipients.push(tracking.entity.assignedToId);
  }

  if (action.notifyTargets.includes('GROUP_LEAD')) {
    recipients.push(...await getGroupLeads(tracking.entity.assignedGroupId));
  }

  if (action.notifyTargets.includes('MANAGER')) {
    // Walks up the manager chain; see "Manager escalation" below
    recipients.push(...await getEscalationChain(tracking.entity.assignedToId));
  }

  if (action.notifyTargets.includes('CUSTOM')) {
    // Explicit user picker in the policy builder
    recipients.push(...action.customUserIds);
  }

  // Publish SLA event → SLANotificationService
  {
    type: status === 'CRITICAL' || status === 'BREACH' ? 'SLA_BREACH' : 'SLA_WARNING',
    trackingId: tracking.id,
    entityType: tracking.entityType,
    entityId: tracking.entityId,
    recipientIds: recipients,
    // NO channels here — the notification type config + user
    // preferences decide the delivery route (in-app/email/…)
    metadata: { percentUsed, deadline: tracking.resolutionDeadline.toISOString() }
  }

  // Recipients are filtered against entity visibility at runtime;
  // dedupe key includes level (and repetition for repeats)
}

3. Execute REASSIGN Action:

async function executeReassignAction(tracking, action) {

  // Target may be a group, a user, or both
  await assignmentService.reassign({
    entityType: tracking.entityType,
    entityId: tracking.entityId,
    groupId: action.reassignToGroupId,   // optional
    userId: action.reassignToUserId,     // optional (must be an active agent)
    reason: 'SLA_ESCALATION'
  });

  // Runs through AssignmentService → Activity, Audit and workload
  // handling identical to a manual reassignment. If both are set,
  // the group is assigned first, then the user.
  // Afterwards fresh entity details are loaded for the notification.
}

4. Execute ESCALATE_PRIORITY Action:

async function executeEscalatePriorityAction(tracking) {

  // Determine new priority (one level up)
  const priorityMap = {
    'LOW': 'MEDIUM',
    'MEDIUM': 'HIGH',
    'HIGH': 'CRITICAL',
    'CRITICAL': 'CRITICAL'  // Already at top
  };

  const currentPriority = tracking.entity.priority;
  const newPriority = priorityMap[currentPriority];

  if (newPriority === currentPriority) {
    return; // Already at top priority
  }

  // Update entity priority
  await escalatePriority({
    entityType: tracking.entityType,
    entityId: tracking.entityId,
    newPriority
  });

  // This triggers SLA reset internally:
  // → New targets for new priority
  // → New deadlines calculated from NOW
  // → Escalation level reset to 0
  // → Fresh start with stricter targets
}

Wiederholende Erinnerungen nach der letzten Stufe

Jede Eskalationsstufe wird genau einmal ausgelöst (currentEscalationLevel steigt je Stufe um eins). Für dauerhaft verletzte SLAs gibt es an der Eskalations-Policy zusätzlich eine Wiederholung (repeatConfig), die nach der letzten Stufe greift:

REPEAT-BEDINGUNGEN (alle müssen gelten):

  repeatConfig.enabled                              Wiederholung eingeschaltet  tracking.breachAt != null                         bereits verletzt  tracking.resolutionMet == null                    noch offen  currentEscalationLevel >= letztes Level            Leiter abgearbeitet  repeatsDone < repeatConfig.maxRepeats             Kontingent übrig  repeatsDone < ⌊businessMinutesAfterBreach / intervalMin⌋

Zähler: repeatsDone = Anzahl der History-Einträge mit triggerType REPEAT.Wirkung: NOTIFY an repeatConfig.notifyTargets, Payload wie die höchste Stufe; History-Eintrag { triggerType: "REPEAT", repetition: n }; lastEscalationAt wird gesetzt; der Dedupe-Key trägt :r&lt;n&gt;, sodass jede Wiederholung einzeln zugestellt wird.

Manager-Eskalation & automatischer Ticket-Zugriff

Beim NOTIFY-Target MANAGER läuft die Benachrichtigung die Manager-Eskalationskette nach oben. Sieht ein Manager das Ticket ohnehin, wird er benachrichtigt. Sieht er es NICHT, greift — nur für TICKET und nur wenn das General-Setting autoGrantManagerAccess aktiv ist (Default AUS, opt-in) — der automatische Zugriff:

  • Der nicht-sichtbare Manager wird als stummer FOLLOWER-Participant aufs Ticket gesetzt, damit der Deep-Link in der Benachrichtigung aufgeht.
  • Bleibt das Ticket trotz Follower unsichtbar (z.B. restricted Mailbox/Gruppe), wird der Follower wieder entfernt und der nächste Manager in der Kette geprüft.
  • Ist das Setting AUS, bleibt es beim reinen Filter-Verhalten (nur ohnehin sichtbare Manager werden benachrichtigt). Änderungen am Setting wirken nach spätestens 60 Sekunden. Tritt bei der Prüfung ein Fehler auf, wird kein Zugriff gewährt.

User Absence System

Abwesenheiten sind eine eigene Domain (/api/absences) mit eigener Doku. Hier steht nur, wie sie auf SLA und Zuweisung wirken: Abwesende Agents werden bei jeder Zuweisung übergangen, bereits zugewiesene Vorgänge bleiben unverändert.

🌴 Vollständige Abwesenheits-API (Typen, Status, Genehmigung, Substitute): Absences API →.

SLA-/Assignment-Integration

  • UserAbsence: type (AbsenceType), startDate/endDate, allDay (+startTime/endTime), status (AbsenceStatus, Default APPROVED — Agents tragen sich meist selbst ein), substituteId?, approvedById/approvedAt. Anlegen über /api/absences.
  • AvailabilityService: isAgentAvailable(agentId), filterAvailableAgents(ids), getAvailableAgentsInGroup(groupId), getAgentAvailabilitySummary(). Abwesende Agents werden bei der Zuweisung herausgefiltert; ein gesetzter substitute wird zur Laufzeit als Empfänger umgeleitet (kein nachträgliches Umhängen bestehender Tickets).
  • UserAbsenceService / AbsenceNotificationService: Lifecycle + Benachrichtigungen der Abwesenheits-Domain.

Event-Driven Architecture

Domain-Events für SLA

Event-Type Trigger Handler
SLA_WARNING Warnschwelle erreicht (Standard 80 %) SLANotificationService → Benachrichtigung über mehrere Kanäle
SLA_BREACH SLA verletzt (100 %+) SLANotificationService → Benachrichtigung über mehrere Kanäle (isCritical, isEnforced)
SLA_ESCALATION Eskalationsstufe ausgelöst SLANotificationService → Eskalations-Empfänger benachrichtigen (isCritical)
ENTITY_STATUS_CHANGED Statuswechsel an Ticket/Incident SLATrackingService → Tracking pausieren/fortsetzen
ENTITY_PRIORITY_CHANGED Priorität angehoben SLATrackingService → Tracking mit neuen Targets zurücksetzen
ABSENCE_APPROVED Abwesenheit genehmigt/aktiv AvailabilityService → Agent gilt als nicht verfügbar und wird bei neuen Zuweisungen übergangen

Event-Flow Visualisierung

┌─────────────────────────────────────────────────────────────────────┐
│                    EVENT-DRIVEN FLOW                            │
└─────────────────────────────────────────────────────────────────────┘

┌──────────────────┐
│  SLA Monitor     │  Detects: SLA at 85% used
│  (Job-Worker)    │
└────────┬─────────┘
         │
         │ Publishes event
         ▼
┌──────────────────────────────────────┐
│  DomainEventBus                    │
│  Event: SLA_WARNING                  │
│  Data: {                             │
│    trackingId, entityId, percentUsed │
│  }                                   │
└────────┬─────────────────────────────┘
         │
         │ Routes to subscribers
         ├────────────────────────┬────────────────────┐
         ▼                        ▼                    ▼
┌──────────────────┐   ┌──────────────────┐   ┌──────────────┐
│ Notification     │   │ Webhook          │   │ Audit        │
│ Adapter          │   │ Dispatcher       │   │ Service      │
└────────┬─────────┘   └────────┬─────────┘   └────────┬─────┘
         │                      │                      │
         ▼                      ▼                      ▼
┌──────────────────┐   ┌──────────────────┐   ┌──────────────┐
│ Multi-Channel    │   │ External System  │   │ Audit Event  │
│ Notification:    │   │ via Webhook      │   │ (SHA-256)    │
│ • WEB            │   │                  │   │              │
│ • EMAIL          │   │                  │   │              │
│ • TEAMS          │   │                  │   │              │
│ • WEBEX          │   │                  │   │              │
└──────────────────┘   └──────────────────┘   └──────────────┘

Performance & Scalability

Caching-Strategie

Zwischengespeichert wird, was in der Deadline-Berechnung häufig abgefragt wird: Feiertagslisten und Business-Hours-Konfiguration. Policies und Trackings werden bei jeder Berechnung aktuell gelesen.

Cache Ebene / TTL Zweck
sla:holidays:{country}:{region} Redis, 1 Stunde Feiertagsliste je Land/Region — instanzübergreifend geteilt, wird bei jeder Feiertags-Änderung invalidiert
Lokaler Feiertags-Cache In-Memory, 5 Minuten je Key Fast-Path vor Redis (Deadline-Berechnung fragt pro Iterationstag)
BusinessHours-Config-Cache In-Memory, 5 Minuten Schedule/Timezone je businessHoursId, spart wiederholte DB-Lookups

Kaskade: lokaler Cache → Redis → Datenbank.

Database-Optimierungen

-- Performance-kritische Indices
CREATE INDEX idx_sla_tracking_entity ON "SLATracking" (entityType, entityId);
CREATE INDEX idx_sla_tracking_deadline ON "SLATracking" (resolutionDeadline)
  WHERE resolutionMet IS NULL;
CREATE INDEX idx_sla_tracking_paused ON "SLATracking" (isPaused)
  WHERE isPaused = true;
CREATE INDEX idx_sla_tracking_breach ON "SLATracking" (breachAt)
  WHERE breachAt IS NOT NULL;
CREATE INDEX idx_sla_tracking_policy ON "SLATracking" (slaPolicyId);

-- SLA Policy Indices
CREATE INDEX idx_sla_policy_entity ON "SLAPolicy" (entityType, isActive);
CREATE INDEX idx_sla_policy_default ON "SLAPolicy" (entityType, isDefault)
  WHERE isDefault = true;
CREATE INDEX idx_sla_policy_category ON "SLAPolicy" USING GIN (categoryIds);

-- Business Hours Index
CREATE INDEX idx_business_hours_default ON "BusinessHours" (isDefault)
  WHERE isDefault = true;

-- Holiday Indices
CREATE INDEX idx_holiday_date ON "Holiday" (date);
CREATE INDEX idx_holiday_country ON "Holiday" (country, region, date);

-- User Absence Indices
CREATE INDEX idx_absence_user ON "UserAbsence" (userId, status);
CREATE INDEX idx_absence_dates ON "UserAbsence" (startDate, endDate)
  WHERE status = 'APPROVED';

Scalability Considerations

  • Distributed Locks: SLA-Monitor verwendet Redis-Locks, damit er nicht doppelt läuft. Bricht ein Worker mitten im Lauf ab, gibt der Scheduler hängengebliebene Jobs beim nächsten Start wieder frei, und der Monitor läuft weiter.
  • Background Job Interval: 2 Minuten (Built-in-Template, in der CronJobs-Verwaltung anpassbar)
  • Batch Processing: Der Monitor liest fällige Trackings seitenweise zu je 500 und arbeitet so den gesamten fälligen Bestand ab (höchstens 20 Seiten pro Lauf)
  • Event Queue: Domain-Events über eine Redis-Queue (asynchron)
  • Multi-Instance Support: Der Job-Worker skaliert horizontal (Abstimmung über Redis-Locks)

Security & Compliance

Audit-Logging

Alle SLA-Operationen werden im Enterprise Audit System geloggt:

Action Severity Metadata
SLA_TRACKING_CREATED INFO policyId, policyName, targetMin, deadlines
SLA_TRACKING_PAUSED INFO pausedAt, reason
SLA_TRACKING_RESUMED INFO pauseDurationSec, deadlineShiftSec
SLA_RECALCULATED_FOR_PRIORITY WARNING newPriority, oldTargets, newTargets, deadlineChange
SLA_RESOLUTION_RESET INFO Soft-Reset beim Reopen (CONTINUE): resolutionMet zurückgesetzt
SLA_TRACKING_RECREATED / SLA_TRACKING_MISSING_POLICY INFO / WARNING Hard-Reset beim Reopen (RESTART) bzw. Reopen ohne passende Policy → Entity läuft ohne Tracking weiter
SLA_CANCELLED INFO Tracking storniert (Entity gelöscht/irrelevant)
SLA_RESPONSE_MET INFO responseAt, deadline, timeToResponse
SLA_RESPONSE_MISSED WARNING responseAt, deadline, breachMinutes
SLA_RESOLUTION_MET INFO resolvedAt, deadline, timeToResolution
SLA_RESOLUTION_MISSED CRITICAL resolvedAt, deadline, breachMinutes
SLA_WARNING / SLA_BREACH / SLA_CRITICAL WARNING/CRITICAL escalationLevel, triggerType, percentUsed, actions

🔒 Compliance: Alle SLA-Events werden in einer SHA-256-Hash-Kette gesichert, sodass nachträgliche Änderungen erkennbar sind. Das unterstützt Audits nach ISO 27001, SOC 2 und DSGVO. Siehe Enterprise Audit System.

Integration mit ITSM-Core

Entity-Service Integration

Entity-Service Imports SLA-Service Integration-Points
TicketMutationService ✅ slaTrackingService createTicket(), updateStatus(), updatePriority(), closeTicket(), addComment()
IncidentMutationService ✅ slaTrackingService createIncident(), updateStatus(), updatePriority(), closeIncident()
ProblemMutationService ✅ slaTrackingService createProblem(), updateStatus(), updatePriority(), closeProblem()
Zuweisungs-Logik ✅ availabilityService isAgentAvailable(), filterAvailableAgents(), getAvailableAgentsInGroup()
NotificationOrchestrator ✅ slaNotificationService Route SLA events to multi-channel notifications

Response-Embedding

Entity-GET-Responses enthalten immer SLA-Informationen:

// Ticket GET Response
GET /api/tickets/:id

{
  "id": "ticket-uuid",
  "title": "Database connection timeout",
  "priority": "HIGH",
  "status": "IN_PROGRESS",
  "createdAt": "2026-01-28T09:00:00Z",

  // Embedded SLA info
  "sla": {
    "id": "tracking-uuid",
    "policyName": "Standard Support SLA",
    "status": "WARNING",         // OK, WARNING, BREACH, CRITICAL
    "percentUsed": 85.5,
    "timeRemaining": "1h 23m",
    "responseDeadline": "2026-01-28T10:00:00Z",
    "resolutionDeadline": "2026-01-28T17:00:00Z",
    "responseMet": true,
    "resolutionMet": null,       // pending
    "isPaused": false,
    "currentEscalationLevel": 1
  }
}

Deployment & Configuration

Konfiguration

Der SLA-Monitor läuft als Cronjob im Job-Worker (Built-in-Template „SLA Monitor“, Intervall ca. 2 min, runOnStartup) und wird in der CronJobs-Verwaltung eingestellt. Die Schwellwerte für WARNING und CRITICAL sind ein Setting (sla-settings), siehe SLA Management API → Schwellwerte konfigurieren.

  • Holiday-Cache in Redis unter Prefix sla:holidays:{country}:{region} (+ lokaler In-Memory-Cache)

Generische Worker-/Redis-Konfiguration (REDIS_URL etc.) siehe Deployment-Seiten.

Initial Setup

# 1. Create default SLA policies via Admin UI or API
POST /api/sla/admin/policies
{
  "name": "Standard Support SLA",
  "entityType": "TICKET",
  "targets": { ... },
  "isDefault": true
}

# 2. Configure business hours
POST /api/sla/admin/business-hours
{
  "name": "German Business Hours",
  "schedule": { ... },
  "timezone": "Europe/Berlin",
  "isDefault": true
}

# 3. Import holidays
POST /api/sla/admin/holidays/bulk
{
  "holidays": [
    { "name": "New Year", "date": "2026-01-01", "isRecurring": true }
  ]
}

# 4. Create escalation policy
POST /api/sla/admin/escalation-policies
{
  "name": "Standard Escalation",
  "levels": [ ... ]
}

# 5. SLA monitor: no setup needed — the built-in job template
#    "SLA Monitor" ships with the system (interval trigger, 2 min,
#    runOnStartup, batchSize 500 = page size; the run pages through the
#    whole due backlog). Enable/adjust it in the CronJobs administration
#    instead of creating your own job.

Best Practices

  1. Business Hours für B2B: Business Hours für realistische SLA-Targets verwenden (nicht 24/7 für Standard-Support)
  2. 24/7 für Premium: Premium-Support sollte 24/7-SLAs verwenden (businessHoursId = null)
  3. Category-Specific Policies: Separate Policies für kritische Kategorien anlegen (z.B. "Production Issues")
  4. Multi-Level Escalation: Mindestens 3 Stufen (80 % Warning, Breach → Reassign, Zeit nach Breach → Prio anheben); für Dauer-Breaches zusätzlich eine Wiederholung (repeatConfig) statt einer endlosen Level-Leiter
  5. Pause bei ON_HOLD: pauseOnStatus = ["ON_HOLD"] verwenden, damit Wartezeiten auf den Kunden nicht zählen; pauseOnIncidentLink und pauseOnChildTicket aktiviert lassen, damit Tickets keine Frist verbrauchen, solange die Ursache in einem Incident oder die Zuarbeit in einem Sub-Ticket bearbeitet wird
  6. Kategorie-Policies überschneidungsfrei: Eine Kategorie darf nur einer aktiven Policy je entityType gehören — das Backend lehnt Überschneidungen mit 409 ab
  7. Priority Reset: SLA bei Priority-Change resetten (neue Targets = neuer Start)
  8. Absence Management: Vertretung (substituteId) pflegen — abwesende Agents fallen aus der Zuweisungs-Auswahl, bestehende Vorgänge werden NICHT automatisch umgehängt
  9. Compliance messen: Quoten immer aus /sla/report (historisch) ziehen, nicht aus /sla/stats (Momentaufnahme); Ausreißer über excludeFromReporting aus den Quoten nehmen
  10. Audit-Retention: Aufbewahrungsfrist der SLA-Events an den eigenen Audit- und Nachweispflichten ausrichten
  11. Kanäle zentral steuern: Zustellwege gehören in die Notification-Typ-Konfiguration und die Nutzer-Präferenzen, nicht in die Eskalations-Policy

Verwandte Dokumentation