Newer
Older
client_dwatch / lib / fixtures / __tests__ / masters.test.ts
import { describe, expect, it } from "vitest";
import { db, USE_FIXTURES } from "@/lib/db";
import { resolveFacilityIdsByMasters } from "@/lib/facilities/resolve-by-master";
import { customers, facilities, groups } from "@/lib/fixtures";

/**
 * 分類マスタの fixtures(`groups.ts` / `customers.ts`)の不変条件(指示書AK 段階1)。
 *
 * ------------------------------------------------------------------------
 * ★★何を守るためのテストか★★
 * ------------------------------------------------------------------------
 *  - ★fixtures 側を `notImplemented` にしないこと★(落とし穴9)—
 *    ★`USE_FIXTURES=true` で画面が落ちる★。ここで実際に `db.groups()` を呼んで確かめる
 *  - ★1グループ1顧客にしないこと★(落とし穴10)—
 *    ★3階層のUIを fixtures で確かめられなくなる★
 *  - ★「グループ0」を作らないこと★(落とし穴3)
 *
 * ★テスト実行時は `USE_FIXTURES` の既定(= true)で走る★
 *   (`lib/db/use-fixtures.ts`: `process.env.USE_FIXTURES !== "false"`)。
 */
describe("fixtures: 分類マスタ", () => {
  it("db.groups() / db.customers() が fixtures で動く(notImplemented にしない)", async () => {
    expect(USE_FIXTURES).toBe(true);
    await expect(db.groups()).resolves.toEqual(groups);
    await expect(db.customers()).resolves.toEqual(customers);
  });

  it("★複数グループ・複数顧客がある★(3階層のUIを確かめられること)", () => {
    expect(groups.length).toBeGreaterThanOrEqual(2);
    expect(customers.length).toBeGreaterThanOrEqual(2);
    // ★同一グループに2社★ … グループ単位の集計が「1社に定まらない」側(段階6 の 6-7)
    const byGroup = new Map<number | null, number>();
    for (const c of customers) {
      byGroup.set(c.group_id, (byGroup.get(c.group_id) ?? 0) + 1);
    }
    expect([...byGroup.values()].some((n) => n >= 2)).toBe(true);
    // ★傘下1社のグループ★ … 422 にならない側(6-6)
    expect([...byGroup.values()].some((n) => n === 1)).toBe(true);
    // ★ホールディングスなしの屋号★(`group_id: null`)
    expect(customers.some((c) => c.group_id === null)).toBe(true);
  });

  it("★id=0 のグループが無い/group_id に 0 を書いていない★(D-011)", () => {
    expect(groups.some((g) => g.id === 0)).toBe(false);
    expect(customers.some((c) => c.group_id === 0)).toBe(false);
  });

  it("id が一意で、customers.group_id は実在するグループを指す", () => {
    expect(new Set(groups.map((g) => g.id)).size).toBe(groups.length);
    expect(new Set(customers.map((c) => c.id)).size).toBe(customers.length);
    const ids = new Set(groups.map((g) => g.id));
    for (const c of customers) {
      if (c.group_id !== null) expect(ids.has(c.group_id)).toBe(true);
    }
  });

  /**
   * ★所属は `Facility` 自身が持つ★(指示書AK ★段階5★。段階1〜4 の
   * `customerIdByFacilityId` は削除済み)。★`customers` / `groups` との整合をここで固定する★ —
   * 片方だけ直すと ★屋号で絞ったときだけ施設が消える★という気づけない壊れ方をする。
   */
  it("施設12件すべてが、実在する屋号に1つだけ割り当たっている", () => {
    const customerById = new Map(customers.map((c) => [c.id, c]));
    const groupNameById = new Map(groups.map((g) => [g.id, g.group_name]));
    for (const f of facilities) {
      const customer = customerById.get(f.customer_id);
      expect(customer, `施設 ${f.id} の屋号(${f.customer_id}) が実在しない`).toBeDefined();
      // ★屋号名・ホールディングスは `customers` / `groups` の写しであること★
      expect(f.customer_name).toBe(customer?.customer_name);
      expect(f.group_id).toBe(customer?.group_id);
      // ★`group_id` が null なら名前も null★(「グループ0」を作らない。落とし穴3)
      expect(f.group_name).toBe(
        f.group_id === null ? null : (groupNameById.get(f.group_id) ?? null)
      );
    }
    // ★1顧客に寄せていないこと★(複数の屋号に散らす。落とし穴10)
    expect(new Set(facilities.map((f) => f.customer_id)).size).toBeGreaterThanOrEqual(2);
    // ★ホールディングスなしの施設がある★(段階5 で `group_name: null` を通すケース)
    expect(facilities.some((f) => f.group_id === null)).toBe(true);
  });
});

