/**
* 分類(ホールディングス/屋号)→ 施設ID の解決(指示書AK 段階3 §6-1)。
*
* ------------------------------------------------------------------------
* ★★戻り値の契約は `resolveFacilityIdsByName()` と完全に同じ★★
* ------------------------------------------------------------------------
* | 状況 | 戻り値 |
* |---|---|
* | `groupId` も `customerId` も未指定 | ★`null`★(=条件なし・全施設) |
* | 指定されたが該当0件 | ★`[]`★(=結果は0件) |
* | 該当あり | 施設IDの配列 |
*
* ★★`null` と `[]` を取り違えないこと★★(★罠1 / 落とし穴1★)—
* `where` 側は `facility_ids !== null` で分岐する約束(`lib/db/index.ts`)なので、
* ★0件ヒットで `null` を返すと条件ごと消えて**全件表示**に化けます★。
* ★例外もテスト失敗も出ません。★
*
* ------------------------------------------------------------------------
* ★★同時指定は AND(積集合)★★
* ------------------------------------------------------------------------
* `d-watch-api.yaml` の `GroupId` / `CustomerId` の description に書いた契約
* (段階2 で明記済み)= ★「両方を満たす施設」★。★どちらか片方を捨てないこと★。
* ★屋号がホールディングスに属していなければ、両方指定の結果は `[]` になる★
* (それが正しい。`null` にしないこと)。
*
* ------------------------------------------------------------------------
* ★★施設名の検索とは事情が違う★★(指示書AK §2-3)
* ------------------------------------------------------------------------
* 施設名がアプリ側照合なのは ★中点を無視する比較が MariaDB に無いから★
* (`lib/facilities/resolve-by-name.ts` 冒頭の実測記録)。
* ★こちらは整数の完全一致★なので、その制約はありません。
* ★真似ているのは「`null` / `[]` の戻り値の契約」と「`in` に渡す形」だけ★です。
*
* ★★施設ごとにループでクエリを撃たないこと★★(★落とし穴14★)—
* 施設マスタは実測12件なので ★1回読んでメモリで解決する★(§6-3)。
*/
import type { MasterIdFilters } from "@/lib/api/master-id";
import { db } from "@/lib/db";
import type { FacilityGroupRef } from "@/lib/types";
/**
* 分類の条件を施設IDの配列に解決する★純粋関数★(DBアクセスを混ぜない)。
*
* @param refs 施設マスタの全行(`db.facilityGroupIndex()` の戻り値)
* @param filters `parseMasterIdFilters()` が検証済みの `groupId` / `customerId`
* @returns
* - ★★`null`★★ … どちらも未指定。= ★分類で絞らない★
* - ★★`[]`★★ … 絞ったが ★1件も当たらなかった★。= ★結果は0件★
* - それ以外 … 当たった施設IDの配列
*
* ★★どちらも未指定のときに `[]` を返さないこと★★ — `[]` は「0件ヒット」の意味なので、
* ★何も絞っていないのに一覧が空になります★。
*
* ★`group_id` の `null`(ホールディングスなし)は `groupId` 指定に当たらない★ —
* API の `group_id` は1以上しか受け付けない(`parseMasterIdParam()` が 0 を 422 にする)ので、
* ★「所属なしの施設だけを引く」経路はそもそも存在しません★(指示書AK §4-1 #3)。
*/
export function facilityIdsMatchingMasters(
refs: readonly FacilityGroupRef[],
filters: MasterIdFilters
): number[] | null {
const { groupId, customerId } = filters;
// ★★`!== null` で分岐すること★★ — `groupId || customerId` と書くと、
// 将来 0 が入り込んだときに「未指定」に化ける(`parseMasterIdParam()` が
// 0 を弾いているのは入口だけで、この関数の契約ではない)
if (groupId === null && customerId === null) return null;
return refs
.filter(
(ref) =>
// ★★AND で畳むこと★★ — 片方が未指定ならその条件だけを素通しする
(groupId === null || ref.group_id === groupId) &&
(customerId === null || ref.customer_id === customerId)
)
.map((ref) => ref.id);
}
/**
* 分類の条件を施設IDの配列に解決する(★API ルートから呼ぶ★)。
*
* ★★どちらも未指定のときは DB を読まずに `null` を返す★★ — 読んでも結果が変わらないので、
* ★「分類を選んでいない検索」で余計な往復を増やさない★
* (`resolveFacilityIdsByName()` が空文字でそうしているのと同じ)。
*
* ★★解決用の読み取りは `deleted_at` で絞らない★★(★罠7★)—
* 理由は `loadFacilityGroupIndex()`(`lib/db/facility-group-source.ts`)の docstring。
* ★`db.facilities()` を流用しないこと★。
*/
export async function resolveFacilityIdsByMasters(
filters: MasterIdFilters
): Promise<number[] | null> {
if (filters.groupId === null && filters.customerId === null) return null;
return facilityIdsMatchingMasters(await db.facilityGroupIndex(), filters);
}