/**
* アラート(事前チェック)の対応状況・備考を更新するときの規則(純粋関数のみ。指示書AT §3-4 / §3-6)。
*
* ★期待値表は `lib/prechecks/__tests__/status-update.test.ts`★(指示書AT §3-4 の #1〜#12 と同じ番号)。
*
* ★★パッチ(DB に書く列)を作るのは `planPrecheckStatusUpdate()` だけ★★ —
* リクエスト本文のキーが DB に届く経路を作らない(`parsePrecheckStatusUpdateBody()` は
* `status` / `note` 以外のキーを捨てる)。
*
* 不変条件(この規則で保たれる形。`d-watch-api.yaml` の「対応開始日時(対応中以降で設定)」に合わせた):
* 未解消 → `responded_at`・`resolved_at` とも null / 対応中 → `responded_at` のみ / 解消 → 両方あり
*
* ★`now` は引数で受け取る★(関数の中で `new Date()` しない)。★`toDbJst()` はここでは通さない★ —
* DB に書く直前に1回だけ通す(`lib/db/robot-error-write.ts`)。
*/
import { fail, type QueryParseResult } from "@/lib/api/query-result";
import { PRECHECK_NOTE_MAX_LENGTH } from "@/lib/config";
import type {
PrecheckStatus,
PrecheckStatusPatch,
PrecheckStatusUpdateRequest,
} from "@/lib/types";
const STATUSES: readonly PrecheckStatus[] = ["unresolved", "in_progress", "resolved"];
/**
* 更新前の行(API の値)。日時は ★「真の瞬間」の `Date`★(`PrecheckItem` の ISO 文字列を
* `new Date()` に通したもの)か null。
*/
export interface PrecheckStatusCurrent {
resolution_status: PrecheckStatus;
responded_at: Date | null;
resolved_at: Date | null;
note: string | null;
}
/**
* 更新前の値と要求から「変える列だけ」のパッチを作る(キーが無い列は書かない)。
*
* 日時の規則(要求する状態の不変条件に合わせる):
* - ★足りない日時は `now` で埋める★(未解消→対応中の `responded_at`、→解消の `resolved_at` 等)
* - ★既にある日時は上書きしない★(#4・#10・#11。解消済みを再度「解消」にしても `resolved_at` は動かない)
* - ★状態を変える要求では、要求する状態で null であるべき日時を消す★(「戻すと消す」#7・#8・#9)
* - ★同じ状態の要求では消さない★(埋めるだけ。#12)— 画面は備考だけ変えるときも現在の状態を送るので、
* 備考の編集で先方が入れた日時を消さないため(表に無い行。テストの「表に無い行」)
*
* 備考の規則(§3-6): `note` が無ければ変えない / 空文字・空白だけは null(削除)/
* それ以外は前後の空白を除く / トリム後が現在値と同じなら書かない。
*/
export function planPrecheckStatusUpdate(
current: PrecheckStatusCurrent,
request: PrecheckStatusUpdateRequest,
now: Date
): PrecheckStatusPatch {
const patch: PrecheckStatusPatch = {};
const target = request.status;
const statusChanges = target !== current.resolution_status;
if (statusChanges) patch.status = target;
// 要求する状態で「値があるべきか」
const needsResponded = target === "in_progress" || target === "resolved";
const needsResolved = target === "resolved";
if (needsResponded) {
if (current.responded_at === null) patch.responded_at = now;
} else if (statusChanges && current.responded_at !== null) {
patch.responded_at = null;
}
if (needsResolved) {
if (current.resolved_at === null) patch.resolved_at = now;
} else if (statusChanges && current.resolved_at !== null) {
patch.resolved_at = null;
}
if (request.note !== undefined) {
const trimmed = request.note.trim();
const next = trimmed === "" ? null : trimmed;
if (next !== current.note) patch.note = next;
}
return patch;
}
/** パッチが空(何も書かない)か */
export function isEmptyPatch(patch: PrecheckStatusPatch): boolean {
return Object.keys(patch).length === 0;
}
/**
* `PATCH /prechecks/{id}/status` の本文を検証する(JSON として読めた後の値を受ける)。
*
* - オブジェクトでなければ 422
* - `status` が enum 外(無い・型違いを含む)なら 422
* - `note` があって文字列でなければ(null も)422 / ★トリム前で★ `PRECHECK_NOTE_MAX_LENGTH` を超えれば 422
* - ★それ以外のキーは捨てる★(`updated_at` / `error_type` / 旧名 `memo` など)
* ★エラーに入力値をエコーしない★(`lib/api/path-params.ts` と同じ)。
*/
export function parsePrecheckStatusUpdateBody(
body: unknown
): QueryParseResult<PrecheckStatusUpdateRequest> {
if (body === null || typeof body !== "object" || Array.isArray(body)) {
return fail(422, "validation_failed", "本文の形式が不正です");
}
const record = body as Record<string, unknown>;
const status = record.status;
if (typeof status !== "string" || !STATUSES.includes(status as PrecheckStatus)) {
return fail(422, "validation_failed", "statusの値が不正です", {
status: [`${STATUSES.join(" / ")} のいずれかを指定してください`],
});
}
if (!("note" in record) || record.note === undefined) {
return { ok: true, query: { status: status as PrecheckStatus } };
}
const note = record.note;
if (typeof note !== "string") {
return fail(422, "validation_failed", "noteは文字列で指定してください");
}
// 文字数はトリム前の入力そのもので数える(`PATCH /ds-devices/{serial_number}/note` と同じ理由)
if (note.length > PRECHECK_NOTE_MAX_LENGTH) {
return fail(422, "validation_failed", "noteが長すぎます", {
note: [`${PRECHECK_NOTE_MAX_LENGTH}文字以内で入力してください`],
});
}
return { ok: true, query: { status: status as PrecheckStatus, note } };
}