/**
* D-Sec アラート(`DsAlert`。`GET /ds-alerts`・ダッシュボード)を `robot_error_reports` から組み立てる変換ロジック(純粋関数のみ)。
*
* ★根拠★: TeamWorks 第11回 P-10 = A(`robot_error_reports` を正とする。D-Sec のアラートも対応状況の更新もこちら)/
* P-8(制御盤のエラーは `robot_id` NULL・シリアルは制御盤の SN。施設は行の `facility_id`・`section_id`)/
* ユーザー判断 2026-10-19(指示書AX U1〜U3)/監督判断(D1〜D3)。D-180。
*
* - ★`control_error_histories` は読まない★(P-10 = A)。`controls` も結合しない(施設は行の `facility_id`。U3)
* - ★`robot_id` / `robot` を読まない★(D-Sec の行は全部 `robot_id` NULL。`ROBOT_ERROR_DS_ALERT_SELECT` だけを使う)
* - ★`security_off` はここからは出ない★(`SECURITY_ON` は D-Sec アラートではない。U1・D4)
* - ★正は純粋関数の `===`、SQL(`dsAlertWhere()`)は写し★(照合順序が大小文字・末尾の空白を区別しない。D2)
* - ★fixtures 版・Prisma 版が同じ `dsAlertsFromErrorRows()` を通す★(`CLAUDE.md` §9.5。D13)
*
* テストは `lib/db/__tests__/ds-alert-mapping.test.ts`(指示書AX 表1・表5)。
*/
import { D_SEC_ALERT_ERROR_TYPES } from "@/lib/config";
import { ERROR_ROW_VISIBLE_WHERE, isVisibleErrorRow } from "@/lib/db/error-report-mapping";
import {
detectedAtFromError,
isKnownRobotErrorStatus,
knownRobotErrorStatusCodes,
resolutionStatusFromRobotErrorStatus,
robotErrorStatusFromResolutionStatus,
type PrecheckDetectedAtSource,
} from "@/lib/db/precheck-mapping";
import { jstIsoFromDb } from "@/lib/format/date";
import type { Prisma } from "@/lib/generated/prisma/client";
import type { DsAlert, DsAlertDbQuery, PrecheckStatus } from "@/lib/types";
/** `ROBOT_ERROR_DS_ALERT_SELECT` の結果がそのまま入る形 */
export interface DsAlertRowSource extends PrecheckDetectedAtSource {
error_id: string;
serial_number: string;
facility_id: bigint;
facility: { facility_name: string; deleted_at: Date | null };
section: { section_name: string; deleted_at: Date | null };
error_type: string;
status: number;
responded_at: Date | null;
resolved_at: Date | null;
comment: string | null;
}
/**
* ★D-Sec アラートの対象か(正)★: `error_type` が `D_SEC_ALERT_ERROR_TYPES` に `===` で一致し、`status` が 0/1/2。
*
* ★`toUpperCase()` / `trim()` で緩めない★(D2。表1 R8・R9)。
* ★`status` が 3 以上の行は外す(例外にしない)★ — `dsAlerts()` はダッシュボードも呼ぶので、
* 1行の想定外の値でダッシュボード全体を500にしない(D3。表1 R10)。
*/
export function isDsAlertRow(row: Pick<DsAlertRowSource, "error_type" | "status">): boolean {
// ★0/1/2 の判定は `isKnownRobotErrorStatus()` の1本(指示書BB D8。旧: このファイルの `dsAlertStatusCodes()`。値は同じ)★
return D_SEC_ALERT_ERROR_TYPES.includes(row.error_type) && isKnownRobotErrorStatus(row.status);
}
/**
* 1行 → `DsAlert`(★`isDsAlertRow()` を通った行だけを渡す★。`status` が 0/1/2 以外なら変換関数が例外)。
* ★`errorReportItemFromRow()`(`lib/db/error-report-mapping.ts`)と同じ書き方で揃える★(日時・BigInt の扱い)。
*/
export function dsAlertFromErrorRow(row: DsAlertRowSource): DsAlert {
return {
// ★`error_id` を string のまま★(varchar(50)。D-105)。★`Number()` を通さない★
id: row.error_id,
alert_type: "disconnected",
// ★生値のまま★(D-077)
device_serial: row.serial_number,
// ★BigInt を残すと `NextResponse.json` が例外を投げる★。★施設は行の facility_id(U3)★
facility_id: Number(row.facility_id),
facility_name: row.facility.facility_name,
section_name: row.section.section_name,
// ★`errored_at ?? created_at`(`detectedAtFromError()`)。9時間を足さない・引かない★
detected_at: jstIsoFromDb(detectedAtFromError(row)),
resolution_status: resolutionStatusFromRobotErrorStatus(row.status),
responded_at: row.responded_at ? jstIsoFromDb(row.responded_at) : null,
resolved_at: row.resolved_at ? jstIsoFromDb(row.resolved_at) : null,
note: row.comment,
};
}
/** ★解消でない状態(未解消・対応中)★。`open_only` の写し(SQL)に使う。★コードは `robotErrorStatusFromResolutionStatus()` から作る★ */
const OPEN_STATUSES: readonly PrecheckStatus[] = ["unresolved", "in_progress"];
/**
* 行 → D-Sec アラート(fixtures 版・Prisma 版が共有する1本)。
* ★対象(`isDsAlertRow()`)かつ見える行(`isVisibleErrorRow()`。削除済みの施設・清掃エリアを外す。U8)だけ★。
* ★`query.open_only` なら解消(resolved)を外す(正。指示書BB A6・D17)★ — ダッシュボードのバナーは解消でないものしか見ない。
* 並べ替えは呼び出し側(`sortDsAlerts()`)。
*/
export function dsAlertsFromErrorRows(rows: readonly DsAlertRowSource[], query: DsAlertDbQuery): DsAlert[] {
const alerts = rows.filter((r) => isDsAlertRow(r) && isVisibleErrorRow(r)).map(dsAlertFromErrorRow);
return query.open_only ? alerts.filter((a) => a.resolution_status !== "resolved") : alerts;
}
/**
* SQL 側の写し(★正は `dsAlertsFromErrorRows()`★。読む量を減らすだけ)。
* `error_type IN`・`status IN`(★`open_only` なら未解消・対応中だけ。指示書BB D17★)・施設と清掃エリアの可視性(`ERROR_ROW_VISIBLE_WHERE`。U8)。
*/
export function dsAlertWhere(query: DsAlertDbQuery): Prisma.RobotErrorReportWhereInput {
return {
error_type: { in: [...D_SEC_ALERT_ERROR_TYPES] },
status: {
in: query.open_only ? OPEN_STATUSES.map(robotErrorStatusFromResolutionStatus) : knownRobotErrorStatusCodes(),
},
...ERROR_ROW_VISIBLE_WHERE,
};
}