Files
Flight-Ops/app/api/test/create-package/route.ts
T
kamilkrzywda 179fad450d
CI / build + unit + e2e (pull_request) Successful in 8m2s
Package weight, payload capacity and per-kg payout (ISS-045)
Packages carry a real weightKg (unit weight x count with +/-30% jitter),
plane capacity becomes maxPayloadKg instead of 6 slots, load validation
sums cargo weight against the payload, and the payout gains a per-kg part.
Loadout display shows tonnage; OpenAPI swaps CatalogEntry.capacity for
maxPayloadKg and adds PackageState.weightKg.
2026-09-20 12:42:03 +00:00

97 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { randomInt, randomUUID } from 'node:crypto';
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.8.0, renamed create-contract → create-package in v0.12.0) —
// lets the e2e suite create an open package with a specified origin and/or
// destination (and optional deadline offset), so the full load → fly loop can be
// exercised on a known, short route without waiting for random spawns. Disabled
// unless FLIGHT_TEST_HOOKS=1 (set in docker-compose.yml for the e2e instance
// only); every other deployment 404s here. Sibling of /api/test/expire-package —
// same gate, same conventions.
//
// POST /api/test/create-package
// body { originIcao?: string, destIcao?: string, deadlineOffsetMs?: number, weightKg?: number }
// - ICACOs must exist in the airport dataset and differ (400 otherwise);
// omitted endpoints are picked at random by generatePackage.
// - deadlineOffsetMs defaults to a normal 6–72h game-time window (NOT the short
// hook deadline of expire-package) — pass an explicit value to override.
// - weightKg (v0.27.0) forces the package weight instead of the RNG draw, so e2e
// flows that load several packages can stay under the plane's payload deterministically.
// Returns 201 + the created package row.
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 input = body as Record<string, unknown>;
for (const key of Object.keys(input)) {
if (key !== 'originIcao' && key !== 'destIcao' && key !== 'deadlineOffsetMs' && key !== 'weightKg') {
return Response.json({ error: `unknown field "${key}"` }, { status: 400 });
}
}
for (const key of ['originIcao', 'destIcao'] as const) {
if (input[key] !== undefined && typeof input[key] !== 'string') {
return Response.json({ error: `${key} must be a string` }, { status: 400 });
}
}
if (input.deadlineOffsetMs !== undefined && (typeof input.deadlineOffsetMs !== 'number' || !Number.isFinite(input.deadlineOffsetMs))) {
return Response.json({ error: 'deadlineOffsetMs must be a finite number' }, { status: 400 });
}
if (input.weightKg !== undefined && (typeof input.weightKg !== 'number' || !Number.isFinite(input.weightKg) || input.weightKg <= 0)) {
return Response.json({ error: 'weightKg must be a positive finite number' }, { status: 400 });
}
try {
const inst = await getGameDb();
const sim = await tickNow(); // clock is current before "now" for the new package
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 knownIcas = new Set(airports.map((a) => a.icao));
for (const key of ['originIcao', 'destIcao'] as const) {
const v = input[key];
if (typeof v === 'string' && !knownIcas.has(v)) {
return Response.json({ error: `unknown airport ${v}` }, { status: 400 });
}
}
const rng = mulberry32(randomInt(0, 2 ** 31));
const created = generatePackage({
id: randomUUID(),
airports,
rng,
nowMs: sim.clockMs,
deadlineOffsetMs: input.deadlineOffsetMs as number | undefined,
originIcao: input.originIcao as string | undefined,
destIcao: input.destIcao as string | undefined,
weightKg: input.weightKg as number | undefined,
});
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 });
}
}