Newer
Older
client_dwatch / lib / fixtures / error-reports.ts
import {
  CLEAN_TIMETABLE_ID_UNSCHEDULED,
  D_SEC_ALERT_ERROR_TYPES,
  ERROR_LIST_NOTE_REQUIRED_IDS,
  PRECHECK_MINUTES_BEFORE,
} from "@/lib/config";
import type { ErrorReportRowSource, ErrorTimetableRef } from "@/lib/db/error-report-mapping";
import { toDbJst } from "@/lib/format/date";
import { at } from "./_helpers";
import { dsDevices } from "./ds-devices";
import { facilities } from "./facilities";

/**
 * エラー一覧(`/errors`。指示書AW 段階3-2)のサンプルデータ。docs/fixtures.md §13。
 *
 * ★DB の行の形で持つ★(指示書AW D13)— 日時は `toDbJst()` を通した壁時計値の `Date`、id は `bigint`。
 *   ★fixtures と Prisma は同じ純粋関数★(`errorReportKeyFromRow()` / `filterSortErrorReportKeys()` /
 *   `errorReportItemFromRow()`)を通すので、`USE_FIXTURES` で件数や並びが変わらない(`CLAUDE.md` §9.5)。
 *
 * ★20行(`FX-ERR-01`〜`20`)。そのうち見える行は18行★ — 既定(未解消・対応中)で15行
 *   (`FX-ERR-03` / `14` / `15` 以外)・「解消済みも表示」で18行。
 *   ★`FX-ERR-20` は `clean_timetable_id = 0`(予定外のタスク。TeamWorks 回答 2026-10-27)★ — 予定は「なし」(`none`)で出る。
 *   ★`FX-ERR-18`(削除済みの施設)・`19`(削除済みの清掃エリア)はどこにも出ない★(`isVisibleErrorRow()`。指示書AX U8・表5 V2/V6)。
 *
 * ★D-Sec アラート(`dsAlerts()`)もこの配列から作る★(`dsAlertsFromErrorRows()`。指示書AX D13)—
 *   `D-S_CONTROL_FAILURE` の行のうち見える7行(`FX-ERR-10`・`12`〜`17`。未解消3・対応中2・解消2)。
 *   ★`FX-ERR-19` は削除済みの清掃エリアなので出ない★。エラー一覧の上書き表を共有するので、
 *   `PATCH /errors/{error_id}/status` で書いた対応状況が D-Sec にも出る(U4)。
 *
 * ★★値はダミー★★:
 *   - 実在の店名・住所を入れない(`CLAUDE.md` §7)。★施設名は `facilities` から id で引く★(直書きしない)
 *   - ★`error_level` の値(`CRITICAL` 以外)と、`SECURITY_ON` の `error_level` は実データ未受領のダミー★。
 *     `NOT_STARTED`・`CRITICAL` の組はステージングの実測の形(D-158 2026-10-06 実測・先方投入)。
 *     `D-S_CONTROL_FAILURE` は `CRITICAL`(第10回 N-5)
 *   - ★セキュリティ・清掃開始失敗のシリアルは `---`(`SERIAL_NOT_APPLICABLE`)★
 *   - `error_type` は生値のまま画面に出る(区分を作らない。D-077 / D-160 項3)
 *
 * ★`robot_id` は読まない★(`select` で絞る。指示書AU 段階0-4)。fixtures でも値は使わない —
 *   ★制御盤・セキュリティ・清掃開始失敗の行は `robot_id` が NULL★ という実データの形を示すためだけに持つ。
 *
 * ★予定(`errorReportTimetables`)は本表・履歴を分けない★(fixtures には2表の区別が無い。
 *   2表から引く規則は Prisma 版の `findTimetablesByChunks()` / `findTimetableById()`)。
 *
 * ★id は `"FX-ERR-01"` 〜★(`lib/fixtures/prechecks.ts` の id と重ならない。上書き表も別。指示書AW §11 Q8)。
 *
 * ★エラー内容(`error_list_id`。指示書BD §5-2。D-184)★ — ★大半は null(未選択)★。値があるのは次の5行だけ:
 *   - `FX-ERR-01`(EmergencyStop)= 40「非常停止スイッチが作動しました」/`FX-ERR-03`(HandleUp・解消済み)= 39「ハンドルが持ち上げられました」
 *     /`FX-ERR-06`(Offline)= 37「ロボットがオフラインです」 — ★有効な id★(`lib/fixtures/error-lists.ts` にある)
 *   - `FX-ERR-11`(★`NOT_STARTED` — 選べる行★)= ★131「その他(該当なし)」+備考あり★(`ERROR_LIST_NOTE_REQUIRED_IDS`)
 *   - `FX-ERR-05`(BrushError)= ★9999 — `error_lists` に無い id★(`error_list: null`・`error_list_id: 9999`。画面は「不明(9999)」)
 *   ★選べない行(`D-S_CONTROL_FAILURE`・`SECURITY_ON`)には入れない★(`ERROR_LIST_UNSELECTABLE_ERROR_TYPES`)。
 *   ★D-Sec の行はローカルDBにも投入される★(`scripts/seed-local-db.ts`)。シードも選べない行には値を入れない(入れようとすると止まる)。
 *   ローカルDBの `error_lists` 表と `error_list_id` 列は `scripts/local-db/` の手順で足す(指示書BD §8)。
 */

