Newer
Older
client_dwatch / lib / facilities / __tests__ / resolve-by-master.test.ts
import { describe, expect, it } from "vitest";

import { facilityIdsMatchingMasters } from "@/lib/facilities/resolve-by-master";
import type { FacilityGroupRef } from "@/lib/types";

/**
 * 指示書AK 段階3 ★§6-1★ — 分類(ホールディングス/屋号)→ 施設ID の解決(★純粋関数の部分★)。
 *
 * ★★ここは「解決関数そのもの」のテストです★★。
 *   ★★これだけでは不十分★★ — ★ルートが `facility_ids` を渡し忘れても赤くなりません★
 *   (それは段階4 の担当。`resolve-by-name.test.ts` の但し書きと同じ)。
 *
 * ★戻り値の契約は `facilityIdsMatchingName()` と完全に同じ★:
 *   `null` = 条件なし / `[]` = 0件ヒット / それ以外 = 当たった施設ID。
 */

/**
 * ★実在の法人名・屋号を使わないための架空の構成★(`CLAUDE.md` §7 / `docs/fixtures.md`)。
 * `lib/fixtures/` の構成をなぞってある(★グループ1 は傘下2社★/★グループ2 は傘下1社★/
 * ★屋号4 はホールディングスなし★)。
 */
const REFS: readonly FacilityGroupRef[] = [
  { id: 1, customer_id: 1, group_id: 1 },
  { id: 2, customer_id: 2, group_id: 1 },
  { id: 3, customer_id: 1, group_id: 1 },
  { id: 4, customer_id: 3, group_id: 2 },
  // ★ホールディングスに属さない屋号の施設★(`group_id` が `null`)
  { id: 5, customer_id: 4, group_id: null },
];

describe("facilityIdsMatchingMasters(指示書AK 段階3 §6-1)", () => {
  it("★★どちらも未指定なら `null`(=分類で絞らない)★★。★`[]` を返さないこと★", () => {
    // ★`[]` は「0件ヒット」の意味★ — 何も選んでいないのに一覧が空になる(落とし穴1)
    expect(
      facilityIdsMatchingMasters(REFS, { groupId: null, customerId: null })
    ).toBeNull();
  });

  it("屋号を指定 → その屋号の施設だけ", () => {
    expect(
      facilityIdsMatchingMasters(REFS, { groupId: null, customerId: 1 })
    ).toEqual([1, 3]);
  });

  it("★★存在しない屋号は `[]`(=0件ヒット)★★。★`null` を返さないこと★", () => {
    // ★`null` を返すと「条件なし」になり、★0件のはずが全件表示に化ける★(罠1 / 落とし穴1)
    expect(
      facilityIdsMatchingMasters(REFS, { groupId: null, customerId: 999 })
    ).toEqual([]);
    expect(
      facilityIdsMatchingMasters(REFS, { groupId: 999, customerId: null })
    ).toEqual([]);
  });

  it("★ホールディングスを指定 → 傘下の複数屋号の施設が**全部**返る★", () => {
    // ★屋号1(施設1・3)と屋号2(施設2)の両方★ — 片方だけになっていたら赤くする
    expect(
      facilityIdsMatchingMasters(REFS, { groupId: 1, customerId: null })
    ).toEqual([1, 2, 3]);
  });

  it("★★同時指定は AND(積集合)★★ — 両方を満たす施設だけ", () => {
    expect(
      facilityIdsMatchingMasters(REFS, { groupId: 1, customerId: 2 })
    ).toEqual([2]);
  });

  it("★★噛み合わない同時指定は `[]`★★(★片方を捨てて `[1,2,3]` にしないこと★)", () => {
    // 屋号3 はホールディングス2 の傘下なので、ホールディングス1 との AND は0件
    expect(
      facilityIdsMatchingMasters(REFS, { groupId: 1, customerId: 3 })
    ).toEqual([]);
  });

  it("★ホールディングスなしの施設は `groupId` 指定に当たらない★(`group_id: null`)", () => {
    // ★API の `group_id` は1以上しか受け付けない★ので「所属なしだけを引く」経路は無い
    //   (`parseMasterIdParam()` が 0 を 422 にする。指示書AK §4-1 #3)
    expect(facilityIdsMatchingMasters(REFS, { groupId: 1, customerId: null })).not.toContain(5);
    expect(facilityIdsMatchingMasters(REFS, { groupId: 2, customerId: null })).not.toContain(5);
    // ★屋号で指定すれば引ける★
    expect(
      facilityIdsMatchingMasters(REFS, { groupId: null, customerId: 4 })
    ).toEqual([5]);
  });

  it("★論理削除された施設も解決対象に含まれる★(罠7)", () => {
    // ★解決用の読み取りが `deleted_at` で絞られていなければ削除済みの施設も `refs` に入る
    //   (`loadFacilityGroupIndex()`)。ここはそれを前提に解決する★
    const withDeleted: FacilityGroupRef[] = [
      ...REFS,
      { id: 99, customer_id: 1, group_id: 1 },
    ];
    expect(
      facilityIdsMatchingMasters(withDeleted, { groupId: null, customerId: 1 })
    ).toEqual([1, 3, 99]);
  });

  it("施設マスタが空でも例外にならない", () => {
    expect(facilityIdsMatchingMasters([], { groupId: 1, customerId: null })).toEqual([]);
    expect(
      facilityIdsMatchingMasters([], { groupId: null, customerId: null })
    ).toBeNull();
  });

  it("★入力の配列を壊さない(純粋関数)★", () => {
    const before = [...REFS];
    facilityIdsMatchingMasters(REFS, { groupId: 1, customerId: 1 });
    expect(REFS).toEqual(before);
  });
});