Adds GET /admin/sla-runs (filterable by status), GET /admin/escalation- events (capped, most-recent-first), and GET /admin/products (with integration status joined in, never the full ProductIntegration row). None of these existed as a single query before - only per-ticket or per-integration-id lookups did. Discovered while planning supporthub-web's 001-agent-admin-ui User Stories 6-7 (SLA/escalation monitoring, product catalog), the same way 011-agent-ticket-queue was discovered for User Story 1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
80 lines
3.0 KiB
TypeScript
80 lines
3.0 KiB
TypeScript
import { FastifyReply, FastifyRequest } from 'fastify';
|
|
import { escalationService, EscalationService } from '../service';
|
|
import {
|
|
createEscalationPolicySchema,
|
|
createEscalationRuleSchema,
|
|
updateEscalationRuleSchema,
|
|
manualEscalationSchema,
|
|
listRecentEventsQuerySchema,
|
|
} from '../schema';
|
|
|
|
function actorFrom(request: FastifyRequest): string {
|
|
return request.reqContext?.actorId ?? 'unknown';
|
|
}
|
|
|
|
export class EscalationController {
|
|
constructor(private readonly service: EscalationService = escalationService) {}
|
|
|
|
async createPolicy(request: FastifyRequest, reply: FastifyReply) {
|
|
const body = createEscalationPolicySchema.parse(request.body);
|
|
const policy = await this.service.createPolicy(body);
|
|
return reply.status(201).send({ success: true, data: policy, meta: null });
|
|
}
|
|
|
|
async listPolicies(_request: FastifyRequest, reply: FastifyReply) {
|
|
const policies = await this.service.listPolicies();
|
|
return reply.status(200).send({ success: true, data: policies, meta: null });
|
|
}
|
|
|
|
async createRule(request: FastifyRequest, reply: FastifyReply) {
|
|
const { id } = request.params as { id: string };
|
|
const body = createEscalationRuleSchema.parse(request.body);
|
|
const rule = await this.service.createRule(id, body);
|
|
return reply.status(201).send({ success: true, data: rule, meta: null });
|
|
}
|
|
|
|
async updateRule(request: FastifyRequest, reply: FastifyReply) {
|
|
const { ruleId } = request.params as { id: string; ruleId: string };
|
|
const body = updateEscalationRuleSchema.parse(request.body);
|
|
const rule = await this.service.updateRule(ruleId, body);
|
|
return reply.status(200).send({ success: true, data: rule, meta: null });
|
|
}
|
|
|
|
async deleteRule(request: FastifyRequest, reply: FastifyReply) {
|
|
const { ruleId } = request.params as { id: string; ruleId: string };
|
|
await this.service.deactivateRule(ruleId);
|
|
return reply.status(204).send();
|
|
}
|
|
|
|
/** 012-admin-list-views: recent escalation events across every ticket, for the monitoring
|
|
* view. */
|
|
async listRecentEvents(request: FastifyRequest, reply: FastifyReply) {
|
|
const { limit } = listRecentEventsQuerySchema.parse(request.query);
|
|
const events = await this.service.listRecentEvents(limit);
|
|
const data = events.map((event) => ({
|
|
ticketId: event.ticketId,
|
|
ticketCode: event.ticket.code,
|
|
reason: event.reason,
|
|
ruleId: event.ruleId,
|
|
triggeredBy: event.triggeredBy,
|
|
toNodeId: event.toNodeId,
|
|
createdAt: event.createdAt,
|
|
}));
|
|
return reply.status(200).send({ success: true, data, meta: null });
|
|
}
|
|
|
|
async escalateManually(request: FastifyRequest, reply: FastifyReply) {
|
|
const { ticketId } = request.params as { ticketId: string };
|
|
const body = manualEscalationSchema.parse(request.body);
|
|
const event = await this.service.escalateManually(
|
|
ticketId,
|
|
body.targetNodeId,
|
|
actorFrom(request),
|
|
body.reason,
|
|
);
|
|
return reply.status(201).send({ success: true, data: event, meta: null });
|
|
}
|
|
}
|
|
|
|
export const escalationController = new EscalationController();
|