import { describe, expect, it } from "vitest";
import {
filterFixtureFacilityPerformance,
filterFixtureReportKpis,
filterFixtureReportRows,
} from "@/lib/db/fixture-reports";
import { facilityPerformance, reportKpis } from "@/lib/fixtures";
import { REPORT_KPI_CUSTOMER_ID } from "@/lib/fixtures/reports";
import { todayJst } from "@/lib/format/date";
import { shiftPeriod } from "@/lib/format/period";
import {
selectFacilityPerformanceRows,
selectKpiRow,
type ReportQuery,
} from "@/lib/reports/query";
import type {
FacilityPerformanceRow,
ReportDbQuery,
ReportKpiDbQuery,
ReportKpiRow,
} from "@/lib/types";
/**
* fixtures 版レポート4メソッドの絞り込み(指示書AC ★段階5★)。
*
* ★★何を守るためのテストか★★
* 1. ★`USE_FIXTURES` の true/false で件数が変わらないこと★(禁止事項6)—
* Prisma 版が SQL に降ろした条件と ★同じ意味★ をここで再現している
* 2. ★★`facility_ids` の `null` と `[]` が別物であること★★(★罠1 / 罠9★)—
* ★`[]` を「条件なし」と読んで全件返す実装になっていないか★
* 3. ★絞り込み規則の「正」(`selectXxxRows()` / `selectKpiRow()`)と一致すること★(C-5)
*/
function dbQuery(partial: Partial<ReportDbQuery> = {}): ReportDbQuery {
// ★`facility_ids: null` = 施設で絞らない★(指示書AK 段階4b で `ReportDbQuery` に足った。
// ★`[]`(0件ヒット)と混同しないこと★ — 既定を `[]` にすると全テストが0件になる)
return {
period: "weekly",
year: 2026,
month: 9,
week: 2,
facility_ids: null,
...partial,
};
}
/**
* `reportKpis()` 用(指示書AK ★段階6★)。★`customer_id` は必須★ —
* ★「顧客が決まらない」状態でデータ層を呼ばない契約★(`ReportKpiDbQuery`)。
* ★既定は fixtures のKPI行が属する屋号1★。
*/
function kpiDbQuery(partial: Partial<ReportKpiDbQuery> = {}): ReportKpiDbQuery {
return { ...dbQuery(), customer_id: REPORT_KPI_CUSTOMER_ID, ...partial };
}
/** `selectXxxRows()`(絞り込み規則の「正」)に渡す形。期間以外は素通しの値にする */
function memoryQuery(db: ReportDbQuery): ReportQuery {
return {
...db,
facilityName: "",
serial: "",
status: "all",
// ★絞り込まない(指示書AK 段階2 で足った2つ。`null` = 条件なし)★
groupId: null,
customerId: null,
limit: 100,
};
}
const KPI_ROWS: ReportKpiRow[] = [
{ period: "weekly", year: 2026, month: 9, week: 2, kpi: reportKpis[0].kpi },
{ period: "weekly", year: 2026, month: 9, week: 1, kpi: reportKpis[0].kpi },
// ★月次は week=0(D-072)。★週次の第0週ではない★ので weekly とは別行になる★
{ period: "monthly", year: 2026, month: 9, week: 0, kpi: reportKpis[0].kpi },
{ period: "monthly", year: 2026, month: 8, week: 0, kpi: reportKpis[0].kpi },
];
describe("filterFixtureReportRows(期間キーで絞る)", () => {
it("要求された期間の行だけを返す", () => {
expect(filterFixtureReportRows(KPI_ROWS, dbQuery())).toEqual([KPI_ROWS[0]]);
});
it("★前期(1つ前の期間)の行は返さない★(Prisma 版は前期比のために読むが、返しはしない)", () => {
const rows = filterFixtureReportRows(KPI_ROWS, dbQuery());
expect(rows.some((row) => row.week === 1)).toBe(false);
});
it("★monthly と weekly は混ざらない★(monthly の week=0 は予約値。D-072)", () => {
expect(
filterFixtureReportRows(KPI_ROWS, dbQuery({ period: "monthly", week: 0 }))
).toEqual([KPI_ROWS[2]]);
});
it("該当が無ければ空配列(404 にしない。指示書AC 段階5 期待値表 #5)", () => {
expect(filterFixtureReportRows(KPI_ROWS, dbQuery({ year: 2020 }))).toEqual([]);
});
});
describe("filterFixtureFacilityPerformance(期間キー + 施設ID)", () => {
const ROWS: FacilityPerformanceRow[] = [
{ period: "weekly", year: 2026, month: 9, week: 2, item: { ...facilityPerformance[0].item, facility_id: 1 } },
{ period: "weekly", year: 2026, month: 9, week: 2, item: { ...facilityPerformance[0].item, facility_id: 2 } },
{ period: "weekly", year: 2026, month: 9, week: 1, item: { ...facilityPerformance[0].item, facility_id: 1 } },
];
it("★`facility_ids: null` → 施設で絞らない★", () => {
expect(
filterFixtureFacilityPerformance(ROWS, { ...dbQuery(), facility_ids: null })
).toHaveLength(2);
});
it("★★`facility_ids: []` → 0件★★(★罠9。「条件なし」と読んで全件返していないか★)", () => {
expect(
filterFixtureFacilityPerformance(ROWS, { ...dbQuery(), facility_ids: [] })
).toEqual([]);
});
it("指定した施設だけを返す", () => {
const rows = filterFixtureFacilityPerformance(ROWS, {
...dbQuery(),
facility_ids: [2],
});
expect(rows.map((row) => row.item.facility_id)).toEqual([2]);
});
it("★期間の絞り込みと AND になる★(同じ施設でも期間が違えば落ちる)", () => {
const rows = filterFixtureFacilityPerformance(ROWS, {
...dbQuery({ week: 1 }),
facility_ids: [1],
});
expect(rows).toHaveLength(1);
expect(rows[0].week).toBe(1);
});
});
describe("★絞り込み規則の「正」と一致する★(指示書AC C-5。fixtures の実データで突き合わせる)", () => {
it("facilityPerformance: fixtures 版の絞り込み = `selectFacilityPerformanceRows()`", () => {
for (const row of facilityPerformance) {
const q = dbQuery({
period: row.period,
year: row.year,
month: row.month,
week: row.week,
});
expect(
filterFixtureFacilityPerformance(facilityPerformance, {
...q,
facility_ids: null,
}).map((r) => r.item)
).toEqual(selectFacilityPerformanceRows(facilityPerformance, memoryQuery(q)));
}
});
it("reportKpis: fixtures 版の絞り込み = `selectKpiRow()`", () => {
for (const row of reportKpis) {
const q = kpiDbQuery({
period: row.period,
year: row.year,
month: row.month,
week: row.week,
});
const filtered = filterFixtureReportKpis(reportKpis, q);
expect(selectKpiRow(filtered, memoryQuery(q))).toEqual(
selectKpiRow(reportKpis, memoryQuery(q))
);
}
});
/**
* ★年次(`month=0, week=0`)は月次(`week=0`)と別キー★(D-151。CLAUDE.md §4.7)
*
* ★何を守るためのテストか★: fixtures に今年・前年の行が無く、`USE_FIXTURES=true` でだけ
* 年次のKPIが4つとも null になっていた(監督の実測 2026-10-01。禁止事項6)。
* 月次の行も `week=0` を持つので、★年次の要求に月次の行が当たらない/その逆も無い★ことを
* fixtures の実データで確かめる。
*/
it("★年次の要求には年次の行だけが当たり、月次の week=0 行と取り違えない(今年・前年とも KPI が出る)★", () => {
const thisYear = Number(todayJst().slice(0, 4));
const lastYear = shiftPeriod("yearly", thisYear, 0, 0, -1);
const monthlyRows = reportKpis.filter((row) => row.period === "monthly");
// 前提: 取り違えの相手になる月次(week=0)の行が fixtures に実在する
expect(monthlyRows.length).toBeGreaterThan(0);
monthlyRows.forEach((row) => expect(row.week).toBe(0));
for (const year of [thisYear, lastYear.year]) {
const q = kpiDbQuery({ period: "yearly", year, month: 0, week: 0 });
const filtered = filterFixtureReportKpis(reportKpis, q);
expect(filtered, `${year}年`).toHaveLength(1);
expect(filtered[0]).toMatchObject({ period: "yearly", year, month: 0, week: 0 });
// ★監督の実測で null だった completion_rate が数値で出る★
const kpi = selectKpiRow(filtered, memoryQuery(q));
expect(kpi, `${year}年`).not.toBeNull();
expect(typeof kpi?.completion_rate.value, `${year}年`).toBe("number");
}
// 逆向き: 月次の要求(week=0)に年次の行は混ざらない
for (const row of monthlyRows) {
const q = kpiDbQuery({ period: "monthly", year: row.year, month: row.month, week: 0 });
expect(filterFixtureReportKpis(reportKpis, q).map((r) => r.period)).toEqual(["monthly"]);
}
});
});
/**
* `filterFixtureReportKpis`(期間キー + ★対象顧客★。指示書AK ★段階6★)。
*
* ★★何を守るためのテストか★★
* - ★Prisma 版が `customer_summaries` の `where` に降ろした `customer_id` と
* ★★同じ意味★★ を fixtures 側でも再現していること★(禁止事項6。
* ★`USE_FIXTURES` の切替で数字が変わってはいけない★)
* - ★★別顧客の行を拾わないこと★★(★拾うと「別会社の数字で判断する」ことになる★。D-5)
*/
describe("filterFixtureReportKpis(期間キー + 対象顧客)", () => {
/** ★2社ぶんの行★(fixtures 本体は1社ぶんしか持たないのでここで組み立てる) */
const ROWS = [
{ period: "weekly", year: 2026, month: 9, week: 2, customer_id: 1, kpi: reportKpis[0].kpi },
{ period: "weekly", year: 2026, month: 9, week: 2, customer_id: 2, kpi: reportKpis[1].kpi },
{ period: "weekly", year: 2026, month: 9, week: 1, customer_id: 2, kpi: reportKpis[1].kpi },
] as const;
it("★対象顧客の行だけを返す★(★期待値表 6-2 の fixtures 版★)", () => {
expect(filterFixtureReportKpis(ROWS, kpiDbQuery({ customer_id: 1 }))).toEqual([ROWS[0]]);
expect(filterFixtureReportKpis(ROWS, kpiDbQuery({ customer_id: 2 }))).toEqual([ROWS[1]]);
});
it("★存在しない顧客 → 空配列★(404 にしない。期待値表 6-9)", () => {
expect(filterFixtureReportKpis(ROWS, kpiDbQuery({ customer_id: 999 }))).toEqual([]);
});
it("★期間が違えば同じ顧客でも返さない★(期間キーの絞り込みを壊していない)", () => {
expect(filterFixtureReportKpis(ROWS, kpiDbQuery({ customer_id: 1, week: 1 }))).toEqual([]);
});
it("★fixtures 本体のKPI行は屋号1 のもの★(`REPORT_KPI_CUSTOMER_ID`)", () => {
for (const row of reportKpis) {
expect(row.customer_id).toBe(REPORT_KPI_CUSTOMER_ID);
}
});
});