/** * RoutePlansController * * Routes: * POST /api/missions/:missionId/route-plan — trigger route plan generation (202) * GET /api/missions/:missionId/route-plan — get latest non-superseded route plan * * Idempotency-Key header is accepted on POST and forwarded to the workflow * (CLAUDE.md #7 — idempotent writes). */ import { Controller, Get, Headers, HttpCode, HttpStatus, Inject, Param, Post, UseGuards, NotFoundException, ForbiddenException, } from "@nestjs/common"; import { eq } from "drizzle-orm"; import { schema, type Database } from "@courtmitra/db"; import { RoutePlansService } from "./route-plans.service.js"; import { DB } from "../db/db.module.js"; import { CurrentUser } from "../auth/current-user.decorator.js"; import { ClerkAuthGuard } from "../auth/clerk-auth.guard.js"; @UseGuards(ClerkAuthGuard) @Controller("missions/:missionId/route-plan") export class RoutePlansController { constructor( @Inject(RoutePlansService) private readonly routePlans: RoutePlansService, @Inject(DB) private readonly db: Database, ) {} /** Verify the mission belongs to the requesting user. */ private async assertMissionOwnership(missionId: string, userId: string) { const [missionRow] = await this.db .select({ userId: schema.missions.userId }) .from(schema.missions) .where(eq(schema.missions.id, missionId)) .limit(1); if (!missionRow) throw new NotFoundException(`Mission not found: ${missionId}`); if (missionRow.userId !== userId) throw new ForbiddenException("You do not own this mission"); } @Post() @HttpCode(HttpStatus.ACCEPTED) async trigger( @Param("missionId") missionId: string, @CurrentUser() userId: string, @Headers("idempotency-key") idempotencyKey?: string, ) { await this.assertMissionOwnership(missionId, userId); return this.routePlans.trigger(missionId, idempotencyKey); } @Get() async getLatest(@Param("missionId") missionId: string, @CurrentUser() userId: string) { await this.assertMissionOwnership(missionId, userId); return this.routePlans.getLatest(missionId); } }