/**
 * fixtures の行(★`robot_id` は読まない。実データの形を示すためだけ★)。
 * ★`section` は全行に持たせる★(実DBでも `section_id` は NOT NULL)。`section_name` は D-Sec アラートが読む。
 */
export interface FixtureErrorReportRow extends ErrorReportRowSource {
  robot_id: bigint | null;
  section: { section_name: string; deleted_at: Date | null };
}

/**
 * セキュリティ・清掃開始失敗の行のシリアル。ステージングの実データ(2026-10-19 監督の SELECT・先方投入)。
 * P-8 の回答は `0` だが実値は `---`(指示書AX §12 申し送り2)。
 */
const SERIAL_NOT_APPLICABLE = "---";

/** 削除の日時(★過去の日時。`deleted_at` は NULL か否かだけで決まる★。指示書AX D12) */
const DELETED_AT = toDbJst(new Date("2026-10-01T10:00:00+09:00"));

// ★分数を直書きしないための別名(禁止事項3)★ — 30分前は設定値から読む
const SECOND_ROUND_MINUTES = PRECHECK_MINUTES_BEFORE[1];
// ★10 は設定(60/30)に無い値であることが本題なので直書きする★(`lib/fixtures/prechecks.ts` の `20` と同じ扱い。
//   D-105 項1: 設定に無い分数でも生の分数で「事前チェック(10分前)」と出す)
const UNCONFIGURED_MINUTES = 10;

/** 予定の id(`clean_timetables.id` 相当) */
const TT_MAIN_A = BigInt(9001);
const TT_PRECHECK_30 = BigInt(9002);
const TT_MAIN_B = BigInt(9003);
/** ★実DBでは `clean_timetable_histories` にだけある想定★(表1 E6。fixtures では2表を分けない) */
const TT_MAIN_HISTORY_ONLY = BigInt(9006);
const TT_PRECHECK_10 = BigInt(9009);
/** ★どの予定にも無い id★(表1 E5「予定不明」) */
const TT_MISSING = BigInt(9999);

/**
 * エラー内容の id(`lib/fixtures/error-lists.ts` の決定稿の id。★値に意味がある★ので名前を付ける)。
 * ★`ERROR_LIST_OTHER` は `ERROR_LIST_NOTE_REQUIRED_IDS` から読む★(備考必須の id を直書きしない)。
 */
const ERROR_LIST_ROBOT_OFFLINE = BigInt(37);
const ERROR_LIST_HANDRAIL_LIFTED = BigInt(39);
const ERROR_LIST_EMERGENCY_STOP = BigInt(40);
const ERROR_LIST_OTHER = BigInt(ERROR_LIST_NOTE_REQUIRED_IDS[0]);
/** ★`error_lists` に無い id★(名前解決できない。`TT_MISSING` と同じ 9999 だが別の表の id) */
const ERROR_LIST_MISSING = BigInt(9999);

/** 予定(本表・履歴をまとめたもの。`{ id, precheck }` だけ) */
export const errorReportTimetables: ErrorTimetableRef[] = [
  { id: TT_MAIN_A, precheck: 0 },
  { id: TT_PRECHECK_30, precheck: SECOND_ROUND_MINUTES },
  { id: TT_MAIN_B, precheck: 0 },
  { id: TT_MAIN_HISTORY_ONLY, precheck: 0 }, // 実DBでは履歴にだけある想定(E6)
  { id: TT_PRECHECK_10, precheck: UNCONFIGURED_MINUTES },
];

/** ★施設名は `facilities` から id で引く★(無ければ fixtures の誤りなので例外) */
function facilityOf(id: number): {
  facility_id: bigint;
  facility: { facility_name: string; deleted_at: Date | null };
} {
  const found = facilities.find((f) => f.id === id);
  if (!found) throw new Error(`[fixtures/error-reports] facilities に id=${id} がありません`);
  return { facility_id: BigInt(id), facility: { facility_name: found.facility_name, deleted_at: null } };
}

