feat(platform-service): add support case management foundation
This commit is contained in:
parent
14346fbd5d
commit
e4bff5a2fe
@ -93,6 +93,10 @@ const CONTAINER_DEFS: Record<string, ContainerConfig> = {
|
|||||||
scim_user_sync: { partitionKeyPath: '/connectorId' },
|
scim_user_sync: { partitionKeyPath: '/connectorId' },
|
||||||
scim_group_sync: { partitionKeyPath: '/connectorId' },
|
scim_group_sync: { partitionKeyPath: '/connectorId' },
|
||||||
scim_events: { partitionKeyPath: '/connectorId', defaultTtl: 90 * 86400 },
|
scim_events: { partitionKeyPath: '/connectorId', defaultTtl: 90 * 86400 },
|
||||||
|
// Support case management
|
||||||
|
support_cases: { partitionKeyPath: '/productId' },
|
||||||
|
support_case_notes: { partitionKeyPath: '/caseId' },
|
||||||
|
support_case_escalations: { partitionKeyPath: '/caseId', defaultTtl: 90 * 86400 },
|
||||||
// Telemetry (client diagnostics — see docs/WINDSURF/CLIENT_TELEMETRY_DESIGN.md)
|
// Telemetry (client diagnostics — see docs/WINDSURF/CLIENT_TELEMETRY_DESIGN.md)
|
||||||
telemetry_events: { partitionKeyPath: '/pk', defaultTtl: 30 * 86400 },
|
telemetry_events: { partitionKeyPath: '/pk', defaultTtl: 30 * 86400 },
|
||||||
telemetry_error_clusters: { partitionKeyPath: '/pk', defaultTtl: 90 * 86400 },
|
telemetry_error_clusters: { partitionKeyPath: '/pk', defaultTtl: 90 * 86400 },
|
||||||
|
|||||||
@ -0,0 +1,57 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
import { MemoryDatastoreProvider } from '@bytelyst/datastore';
|
||||||
|
import { _resetDatastoreProvider, setProvider } from '../../lib/datastore.js';
|
||||||
|
import * as repo from './repository.js';
|
||||||
|
|
||||||
|
describe('support cases repository', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setProvider(new MemoryDatastoreProvider());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
_resetDatastoreProvider();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores support cases, notes, and escalations', async () => {
|
||||||
|
await repo.createCase({
|
||||||
|
id: 'sup_1',
|
||||||
|
productId: 'lysnrai',
|
||||||
|
title: 'Agent escalated an incident',
|
||||||
|
status: 'open',
|
||||||
|
priority: 'high',
|
||||||
|
source: 'agent',
|
||||||
|
tags: ['incident'],
|
||||||
|
createdAt: '2026-03-15T00:00:00.000Z',
|
||||||
|
updatedAt: '2026-03-15T00:00:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
await repo.createNote({
|
||||||
|
id: 'sup_1:note:1',
|
||||||
|
caseId: 'sup_1',
|
||||||
|
productId: 'lysnrai',
|
||||||
|
authorType: 'user',
|
||||||
|
authorId: 'admin_1',
|
||||||
|
visibility: 'internal',
|
||||||
|
body: 'Initial triage completed.',
|
||||||
|
createdAt: '2026-03-15T00:01:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
await repo.createEscalation({
|
||||||
|
id: 'sup_1:esc:1',
|
||||||
|
caseId: 'sup_1',
|
||||||
|
productId: 'lysnrai',
|
||||||
|
escalatedTo: 'tier2',
|
||||||
|
reason: 'Customer impact confirmed',
|
||||||
|
triggeredBy: 'admin_1',
|
||||||
|
createdAt: '2026-03-15T00:02:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
const cases = await repo.listCases('lysnrai', { limit: 20 });
|
||||||
|
const notes = await repo.listNotes('sup_1');
|
||||||
|
const escalations = await repo.listEscalations('sup_1');
|
||||||
|
|
||||||
|
expect(cases).toHaveLength(1);
|
||||||
|
expect(notes[0].body).toContain('triage');
|
||||||
|
expect(escalations[0].escalatedTo).toBe('tier2');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import { NotFoundError } from '../../lib/errors.js';
|
||||||
|
import { getCollection } from '../../lib/datastore.js';
|
||||||
|
import type {
|
||||||
|
ListSupportCasesQuery,
|
||||||
|
SupportCaseDoc,
|
||||||
|
SupportCaseNoteDoc,
|
||||||
|
SupportEscalationEventDoc,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
function caseCollection() {
|
||||||
|
return getCollection<SupportCaseDoc>('support_cases', '/productId');
|
||||||
|
}
|
||||||
|
|
||||||
|
function noteCollection() {
|
||||||
|
return getCollection<SupportCaseNoteDoc>('support_case_notes', '/caseId');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escalationCollection() {
|
||||||
|
return getCollection<SupportEscalationEventDoc>('support_case_escalations', '/caseId');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCase(doc: SupportCaseDoc): Promise<SupportCaseDoc> {
|
||||||
|
return caseCollection().create(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listCases(
|
||||||
|
productId: string,
|
||||||
|
query: ListSupportCasesQuery
|
||||||
|
): Promise<SupportCaseDoc[]> {
|
||||||
|
return caseCollection().findMany({
|
||||||
|
filter: {
|
||||||
|
productId,
|
||||||
|
...(query.status ? { status: query.status } : {}),
|
||||||
|
...(query.priority ? { priority: query.priority } : {}),
|
||||||
|
...(query.source ? { source: query.source } : {}),
|
||||||
|
...(query.assignedTo ? { assignedTo: query.assignedTo } : {}),
|
||||||
|
},
|
||||||
|
sort: { createdAt: -1 },
|
||||||
|
limit: query.limit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCase(id: string, productId: string): Promise<SupportCaseDoc> {
|
||||||
|
const doc = await caseCollection().findById(id, productId);
|
||||||
|
if (!doc) throw new NotFoundError(`Support case '${id}' not found`);
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCase(
|
||||||
|
id: string,
|
||||||
|
productId: string,
|
||||||
|
updates: Partial<SupportCaseDoc>
|
||||||
|
): Promise<SupportCaseDoc> {
|
||||||
|
const updated = await caseCollection().update(id, productId, {
|
||||||
|
...updates,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
if (!updated) throw new NotFoundError(`Support case '${id}' not found`);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createNote(doc: SupportCaseNoteDoc): Promise<SupportCaseNoteDoc> {
|
||||||
|
return noteCollection().create(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listNotes(caseId: string): Promise<SupportCaseNoteDoc[]> {
|
||||||
|
return noteCollection().findMany({
|
||||||
|
filter: { caseId },
|
||||||
|
sort: { createdAt: 1 },
|
||||||
|
limit: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createEscalation(
|
||||||
|
doc: SupportEscalationEventDoc
|
||||||
|
): Promise<SupportEscalationEventDoc> {
|
||||||
|
return escalationCollection().create(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listEscalations(caseId: string): Promise<SupportEscalationEventDoc[]> {
|
||||||
|
return escalationCollection().findMany({
|
||||||
|
filter: { caseId },
|
||||||
|
sort: { createdAt: -1 },
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
import Fastify from 'fastify';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
const repoMock = {
|
||||||
|
listCases: vi.fn(),
|
||||||
|
createCase: vi.fn(),
|
||||||
|
getCase: vi.fn(),
|
||||||
|
updateCase: vi.fn(),
|
||||||
|
createNote: vi.fn(),
|
||||||
|
listNotes: vi.fn(),
|
||||||
|
createEscalation: vi.fn(),
|
||||||
|
listEscalations: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('./repository.js', () => repoMock);
|
||||||
|
|
||||||
|
async function buildApp(payload?: { sub: string; productId: string; role?: string }) {
|
||||||
|
const { supportCaseRoutes } = await import('./routes.js');
|
||||||
|
const app = Fastify({ logger: false });
|
||||||
|
if (payload) {
|
||||||
|
app.addHook('onRequest', async req => {
|
||||||
|
req.jwtPayload = payload;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await app.register(supportCaseRoutes, { prefix: '/api' });
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('supportCaseRoutes', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /support/cases creates an agent-linked case', async () => {
|
||||||
|
repoMock.createCase.mockResolvedValue({ id: 'sup_1' });
|
||||||
|
|
||||||
|
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/support/cases',
|
||||||
|
payload: {
|
||||||
|
title: 'Incident escalation',
|
||||||
|
source: 'agent',
|
||||||
|
runId: 'run_1',
|
||||||
|
reviewId: 'rev_1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(repoMock.createCase).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
source: 'agent',
|
||||||
|
status: 'triaged',
|
||||||
|
runId: 'run_1',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /support/cases/:id/notes adds a note to an existing case', async () => {
|
||||||
|
repoMock.getCase.mockResolvedValue({ id: 'sup_1', productId: 'lysnrai' });
|
||||||
|
repoMock.createNote.mockResolvedValue({ id: 'sup_1:note:1' });
|
||||||
|
|
||||||
|
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/support/cases/sup_1/notes',
|
||||||
|
payload: {
|
||||||
|
authorId: 'admin_1',
|
||||||
|
body: 'Reached out to the customer.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(repoMock.createNote).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
caseId: 'sup_1',
|
||||||
|
authorId: 'admin_1',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /support/cases/:id/escalations updates the case and records the event', async () => {
|
||||||
|
repoMock.getCase.mockResolvedValue({ id: 'sup_1', productId: 'lysnrai' });
|
||||||
|
repoMock.updateCase.mockResolvedValue({ id: 'sup_1', status: 'escalated' });
|
||||||
|
repoMock.createEscalation.mockResolvedValue({ id: 'sup_1:esc:1' });
|
||||||
|
|
||||||
|
const app = await buildApp({ sub: 'admin_1', productId: 'lysnrai', role: 'admin' });
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/support/cases/sup_1/escalations',
|
||||||
|
payload: {
|
||||||
|
escalatedTo: 'tier2',
|
||||||
|
reason: 'Customer impact confirmed',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(repoMock.updateCase).toHaveBeenCalledWith('sup_1', 'lysnrai', { status: 'escalated' });
|
||||||
|
expect(repoMock.createEscalation).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
caseId: 'sup_1',
|
||||||
|
escalatedTo: 'tier2',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
143
services/platform-service/src/modules/support-cases/routes.ts
Normal file
143
services/platform-service/src/modules/support-cases/routes.ts
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import type { FastifyInstance } from 'fastify';
|
||||||
|
import { BadRequestError, ForbiddenError } from '../../lib/errors.js';
|
||||||
|
import {
|
||||||
|
CreateSupportCaseSchema,
|
||||||
|
CreateSupportEscalationSchema,
|
||||||
|
CreateSupportNoteSchema,
|
||||||
|
ListSupportCasesQuerySchema,
|
||||||
|
SupportCaseDoc,
|
||||||
|
SupportCaseNoteDoc,
|
||||||
|
SupportEscalationEventDoc,
|
||||||
|
UpdateSupportCaseSchema,
|
||||||
|
} from './types.js';
|
||||||
|
import * as repo from './repository.js';
|
||||||
|
|
||||||
|
function requireAdmin(req: { jwtPayload?: { sub?: string; role?: string; productId?: string } }): {
|
||||||
|
userId: string;
|
||||||
|
productId: string;
|
||||||
|
} {
|
||||||
|
const payload = req.jwtPayload;
|
||||||
|
if (!payload?.sub) throw new ForbiddenError('Authentication required');
|
||||||
|
if (!payload.role || !['super_admin', 'admin'].includes(payload.role)) {
|
||||||
|
throw new ForbiddenError('Admin access required');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
userId: payload.sub,
|
||||||
|
productId: payload.productId ?? process.env.DEFAULT_PRODUCT_ID ?? 'lysnrai',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validationError(message: string): never {
|
||||||
|
throw new BadRequestError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function supportCaseRoutes(app: FastifyInstance) {
|
||||||
|
app.get('/support/cases', async req => {
|
||||||
|
const access = requireAdmin(req);
|
||||||
|
const parsed = ListSupportCasesQuerySchema.safeParse(req.query);
|
||||||
|
if (!parsed.success) {
|
||||||
|
validationError(parsed.error.issues.map(issue => issue.message).join('; '));
|
||||||
|
}
|
||||||
|
return repo.listCases(access.productId, parsed.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/support/cases', async req => {
|
||||||
|
const access = requireAdmin(req);
|
||||||
|
const parsed = CreateSupportCaseSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
validationError(parsed.error.issues.map(issue => issue.message).join('; '));
|
||||||
|
}
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const doc: SupportCaseDoc = {
|
||||||
|
id: `sup_${randomUUID()}`,
|
||||||
|
productId: access.productId,
|
||||||
|
orgId: parsed.data.orgId,
|
||||||
|
workspaceId: parsed.data.workspaceId,
|
||||||
|
requesterUserId: parsed.data.requesterUserId,
|
||||||
|
assignedTo: parsed.data.assignedTo,
|
||||||
|
title: parsed.data.title,
|
||||||
|
description: parsed.data.description,
|
||||||
|
status: parsed.data.source === 'agent' ? 'triaged' : 'open',
|
||||||
|
priority: parsed.data.priority,
|
||||||
|
source: parsed.data.source,
|
||||||
|
runId: parsed.data.runId,
|
||||||
|
reviewId: parsed.data.reviewId,
|
||||||
|
knowledgeBaseId: parsed.data.knowledgeBaseId,
|
||||||
|
tags: parsed.data.tags,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
return repo.createCase(doc);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/support/cases/:id', async req => {
|
||||||
|
const access = requireAdmin(req);
|
||||||
|
const { id } = req.params as { id: string };
|
||||||
|
return repo.getCase(id, access.productId);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.patch('/support/cases/:id', async req => {
|
||||||
|
const access = requireAdmin(req);
|
||||||
|
const { id } = req.params as { id: string };
|
||||||
|
const parsed = UpdateSupportCaseSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
validationError(parsed.error.issues.map(issue => issue.message).join('; '));
|
||||||
|
}
|
||||||
|
return repo.updateCase(id, access.productId, parsed.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/support/cases/:id/notes', async req => {
|
||||||
|
requireAdmin(req);
|
||||||
|
const { id } = req.params as { id: string };
|
||||||
|
return repo.listNotes(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/support/cases/:id/notes', async req => {
|
||||||
|
const access = requireAdmin(req);
|
||||||
|
const { id } = req.params as { id: string };
|
||||||
|
await repo.getCase(id, access.productId);
|
||||||
|
const parsed = CreateSupportNoteSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
validationError(parsed.error.issues.map(issue => issue.message).join('; '));
|
||||||
|
}
|
||||||
|
const doc: SupportCaseNoteDoc = {
|
||||||
|
id: `${id}:note:${randomUUID()}`,
|
||||||
|
caseId: id,
|
||||||
|
productId: access.productId,
|
||||||
|
authorType: parsed.data.authorType,
|
||||||
|
authorId: parsed.data.authorId,
|
||||||
|
visibility: parsed.data.visibility,
|
||||||
|
body: parsed.data.body,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
return repo.createNote(doc);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/support/cases/:id/escalations', async req => {
|
||||||
|
requireAdmin(req);
|
||||||
|
const { id } = req.params as { id: string };
|
||||||
|
return repo.listEscalations(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/support/cases/:id/escalations', async req => {
|
||||||
|
const access = requireAdmin(req);
|
||||||
|
const { id } = req.params as { id: string };
|
||||||
|
await repo.getCase(id, access.productId);
|
||||||
|
const parsed = CreateSupportEscalationSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
validationError(parsed.error.issues.map(issue => issue.message).join('; '));
|
||||||
|
}
|
||||||
|
const event: SupportEscalationEventDoc = {
|
||||||
|
id: `${id}:esc:${randomUUID()}`,
|
||||||
|
caseId: id,
|
||||||
|
productId: access.productId,
|
||||||
|
escalatedTo: parsed.data.escalatedTo,
|
||||||
|
reason: parsed.data.reason,
|
||||||
|
triggeredBy: access.userId,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
await repo.updateCase(id, access.productId, { status: 'escalated' });
|
||||||
|
return repo.createEscalation(event);
|
||||||
|
});
|
||||||
|
}
|
||||||
118
services/platform-service/src/modules/support-cases/types.ts
Normal file
118
services/platform-service/src/modules/support-cases/types.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const SupportCaseStatusSchema = z.enum([
|
||||||
|
'open',
|
||||||
|
'triaged',
|
||||||
|
'in_progress',
|
||||||
|
'waiting_customer',
|
||||||
|
'resolved',
|
||||||
|
'closed',
|
||||||
|
'escalated',
|
||||||
|
]);
|
||||||
|
export const SupportCasePrioritySchema = z.enum(['critical', 'high', 'medium', 'low']);
|
||||||
|
export const SupportCaseSourceSchema = z.enum(['manual', 'agent', 'telemetry', 'customer']);
|
||||||
|
export const SupportNoteVisibilitySchema = z.enum(['internal', 'customer']);
|
||||||
|
export const SupportAuthorTypeSchema = z.enum(['user', 'agent', 'system']);
|
||||||
|
|
||||||
|
export const SupportCaseSchema = z.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
productId: z.string().min(1),
|
||||||
|
orgId: z.string().optional(),
|
||||||
|
workspaceId: z.string().optional(),
|
||||||
|
requesterUserId: z.string().optional(),
|
||||||
|
assignedTo: z.string().optional(),
|
||||||
|
title: z.string().min(1),
|
||||||
|
description: z.string().optional(),
|
||||||
|
status: SupportCaseStatusSchema,
|
||||||
|
priority: SupportCasePrioritySchema,
|
||||||
|
source: SupportCaseSourceSchema,
|
||||||
|
runId: z.string().optional(),
|
||||||
|
reviewId: z.string().optional(),
|
||||||
|
knowledgeBaseId: z.string().optional(),
|
||||||
|
tags: z.array(z.string()).default([]),
|
||||||
|
createdAt: z.string(),
|
||||||
|
updatedAt: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SupportCaseDoc = z.infer<typeof SupportCaseSchema> & {
|
||||||
|
_ts?: number;
|
||||||
|
_etag?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SupportCaseNoteSchema = z.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
caseId: z.string().min(1),
|
||||||
|
productId: z.string().min(1),
|
||||||
|
authorType: SupportAuthorTypeSchema,
|
||||||
|
authorId: z.string().min(1),
|
||||||
|
visibility: SupportNoteVisibilitySchema,
|
||||||
|
body: z.string().min(1),
|
||||||
|
createdAt: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SupportCaseNoteDoc = z.infer<typeof SupportCaseNoteSchema> & {
|
||||||
|
_ts?: number;
|
||||||
|
_etag?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SupportEscalationEventSchema = z.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
caseId: z.string().min(1),
|
||||||
|
productId: z.string().min(1),
|
||||||
|
escalatedTo: z.string().min(1),
|
||||||
|
reason: z.string().min(1),
|
||||||
|
triggeredBy: z.string().min(1),
|
||||||
|
createdAt: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SupportEscalationEventDoc = z.infer<typeof SupportEscalationEventSchema> & {
|
||||||
|
_ts?: number;
|
||||||
|
_etag?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreateSupportCaseSchema = z.object({
|
||||||
|
orgId: z.string().optional(),
|
||||||
|
workspaceId: z.string().optional(),
|
||||||
|
requesterUserId: z.string().optional(),
|
||||||
|
assignedTo: z.string().optional(),
|
||||||
|
title: z.string().min(1),
|
||||||
|
description: z.string().optional(),
|
||||||
|
priority: SupportCasePrioritySchema.default('medium'),
|
||||||
|
source: SupportCaseSourceSchema.default('manual'),
|
||||||
|
runId: z.string().optional(),
|
||||||
|
reviewId: z.string().optional(),
|
||||||
|
knowledgeBaseId: z.string().optional(),
|
||||||
|
tags: z.array(z.string()).default([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateSupportCaseSchema = z.object({
|
||||||
|
assignedTo: z.string().optional(),
|
||||||
|
title: z.string().min(1).optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
status: SupportCaseStatusSchema.optional(),
|
||||||
|
priority: SupportCasePrioritySchema.optional(),
|
||||||
|
knowledgeBaseId: z.string().optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateSupportNoteSchema = z.object({
|
||||||
|
authorType: SupportAuthorTypeSchema.default('user'),
|
||||||
|
authorId: z.string().min(1),
|
||||||
|
visibility: SupportNoteVisibilitySchema.default('internal'),
|
||||||
|
body: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateSupportEscalationSchema = z.object({
|
||||||
|
escalatedTo: z.string().min(1),
|
||||||
|
reason: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ListSupportCasesQuerySchema = z.object({
|
||||||
|
status: SupportCaseStatusSchema.optional(),
|
||||||
|
priority: SupportCasePrioritySchema.optional(),
|
||||||
|
source: SupportCaseSourceSchema.optional(),
|
||||||
|
assignedTo: z.string().optional(),
|
||||||
|
limit: z.coerce.number().min(1).max(100).default(20),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ListSupportCasesQuery = z.infer<typeof ListSupportCasesQuerySchema>;
|
||||||
@ -40,6 +40,7 @@ import { agentEvalRoutes } from './modules/agent-evals/routes.js';
|
|||||||
import { aiBudgetRoutes } from './modules/ai-budgets/routes.js';
|
import { aiBudgetRoutes } from './modules/ai-budgets/routes.js';
|
||||||
import { knowledgeRoutes } from './modules/knowledge/routes.js';
|
import { knowledgeRoutes } from './modules/knowledge/routes.js';
|
||||||
import { scimRoutes } from './modules/scim/routes.js';
|
import { scimRoutes } from './modules/scim/routes.js';
|
||||||
|
import { supportCaseRoutes } from './modules/support-cases/routes.js';
|
||||||
import { notificationRoutes } from './modules/notifications/routes.js';
|
import { notificationRoutes } from './modules/notifications/routes.js';
|
||||||
import { flagRoutes } from './modules/flags/routes.js';
|
import { flagRoutes } from './modules/flags/routes.js';
|
||||||
import { rateLimitRoutes } from './modules/ratelimit/routes.js';
|
import { rateLimitRoutes } from './modules/ratelimit/routes.js';
|
||||||
@ -149,6 +150,7 @@ await app.register(agentEvalRoutes, { prefix: '/api' });
|
|||||||
await app.register(aiBudgetRoutes, { prefix: '/api' });
|
await app.register(aiBudgetRoutes, { prefix: '/api' });
|
||||||
await app.register(knowledgeRoutes, { prefix: '/api' });
|
await app.register(knowledgeRoutes, { prefix: '/api' });
|
||||||
await app.register(scimRoutes, { prefix: '/api' });
|
await app.register(scimRoutes, { prefix: '/api' });
|
||||||
|
await app.register(supportCaseRoutes, { prefix: '/api' });
|
||||||
await app.register(notificationRoutes, { prefix: '/api' });
|
await app.register(notificationRoutes, { prefix: '/api' });
|
||||||
await app.register(flagRoutes, { prefix: '/api' });
|
await app.register(flagRoutes, { prefix: '/api' });
|
||||||
await app.register(rateLimitRoutes, { prefix: '/api' });
|
await app.register(rateLimitRoutes, { prefix: '/api' });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user