- ticket-category-service.ts: createCategory, updateCategory, listCategories - ticket-service.ts: createTicket (TKT-NNNN numbering), updateTicket, getTicket, listTickets, transitionTicketStatus (guard map), resolveTicket (idempotent) - 5 ticket API routes: GET/POST /api/tickets, GET/PUT /api/tickets/[id], POST /api/tickets/[id]/status - 2 category API routes: GET/POST /api/ticket-categories, PUT /api/ticket-categories/[id] - 28 integration tests: lifecycle, transitions, deactivated category rejection, idempotent resolve, cross-tenant isolation
77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { withPermission } from "@/lib/middleware/authorize";
|
|
import { withTenantContext } from "@/lib/prisma-tenant";
|
|
import { transitionTicketStatus } from "@/lib/services/ticket-service";
|
|
import { TicketStatus } from "@prisma/client";
|
|
|
|
/**
|
|
* POST /api/tickets/[id]/status
|
|
*
|
|
* Transition a ticket to a new status.
|
|
* Enforces the guard map: OPEN->ASSIGNED|CLOSED, ASSIGNED->OPEN|RESOLVED,
|
|
* RESOLVED->CLOSED|OPEN, CLOSED->(terminal).
|
|
*
|
|
* Accepts: { status: TicketStatus }
|
|
*
|
|
* Requires: update on Ticket subject.
|
|
*
|
|
* Response:
|
|
* 200 OK — updated ticket with new status
|
|
* 400 Bad Request — invalid or disallowed transition
|
|
* 404 Not Found — ticket not found
|
|
* 401 Unauthorized — no session
|
|
* 403 Forbidden — insufficient role
|
|
*/
|
|
export function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
return withPermission("update", "Ticket")(
|
|
async (req: NextRequest, { user }) => {
|
|
if (!user.tenantId) {
|
|
return NextResponse.json(
|
|
{ error: "No tenant context — super-admins must use the admin API" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const { id } = await params;
|
|
|
|
let body: unknown;
|
|
try {
|
|
body = await req.json();
|
|
} catch {
|
|
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
}
|
|
|
|
const { status } = body as Record<string, unknown>;
|
|
|
|
if (!status || typeof status !== "string") {
|
|
return NextResponse.json({ error: "status is required" }, { status: 400 });
|
|
}
|
|
|
|
const validStatuses = Object.values(TicketStatus) as string[];
|
|
if (!validStatuses.includes(status)) {
|
|
return NextResponse.json(
|
|
{ error: `Invalid status. Must be one of: ${validStatuses.join(", ")}` },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const tenantPrisma = withTenantContext(user.tenantId);
|
|
|
|
try {
|
|
const ticket = await transitionTicketStatus(
|
|
tenantPrisma,
|
|
id,
|
|
status as TicketStatus
|
|
);
|
|
return NextResponse.json(ticket);
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : "Failed to transition ticket status";
|
|
if (message.includes("not found")) {
|
|
return NextResponse.json({ error: message }, { status: 404 });
|
|
}
|
|
return NextResponse.json({ error: message }, { status: 400 });
|
|
}
|
|
}
|
|
)(req);
|
|
}
|