Files
0pt/app/api/orders/route.ts
admin a2377215e7
CI (SIAX Cloud) / sonar (pull_request) Skipped
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (pull_request) Successful in 7s
masterplan-lock / check (push) Successful in 8s
CI (SIAX Cloud) / security (pull_request) Successful in 19s
Security Scan (SIAX Cloud) / security (push) Successful in 20s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 23s
CI (SIAX Cloud) / security (push) Successful in 24s
CI (SIAX Cloud) / integration (pull_request) Successful in 31s
CI (SIAX Cloud) / quality (push) Successful in 44s
CI (SIAX Cloud) / quality (pull_request) Successful in 2m13s
feat(admin): InboxList — orderkön som universal inkorg (våg 3.3-adoption)
Servern projicerar öppna ordrar (draft/quoted/in_production/review) på
universal-inbox-konvolverna via createInboxItem (buildOrderInboxItem i
lib/order-flow, ADR-0001-mönstret: policy/resolution i serverlagret).
Admin renderar InboxList (@siax/ui 0.11.1) ovanpå orderkön — resolve-
aktionen gateras genom advance_action-state (available krävs). Terminala
statusar ger ingen post. tests/order-flow 6/6; full svit 1144/1144; build ✓.
2026-09-29 16:10:10 +02:00

69 lines
3.5 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { createOrder, listOwnedOrders, IdempotencyKeyConflictError, type OrderConfig } from "@/lib/orders-store";
import { requireOrderOwner } from "@/src/auth/require-order-owner";
import { resolveAdvanceAction, buildOrderInboxItem } from "@/lib/order-flow";
import { isSiaxError, siaxErrorResponseForLegacy } from "../siax-error-mapping";
// POST /api/orders (docs/04) — create a draft order from a configurator config.
// A payable order is created only for a verified Zitadel tenant/buyer.
// Anonymous prospects may obtain estimates but cannot mint payment orders.
//
// P014: honors an optional `Idempotency-Key` header (the convention this
// skill's own docs already name -- "Idempotency-Key on order/payment POSTs")
// so a retried submission (double-click, dropped response, proxy replay)
// returns the ORIGINAL order (200) instead of creating a duplicate (was:
// no idempotency mechanism at all -- every retry created a new row, P014
// recon "Duplicate delivery idempotent: NOT MET"). Omitting the header keeps
// the pre-P014 behavior exactly: every call creates a fresh order.
export async function POST(req: NextRequest) {
const auth = await requireOrderOwner(req);
if (auth.response) return auth.response;
let body: { flow?: string; config?: OrderConfig };
try {
body = await req.json();
} catch {
return NextResponse.json({ error: { code: "bad_json", message: "Invalid JSON" } }, { status: 400 });
}
if (!body || typeof body !== "object") {
return NextResponse.json({ error: { code: "bad_json", message: "Invalid JSON body" } }, { status: 400 });
}
const flow = body.flow || "custom";
const config = body.config || {};
const idempotencyKey = req.headers.get("idempotency-key") ?? undefined;
try {
const { order, replayed } = await createOrder(flow, config, idempotencyKey, auth.owner);
return NextResponse.json(
{ order_id: order.order_id, status: order.status, deposit_ore: order.deposit_ore, total_ore: order.total_ore },
{ status: replayed ? 200 : 201 },
);
} catch (err: unknown) {
if (err instanceof IdempotencyKeyConflictError) {
return NextResponse.json({ error: { code: "idempotency_key_conflict", message: err.message } }, { status: 409 });
}
// 0pt#53: getSqlForDurableStore throws SiaxError(DEPENDENCY_UNAVAILABLE, 503) when
// production has no DATABASE_URL — surface it as documented, not opaque 500.
if (isSiaxError(err)) return siaxErrorResponseForLegacy(err);
throw err;
}
}
// GET /api/orders — list all orders (queue + financial aggregates). This is
// outside middleware.ts's matcher (/api/0pt/:path* only), so auth is enforced
// here directly via the same fail-closed Zitadel verifier — see
// src/auth/require-route-auth.ts for why the request is re-verified rather
// than trusting a forwarded header.
export async function GET(req: NextRequest) {
const auth = await requireOrderOwner(req);
if (auth.response) return auth.response;
try {
const orders = await listOwnedOrders(auth.owner.tenantId);
// ADR-0001: servern resolvér advance-aktionen per order — klienten
// renderar bara (ActionGate). Policy ägs här, inte i UI-lagret.
const withActions = orders.map((o) => ({ ...o, advance_action: resolveAdvanceAction(o.status), inbox_item: buildOrderInboxItem(o) }));
return NextResponse.json(withActions);
} catch (err: unknown) {
if (isSiaxError(err)) return siaxErrorResponseForLegacy(err);
throw err;
}
}