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"),
}),
];