/**
 * ★削除済みのダミー施設★(表5 V2)。★`facilities`(`lib/fixtures/facilities.ts`)には足さない★ —
 *   施設一覧・分類・他の画面に出さないため。`facilityOf()` を通さない。
 */
function deletedDummyFacility(): {
  facility_id: bigint;
  facility: { facility_name: string; deleted_at: Date | null };
} {
  return {
    facility_id: BigInt(99),
    facility: { facility_name: "削除済みテスト施設(ダミー)", deleted_at: DELETED_AT },
  };
}

/** その施設の先頭のロボットのシリアル(直書きしない) */
function robotSerialOf(facilityId: number): string {
  const serial = facilities.find((f) => f.id === facilityId)?.robots[0]?.serial_number;
  if (!serial) throw new Error(`[fixtures/error-reports] 施設 id=${facilityId} にロボットがありません`);
  return serial;
}

/** その施設の D-S制御盤のシリアル(`lib/fixtures/ds-devices.ts` の実在の値) */
function controlPanelSerialOf(facilityId: number): string {
  const serial = dsDevices.find((d) => d.facility_id === facilityId)?.serial_number;
  if (!serial) throw new Error(`[fixtures/error-reports] 施設 id=${facilityId} に D-S制御盤がありません`);
  return serial;
}

/** n日前の HH:MM(JST)→ DB の壁時計値の `Date`(Prisma が返すのと同じ形) */
function dbAt(n: number, time: string): Date {
  return toDbJst(new Date(at(n, time)));
}

/** ロボットのエラー行の既定値(施設1・本清掃 A・未解消) */
function robotError(
  errorId: string,
  overrides: Partial<FixtureErrorReportRow> = {}
): FixtureErrorReportRow {
  return {
    error_id: errorId,
    robot_id: BigInt(101),
    clean_timetable_id: TT_MAIN_A,
    serial_number: robotSerialOf(1),
    ...facilityOf(1),
    section: { section_name: "1F売り場", deleted_at: null },
    error_type: "EmergencyStop",
    error_level: "ERROR",
    errored_at: dbAt(1, "23:10"),
    created_at: dbAt(1, "23:11"),
    status: 0,
    responded_at: null,
    resolved_at: null,
    comment: null,
    // ★既定は未選択★(指示書BD §5-2)
    error_list_id: null,
    ...overrides,
  };
}

/**
 * D-Sec アラートの行(制御盤のエラー)の既定値(指示書AX 段階3-1-3)。
 * ★`robot_id` NULL・`error_type` は設定から・`error_level` CRITICAL(第10回 N-5)・本清掃 A・未解消★。
 * ★シリアルは行の施設の制御盤★(直書きしない)。FX-ERR-16 のように食い違わせるときは `overrides` で上書きする。
 */
function dSecError(
  errorId: string,
  facilityId: number,
  overrides: Partial<FixtureErrorReportRow> = {}
): FixtureErrorReportRow {
  return robotError(errorId, {
    robot_id: null,
    serial_number: controlPanelSerialOf(facilityId),
    ...facilityOf(facilityId),
    error_type: D_SEC_ALERT_ERROR_TYPES[0],
    error_level: "CRITICAL",
    ...overrides,
  });
}

