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
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 ✓.
69 lines
3.5 KiB
TypeScript
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;
|
|
}
|
|
}
|