feat: P4.4 support REST (tickets/escalate/callbacks/queue admin) + kernel-client methods

8 routes under /v1/support (session-auth). RestClient gains createTicket/escalate/
listTickets/patchTicket/requestCallback/createQueue/joinQueue/setAvailability +
Ticket/CallbackRequest types.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-01 11:58:52 +05:30
parent 859124914c
commit e9e61b52bc
5 changed files with 188 additions and 1 deletions
@@ -0,0 +1,76 @@
import { BadRequestException, Body, Controller, Get, Headers, Param, Patch, Post, Query } from '@nestjs/common';
import { IiosTicketState } from '@prisma/client';
import { SupportService } from './support.service';
import { AssignmentService } from './assignment.service';
import { SessionVerifier } from '../platform/session.verifier';
import type { MessagePrincipal } from '../identity/actor.resolver';
import {
AvailabilityDto,
CallbackDto,
CreateQueueDto,
CreateTicketDto,
EscalateDto,
PatchTicketDto,
} from './support.dto';
@Controller('v1/support')
export class SupportController {
constructor(
private readonly support: SupportService,
private readonly assignment: AssignmentService,
private readonly session: SessionVerifier,
) {}
@Post('tickets')
async createTicket(@Body() body: CreateTicketDto, @Headers('authorization') auth?: string) {
return this.support.createTicket(this.principal(auth), body);
}
@Post('escalate')
async escalate(@Body() body: EscalateDto, @Headers('authorization') auth?: string) {
return this.support.escalate(body.threadId, this.principal(auth), body.subject);
}
@Get('tickets')
async listTickets(@Query('scope') scope?: string, @Headers('authorization') auth?: string) {
return this.support.listTickets(this.principal(auth), { scope: scope === 'assigned' ? 'assigned' : 'mine' });
}
@Patch('tickets/:id')
async patchTicket(@Param('id') id: string, @Body() body: PatchTicketDto, @Headers('authorization') auth?: string) {
return this.support.transition(id, this.principal(auth), body.state as IiosTicketState, body.reason);
}
@Post('callbacks')
async callback(@Body() body: CallbackDto, @Headers('authorization') auth?: string) {
return this.support.requestCallback(this.principal(auth), {
preferChannel: body.preferChannel,
preferredTime: body.preferredTime ? new Date(body.preferredTime) : undefined,
notes: body.notes,
ticketId: body.ticketId,
});
}
// ─── admin / agent ────────────────────────────────────────────────
@Post('queues')
async createQueue(@Body() body: CreateQueueDto, @Headers('authorization') auth?: string) {
return this.assignment.createQueue(this.principal(auth), body.name);
}
@Post('queues/:id/members')
async joinQueue(@Param('id') id: string, @Headers('authorization') auth?: string) {
return this.assignment.addMember(id, this.principal(auth));
}
@Patch('members/me/availability')
async availability(@Body() body: AvailabilityDto, @Headers('authorization') auth?: string) {
return this.assignment.setAvailability(this.principal(auth), body.state);
}
private principal(authorization?: string): MessagePrincipal {
const token = (authorization ?? '').replace(/^Bearer\s+/i, '');
if (!token) throw new BadRequestException('Authorization bearer token is required');
return this.session.verify(token);
}
}
@@ -0,0 +1,35 @@
import { IsIn, IsOptional, IsString } from 'class-validator';
const PRIORITIES = ['P0', 'P1', 'P2', 'P3', 'P4'] as const;
const STATES = ['NEW', 'OPEN', 'PENDING_CUSTOMER', 'PENDING_INTERNAL', 'RESOLVED', 'CLOSED', 'CANCELLED'] as const;
export class CreateTicketDto {
@IsString() subject!: string;
@IsOptional() @IsIn(PRIORITIES) priority?: (typeof PRIORITIES)[number];
@IsOptional() @IsString() threadId?: string;
}
export class EscalateDto {
@IsString() threadId!: string;
@IsOptional() @IsString() subject?: string;
}
export class PatchTicketDto {
@IsIn(STATES) state!: (typeof STATES)[number];
@IsOptional() @IsString() reason?: string;
}
export class CallbackDto {
@IsString() preferChannel!: string;
@IsOptional() @IsString() preferredTime?: string;
@IsOptional() @IsString() notes?: string;
@IsOptional() @IsString() ticketId?: string;
}
export class CreateQueueDto {
@IsString() name!: string;
}
export class AvailabilityDto {
@IsString() state!: string;
}
@@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
import { OutboxModule } from '../outbox/outbox.module';
import { SupportService } from './support.service';
import { AssignmentService } from './assignment.service';
import { SupportController } from './support.controller';
@Module({
imports: [OutboxModule],
controllers: [SupportController],
providers: [SupportService, AssignmentService],
exports: [SupportService, AssignmentService],
})