import { randomInt, randomUUID } from 'node:crypto'; import { eq } from 'drizzle-orm'; import { getGameDb, persist } from '@/lib/db/client'; import * as schema from '@/lib/db/schema'; import { loadAirportsGeoJson } from '@/lib/airports'; import { generatePackage, mulberry32 } from '@/lib/game/seed'; import { tickNow } from '@/lib/sim/engine'; // TEST-ONLY hook (v0.7.0, adapted to packages in v0.12.0) — lets the e2e suite // observe package expiry without waiting hours of game time. Disabled unless // FLIGHT_TEST_HOOKS=1, which is set in docker-compose.yml for the e2e instance // only; every other deployment 404s here. // // POST /api/test/expire-package // body { packageId?: string } // - with packageId: moves that OPEN package's deadline to now + HOOK_DEADLINE_MS // (game time) so the next fast tick expires it; 404 if unknown/not open. // - without: creates a fresh open package with the same short deadline. const HOOK_DEADLINE_MS = 10_000; // game ms past the current clock (~0.2 s real at 60×) export async function POST(req: Request) { if (process.env.FLIGHT_TEST_HOOKS !== '1') { return Response.json({ error: 'test hooks disabled' }, { status: 404 }); } let body: unknown = {}; try { const text = await req.text(); body = text ? JSON.parse(text) : {}; } catch { return Response.json({ error: 'invalid JSON body' }, { status: 400 }); } if (typeof body !== 'object' || body === null || Array.isArray(body)) { return Response.json({ error: 'body must be a JSON object' }, { status: 400 }); } const packageId = (body as Record).packageId; if (packageId !== undefined && typeof packageId !== 'string') { return Response.json({ error: 'packageId must be a string' }, { status: 400 }); } try { const inst = await getGameDb(); const sim = await tickNow(); // clock is current before we set the deadline const deadlineSimTime = Math.round(sim.clockMs + HOOK_DEADLINE_MS); if (typeof packageId === 'string') { const rows = await inst.db.select().from(schema.packages).where(eq(schema.packages.id, packageId)); const row = rows[0]; if (!row || row.status !== 'open') { return Response.json({ error: 'package not found or not open' }, { status: 404 }); } await inst.db.update(schema.packages).set({ deadlineSimTime }).where(eq(schema.packages.id, packageId)); await persist(inst); const updated = (await inst.db.select().from(schema.packages).where(eq(schema.packages.id, packageId)))[0]; return Response.json(updated ?? row); } const fc = await loadAirportsGeoJson(); const airports = fc.features.map((f) => ({ icao: f.properties.icao, name: f.properties.name, lat: f.properties.lat, lon: f.properties.lon, })); const rng = mulberry32(randomInt(0, 2 ** 31)); const created = generatePackage({ id: randomUUID(), airports, rng, nowMs: sim.clockMs, deadlineOffsetMs: HOOK_DEADLINE_MS }); await inst.db.insert(schema.packages).values(created); await persist(inst); return Response.json(created, { status: 201 }); } catch (err) { console.error('test hook failed', err); return Response.json({ error: 'test hook failed' }, { status: 500 }); } }