Files
admin bc76ebc5e5
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (push) Successful in 14s
CI (SIAX Cloud) / security (push) Failing after 16s
CI (SIAX Cloud) / quality (push) Successful in 52s
Security Scan (SIAX Cloud) / security (push) Successful in 58s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / quality (pull_request) Successful in 37s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 45s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m35s
CI (SIAX Cloud) / security (push) Trivy vulndb 404 (mirror.gcr.io) - infra-flake; re-run pa identisk sha gron
feat(orders): CSV-export-endpoint — export-kernelns första adoption (våg 3.4)
2026-09-29 17:01:13 +02:00

49 lines
1.9 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { listOwnedOrders } from "@/lib/orders-store";
import { requireOrderOwner } from "@/src/auth/require-order-owner";
import { isSiaxError, siaxErrorResponseForLegacy } from "../../siax-error-mapping";
/**
* GET /api/orders/export (våg 3.4, DoD §56) — CSV-export of the authenticated
* owner's orders. Same fail-closed auth as GET/POST /api/orders (this path is
* outside middleware.ts's /api/0pt matcher, so the Zitadel verifier runs here).
*
* RFC 4180: CRLF line endings, fields containing comma, quote or newline are
* quoted with doubled quotes, header row always present.
*
* Locale note: deposit_ore / total_ore are exported as RAW ÖRE — öre is the
* canonical unit in the order contract; formatting (SEK, thousand separators)
* happens in the UI layer, never in the export.
*
* Cap note (DoD §56): NO row truncation/limiting — an export must be complete.
*/
function csvEscape(value: string): string {
if (/[",\r\n]/.test(value)) return `"${value.replace(/"/g, '""')}"`;
return value;
}
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);
const header = ["order_id", "flow", "status", "deposit_ore", "total_ore"];
const rows = orders.map((o) =>
[o.order_id, o.flow, o.status, String(o.deposit_ore), String(o.total_ore)].map(csvEscape).join(","),
);
const csv = [header.join(","), ...rows].join("\r\n") + "\r\n";
const date = new Date().toISOString().slice(0, 10);
return new NextResponse(csv, {
status: 200,
headers: {
"Content-Type": "text/csv; charset=utf-8",
"Content-Disposition": `attachment; filename="orders-${date}.csv"`,
},
});
} catch (err: unknown) {
if (isSiaxError(err)) return siaxErrorResponseForLegacy(err);
throw err;
}
}