Files
NetForge/src/app/api/tickets/[id]/status/route.ts
kevin-asprec 74d26d92f0 feat(03-03): Ticket service, category service, API routes, and 28 integration tests
- 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
2026-03-05 07:40:10 +08:00

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);
}