/**
* GET /reports/error-breakdown — エラー種別の内訳(発生件数の降順・生値。D-077)
*
* 形は `app/api/ds-alerts/route.ts` に合わせている。
* 仕様の正は `d-watch-api.yaml` の /reports/error-breakdown。
* `type` は `robot_error_reports.error_type` の生値。サーバ側で変換しない(D-077)。
*/
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { resolveFacilityIdsByMasters } from "@/lib/facilities/resolve-by-master";
import {
parseReportQuery,
selectErrorBreakdownRows,
sortErrorBreakdown,
toReportDbQuery,
} from "@/lib/reports/query";
import { withRequestId } from "@/lib/logger";
import { logApiError } from "@/lib/logger/api";
import type {
ApiError,
ErrorBreakdownItem,
ErrorBreakdownRow,
} from "@/lib/types";
const ROUTE = "/api/reports/error-breakdown";
function errorResponse(error: ApiError, status: number) {
return NextResponse.json(error, { status });
}
/**
* ★meta を返さない(一覧APIの例外。D-088で再判断)★
* CLAUDE.md §9.3 は「一覧APIは必ず meta を返す」としているが、本エンドポイントは
* **意図的に meta を返さない**。理由: エラー種別の内訳は集計結果であり全件返す。
* ページングが要るほどの種別数にはならない見込み(limit パラメータも無い)。
* YAML の /reports/error-breakdown も `{ items }` のみで定義済み。
* `ListResponse<T>` を使わないのはこの理由(型を合わせるために meta を捏造しない)。
* ★生値方針(D-077)では種別数が固定されない。実データで数十に膨らむ可能性があるため、
* 実DB接続後に件数を実測して再判断すること(D-088)★
*/
interface ErrorBreakdownResponse {
items: ErrorBreakdownItem[];
}
export async function GET(request: Request) {
return withRequestId(request, async () => {
const { searchParams } = new URL(request.url);
// `parseReportQuery` は /reports/* 共通なので status / facility_name / serial / limit も
// 解釈するが、本エンドポイントにそれらのパラメータは存在しない。使わないフィールドは
// 単に無視する(period / year / month / week の検証だけが必要)。
const parsed = parseReportQuery(searchParams);
if (!parsed.ok) {
return errorResponse(parsed.error, parsed.status);
}
let rows: ErrorBreakdownRow[];
try {
// ★★期間を SQL に降ろす★★(指示書AC 段階5)— ★`parseReportQuery()` を通った値を渡す★。
// ★★`selectErrorBreakdownRows()` / `sortErrorBreakdown()` は残す★★(指示書AC C-5)。
// ★★分類(`group_id` / `customer_id`)は施設IDの配列に解決してから渡す★★
// (指示書AK 段階4b §7-3。★`facility_error_summaries` を `facility_id` で
// 絞っていなかった D-133 申し送り③の解消★)— ★`null` = 絞らない / `[]` = 0件★。
// ★`rate` の分母も絞ったあとの母集団になる★(「この屋号のエラーのうち何%」)。
rows = await db.errorBreakdown(
toReportDbQuery(parsed.query, await resolveFacilityIdsByMasters(parsed.query))
);
} catch (e) {
const body: ApiError = {
error: {
code: "internal_error",
message: "データの取得に失敗しました",
details: null,
},
};
logApiError({ route: ROUTE, method: "GET", body, status: 500, err: e });
return errorResponse(body, 500);
}
// ここから下は純粋な絞り込み・並べ替え。すべて try の外
// (`app/api/ds-alerts/route.ts` と同じ方針)。
const inPeriod: ErrorBreakdownItem[] = selectErrorBreakdownRows(
rows,
parsed.query
);
const items = sortErrorBreakdown(inPeriod);
const body: ErrorBreakdownResponse = { items };
return NextResponse.json(body);
});
}