/**
 * 段階3(施設IDへの解決)が ★`USE_FIXTURES=true` でも動くこと★(指示書AK §6-3 / 落とし穴9)。
 *
 * ★★純粋関数の単体テストは `lib/facilities/__tests__/resolve-by-master.test.ts`★★。
 *   ここで確かめるのは ★fixtures 側のデータ取得(`db.facilityGroupIndex()`)が
 *   `notImplemented` でないこと★ と、★Prisma 版と同じ契約で解決できること★。
 */
describe("fixtures: 分類 → 施設IDの解決(指示書AK 段階3)", () => {
  it("db.facilityGroupIndex() が fixtures で動く(notImplemented にしない)", async () => {
    expect(USE_FIXTURES).toBe(true);
    const refs = await db.facilityGroupIndex();
    // ★施設を間引かないこと★(`deleted_at` の概念が無いので全件。罠7)
    expect(refs).toHaveLength(facilities.length);
    const customerIdByFacilityId = new Map(facilities.map((f) => [f.id, f.customer_id]));
    for (const ref of refs) {
      expect(customerIdByFacilityId.get(ref.id)).toBe(ref.customer_id);
    }
    // ★`group_id` は API の形(0 を漏らさない。落とし穴3)★
    expect(refs.some((r) => r.group_id === 0)).toBe(false);
    expect(refs.some((r) => r.group_id === null)).toBe(true);
  });

  it("★どちらも未指定なら DB を読まずに `null`★(条件なし)", async () => {
    await expect(
      resolveFacilityIdsByMasters({ groupId: null, customerId: null })
    ).resolves.toBeNull();
  });

  it("★屋号で絞ると、その屋号の施設だけが返る★", async () => {
    const expected = facilities
      .filter((f) => f.customer_id === 2)
      .map((f) => f.id)
      .sort((a, b) => a - b);
    const ids = await resolveFacilityIdsByMasters({ groupId: null, customerId: 2 });
    expect(ids).not.toBeNull();
    expect([...(ids as number[])].sort((a, b) => a - b)).toEqual(expected);
  });

  it("★ホールディングスで絞ると、傘下の複数屋号の施設が全部返る★", async () => {
    const customerIds = new Set(
      customers.filter((c) => c.group_id === 1).map((c) => c.id)
    );
    // ★傘下が2社あることを前提にしたケース★(fixtures がそうなっている。落とし穴10)
    expect(customerIds.size).toBeGreaterThanOrEqual(2);
    const expected = facilities
      .filter((f) => customerIds.has(f.customer_id))
      .map((f) => f.id)
      .sort((a, b) => a - b);
    const ids = await resolveFacilityIdsByMasters({ groupId: 1, customerId: null });
    expect([...(ids as number[])].sort((a, b) => a - b)).toEqual(expected);
  });

  it("★★実在しないIDは `[]`(0件ヒット)★★。★`null` にしないこと★", async () => {
    await expect(
      resolveFacilityIdsByMasters({ groupId: null, customerId: 9999 })
    ).resolves.toEqual([]);
  });
});