Newer
Older
client_dwatch / lib / reports / __tests__ / target-customer.test.ts
import { describe, expect, it } from "vitest";

import {
  resolveTargetCustomer,
  TARGET_CUSTOMER_REQUIRED_MESSAGE,
} from "@/lib/reports/target-customer";
import type { Customer } from "@/lib/types";

/**
 * 確定期間KPIの「対象顧客をどう決めるか」(指示書AK ★段階6★ / §2-1 の決定表)。
 *
 * ★★何を守るためのテストか★★
 *  - ★★候補が2つ以上のときに「静かに1社を選ぶ」ことをしない★★(★落とし穴6★。D-133)—
 *    ★静かに選ぶ実装は例外もテスト失敗も出さない★ので、ここで固定しておく以外に検出手段が無い
 *  - ★`customer_id` が明示されたら顧客マスタを見ない(存在確認をしない)★(期待値表 6-9)
 *  - ★`group_id` の候補は「その傘下だけ」★(別ホールディングスの顧客を混ぜない)
 *
 * ★ルート(HTTPステータス・`details`)側は
 *   `app/api/reports/__tests__/route.test.ts` の「段階6 — 対象顧客の決定」が見る★。
 */

/** 顧客マスタ1行(`db.customers()` の形。★`group_id` は API の形=`null` 可★。D-011) */
function customer(id: number, groupId: number | null): Customer {
  return { id, customer_name: `屋号${id}`, group_id: groupId };
}

const NONE = { groupId: null, customerId: null };

describe("resolveTargetCustomer(指示書AK §2-1 の決定表)", () => {
  it("customer_id 指定 → その顧客(★顧客マスタを見ない=存在確認をしない★。6-9)", () => {
    // ★マスタが空でも、存在しないIDでも、そのまま通す★
    expect(resolveTargetCustomer([], { groupId: null, customerId: 999 })).toEqual({
      ok: true,
      customerId: 999,
    });
  });

  it("★customer_id と group_id の同時指定は customer_id が勝つ★(決定表の1行目)", () => {
    const customers = [customer(1, 1), customer(2, 1)];
    // ★傘下2社でも 422 にならない★ — 顧客が名指しされているので「決まらない」状態ではない
    expect(resolveTargetCustomer(customers, { groupId: 1, customerId: 2 })).toEqual({
      ok: true,
      customerId: 2,
    });
  });

  it("group_id 指定・傘下1社 → その顧客(6-6)", () => {
    const customers = [customer(1, 1), customer(2, 2), customer(3, null)];
    expect(resolveTargetCustomer(customers, { groupId: 2, customerId: null })).toEqual({
      ok: true,
      customerId: 2,
    });
  });

  it("★★group_id 指定・傘下2社 → 定まらない。候補は傘下だけ★★(6-7)", () => {
    const customers = [customer(1, 1), customer(2, 1), customer(3, 2)];
    expect(resolveTargetCustomer(customers, { groupId: 1, customerId: null })).toEqual({
      ok: false,
      candidateCustomerIds: [1, 2],
    });
  });

  it("★group_id 指定・傘下0社 → 定まらない(候補は空配列)★(★指示書に明記が無く、起草者が決めた★)", () => {
    const customers = [customer(1, 1)];
    expect(resolveTargetCustomer(customers, { groupId: 9, customerId: null })).toEqual({
      ok: false,
      candidateCustomerIds: [],
    });
  });

  it("どちらも未指定・顧客が1社だけ → その顧客(★P1 はここ★。6-3)", () => {
    expect(resolveTargetCustomer([customer(7, null)], NONE)).toEqual({
      ok: true,
      customerId: 7,
    });
  });

  it("★★どちらも未指定・顧客が2社 → 定まらない(★静かに1社を選ばない★)★★(6-4 / 6-5)", () => {
    const customers = [customer(1, 1), customer(2, 2)];
    expect(resolveTargetCustomer(customers, NONE)).toEqual({
      ok: false,
      candidateCustomerIds: [1, 2],
    });
  });

  it("★どちらも未指定・顧客が0社 → 定まらない★(マスタが空のときに何かを選ばない)", () => {
    expect(resolveTargetCustomer([], NONE)).toEqual({
      ok: false,
      candidateCustomerIds: [],
    });
  });

  it("★`group_id = null`(ホールディングスなし)の顧客は group_id 指定に当たらない★(D-011)", () => {
    const customers = [customer(1, null), customer(2, 1)];
    // ★「所属なしの顧客だけを引く」経路はそもそも存在しない★(API は 1 以上しか受け付けない)
    expect(resolveTargetCustomer(customers, { groupId: 1, customerId: null })).toEqual({
      ok: true,
      customerId: 2,
    });
  });

  it("★422 の文言は指示書AK §2-1 の原文★(言い換えない)", () => {
    expect(TARGET_CUSTOMER_REQUIRED_MESSAGE).toBe(
      "顧客またはグループを指定してください。複数の顧客のデータを合算した数値は出せません。"
    );
  });
});