export const errorReportRows: FixtureErrorReportRow[] = [
  // E1: ロボットのエラー・本清掃に紐づく(未解消)。★エラー内容 = 有効な id(40)★
  robotError("FX-ERR-01", { error_list_id: ERROR_LIST_EMERGENCY_STOP }),
  // E2: ロボットのエラー・事前チェック(30分前)に紐づく(対応中・responded_at あり)
  robotError("FX-ERR-02", {
    clean_timetable_id: TT_PRECHECK_30,
    error_type: "LowBattery",
    error_level: "WARNING",
    errored_at: dbAt(1, "22:30"),
    created_at: dbAt(1, "22:31"),
    status: 1,
    responded_at: dbAt(0, "08:30"),
  }),
  // E3: 解消済み・本清掃(responded_at / resolved_at / 備考あり)— ★既定では出ない★
  robotError("FX-ERR-03", {
    robot_id: BigInt(103),
    serial_number: robotSerialOf(3),
    ...facilityOf(3),
    clean_timetable_id: TT_MAIN_B,
    error_type: "HandleUp",
    errored_at: dbAt(2, "23:40"),
    created_at: dbAt(2, "23:41"),
    status: 2,
    responded_at: dbAt(1, "08:20"),
    resolved_at: dbAt(1, "09:05"),
    comment: "現場でハンドルを戻してもらった",
    // ★解消済みの行でもエラー内容を持つ★(有効な id)
    error_list_id: ERROR_LIST_HANDRAIL_LIFTED,
  }),
  // E4: clean_timetable_id NULL(予定なし)
  robotError("FX-ERR-04", {
    robot_id: BigInt(102),
    serial_number: robotSerialOf(2),
    ...facilityOf(2),
    clean_timetable_id: null,
    error_type: "LostLocation",
    errored_at: dbAt(1, "15:20"),
    created_at: dbAt(1, "15:21"),
  }),
  // E5: clean_timetable_id がどの予定にも無い id(予定不明)
  robotError("FX-ERR-05", {
    robot_id: BigInt(102),
    serial_number: robotSerialOf(2),
    ...facilityOf(2),
    clean_timetable_id: TT_MISSING,
    error_type: "BrushError",
    errored_at: dbAt(1, "23:50"),
    created_at: dbAt(1, "23:51"),
    // ★`error_lists` に無い id★(`error_list: null`・`error_list_id` は 9999 のまま)
    error_list_id: ERROR_LIST_MISSING,
  }),
  // E6: 予定が履歴にだけある想定・本清掃
  robotError("FX-ERR-06", {
    robot_id: BigInt(104),
    serial_number: robotSerialOf(4),
    ...facilityOf(4),
    clean_timetable_id: TT_MAIN_HISTORY_ONLY,
    error_type: "Offline",
    errored_at: dbAt(3, "23:05"),
    created_at: dbAt(3, "23:06"),
    error_list_id: ERROR_LIST_ROBOT_OFFLINE,
  }),
  // E7: errored_at NULL(検知時刻は created_at)・本清掃
  robotError("FX-ERR-07", {
    robot_id: BigInt(104),
    serial_number: robotSerialOf(4),
    ...facilityOf(4),
    clean_timetable_id: TT_MAIN_B,
    error_type: "Offline",
    errored_at: null,
    created_at: dbAt(0, "06:40"),
  }),
  // E8: ★robot_id NULL・serial_number '---'・error_type SECURITY_ON★(セキュリティのエラー。D-158 P-8)。
  //   ★D-Sec アラートには出ない★(指示書AX U1)
  robotError("FX-ERR-08", {
    robot_id: null,
    serial_number: SERIAL_NOT_APPLICABLE,
    ...facilityOf(5),
    clean_timetable_id: TT_MAIN_B,
    error_type: "SECURITY_ON",
    error_level: "ERROR", // ★ダミー(実データ未受領)★
    errored_at: dbAt(1, "22:55"),
    created_at: dbAt(1, "22:56"),
  }),
  // E9: 事前チェック(★10分前。設定に無い値★)
  robotError("FX-ERR-09", {
    clean_timetable_id: TT_PRECHECK_10,
    error_type: "LowBattery",
    error_level: "WARNING",
    errored_at: dbAt(1, "22:50"),
    created_at: dbAt(1, "22:51"),
  }),
  // P-8 a: ★robot_id NULL・serial_number = 制御盤の SN・error_type D-S_CONTROL_FAILURE★(制御盤のエラー)。
  //   ★D-Sec アラート(指示書AX 表1 R1)★
  robotError("FX-ERR-10", {
    robot_id: null,
    serial_number: controlPanelSerialOf(1),
    clean_timetable_id: TT_MAIN_A,
    error_type: D_SEC_ALERT_ERROR_TYPES[0],
    error_level: "CRITICAL",
    errored_at: dbAt(1, "22:45"),
    created_at: dbAt(1, "22:46"),
  }),
  // ★robot_id NULL・serial_number '---'・error_type NOT_STARTED・error_level CRITICAL★
  //   (清掃を開始できなかった。ステージング実測の形 — D-158 2026-10-06 実測・先方投入)。
  //   ★D-Sec アラートには出ない★(指示書AX U2)
  robotError("FX-ERR-11", {
    robot_id: null,
    serial_number: SERIAL_NOT_APPLICABLE,
    ...facilityOf(5),
    clean_timetable_id: TT_MAIN_B,
    error_type: "NOT_STARTED",
    error_level: "CRITICAL",
    errored_at: dbAt(1, "23:30"),
    created_at: dbAt(1, "23:31"),
    // ★清掃開始失敗の行は選べる★(指示書BD §0 の決定)。★その他(備考必須)+備考あり★
    error_list_id: ERROR_LIST_OTHER,
    comment: "店舗の閉店作業が長引き、清掃開始時刻に通路が塞がっていた(ダミー)",
  }),

  // ---- D-Sec アラート(制御盤のエラー。指示書AX 段階3-1-3・表1)----
  // R4・R13: ★errored_at NULL(検知時刻は created_at)・clean_timetable_id NULL★
  dSecError("FX-ERR-12", 11, {
    clean_timetable_id: null,
    section: { section_name: "2F衣料品", deleted_at: null },
    errored_at: null,
    created_at: dbAt(0, "02:55"),
  }),
  // R2: 対応中・備考あり
  dSecError("FX-ERR-13", 5, {
    clean_timetable_id: TT_MAIN_B,
    section: { section_name: "1Fレジ前", deleted_at: null },
    errored_at: dbAt(1, "23:10"),
    created_at: dbAt(1, "23:11"),
    status: 1,
    responded_at: dbAt(0, "09:05"),
    comment: "回線業者に確認中",
  }),
  // R3: 解消
  dSecError("FX-ERR-14", 4, {
    section: { section_name: "B1食品", deleted_at: null },
    errored_at: dbAt(1, "03:55"),
    created_at: dbAt(1, "03:56"),
    status: 2,
    responded_at: dbAt(1, "08:40"),
    resolved_at: dbAt(1, "09:15"),
    comment: "正常復帰を確認",
  }),
  // R3: 解消(2日前)
  dSecError("FX-ERR-15", 10, {
    clean_timetable_id: TT_MAIN_B,
    section: { section_name: "1Fレジ前", deleted_at: null },
    errored_at: dbAt(2, "02:00"),
    created_at: dbAt(2, "02:01"),
    status: 2,
    responded_at: dbAt(2, "08:20"),
    resolved_at: dbAt(2, "08:30"),
    comment: "正常復帰を確認",
  }),
  // R11: ★シリアルは施設2の制御盤なのに、行の施設は3★(ステージング 1000004 の形。行の施設で出す。U3)
  dSecError("FX-ERR-16", 3, {
    serial_number: controlPanelSerialOf(2),
    errored_at: dbAt(0, "01:30"),
    created_at: dbAt(0, "01:31"),
  }),
  // R12: ★FX-ERR-16 と同じ検知時刻★(同値は id の辞書順で "FX-ERR-16" が先)
  dSecError("FX-ERR-17", 8, {
    clean_timetable_id: TT_MAIN_B,
    errored_at: dbAt(0, "01:30"),
    created_at: dbAt(0, "01:32"),
    status: 1,
    responded_at: dbAt(0, "08:50"),
  }),

  // ---- 削除済みの施設・清掃エリア(指示書AX U8・表5)。★どちらも status 0 — 可視性が無ければ既定で出るはずの行★ ----
  // V2: ★施設が削除済み★(清掃エリアは生きている)・ロボットのエラー
  robotError("FX-ERR-18", {
    robot_id: BigInt(199),
    serial_number: "DUMMY-DELETED-ROBOT", // ★ダミー施設は facilities に無いので、ロボットのシリアルも引けない★
    ...deletedDummyFacility(),
    error_type: "EmergencyStop",
    errored_at: dbAt(1, "23:20"),
    created_at: dbAt(1, "23:21"),
  }),
  // V6: ★清掃エリアが削除済み★(施設2は生きている)・D-S_CONTROL_FAILURE。
  //   ★施設2の他の行(FX-ERR-04・05)の清掃エリアは生きているので出続ける(表5 V9)★
  dSecError("FX-ERR-19", 2, {
    section: { section_name: "旧2F売り場(削除済み)", deleted_at: DELETED_AT },
    errored_at: dbAt(0, "02:10"),
    created_at: dbAt(0, "02:11"),
  }),

  // ---- clean_timetable_id = 0(予定外のタスク。TeamWorks 回答 2026-10-27)----
  // E4 と同じく ★予定は「なし」(`none`)・API の `clean_timetable_id` は null★(`scheduledTimetableId()`)。
  //   ★「予定不明」(`unknown`)に化けないこと★(0 は本表にも履歴にも無い id なので、`=== null` だけで判定すると化ける)
  robotError("FX-ERR-20", {
    robot_id: BigInt(110),
    serial_number: robotSerialOf(6),
    ...facilityOf(6),
    // ★`0` を直書きしない★(`CLEAN_TIMETABLE_ID_UNSCHEDULED`)
    clean_timetable_id: BigInt(CLEAN_TIMETABLE_ID_UNSCHEDULED),
    error_type: "LostLocation",
    errored_at: dbAt(1, "14:40"),
    created_at: dbAt(1, "14:41"),
  }),
];