Newer
Older
client_dwatch / lib / db / ds-alert-mapping.ts
/**
 * 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,
  };
}