Newer
Older
client_dwatch / lib / masters / query.ts
/**
 * `GET /groups` / `GET /customers` で使うクエリ処理(分類マスタ。指示書AK 段階1)。
 * 仕様の正は `d-watch-api.yaml` の /groups と /customers。
 *
 * `db.groups()` / `db.customers()` は全件を返すだけなので、絞り込みと並べ替えはここで組み立てる
 * (`lib/schedules/query.ts` と同じ分担。`lib/db/` はデータ取得だけを担う)。
 *
 * ★★ページングしない★★(指示書AK §4-1 #1)—
 *   セレクトリストの選択肢を返すエンドポイントなので、`page` / `per_page` を受け取らない。
 *   既定の `per_page=50` で切ると ★51件目以降の屋号がセレクトから黙って消える★。
 *   `meta` は返す(`CLAUDE.md` §9.3「一覧APIは必ず meta を返す」)。意味は
 *   `/reports/facility-performance` と同じ「page を持たない一覧」の書き方に合わせてある:
 *     `meta.total` = 絞り込み後の全件数 / `meta.page` = 常に 1 /
 *     `meta.per_page` = そのページに入っている件数(= `total`。1ページに全件返すため)
 *
 * ★純粋関数にすること★(`CLAUDE.md` §9.4)— DBアクセスやログ出力を混ぜない。
 */
import { parseMasterIdParam } from "@/lib/api/master-id";
import type { QueryParseResult } from "@/lib/api/query-result";
import type { Customer, Group, ListResponse } from "@/lib/types";

export interface CustomerQuery {
  /** ホールディングスで絞り込み。★`null` = 絞らない★ */
  groupId: number | null;
}

/**
 * `/customers` のクエリパラメータをパースする。
 *
 * ★検証は `parseMasterIdParam()` の1本に任せる★(`lib/api/master-id.ts`)—
 *   ★`Number.parseInt()` で書き直さないこと★(`1abc` が `1` として通る。D-123)。
 */
export function parseCustomerQuery(
  searchParams: URLSearchParams
): QueryParseResult<CustomerQuery> {
  const groupId = parseMasterIdParam(searchParams.get("group_id"), "group_id");
  if (!groupId.ok) return groupId;
  return { ok: true, query: { groupId: groupId.query } };
}

/**
 * ホールディングスで絞り込む。
 *
 * ★★`null`(未指定)と「該当0件」を取り違えないこと★★ —
 *   未指定のときだけ全件を返す。実在しない `group_id` を指定されたら ★空配列★ を返す
 *   (404 にしない。「該当なし」は正常な結果。指示書AK §9-3 の 6-9 と同じ考え方)。
 *
 * ★`group_id: null`(ホールディングスなし)の屋号だけを引く手段は用意しない★
 *   (指示書AK §4-1 #3)。0 を API に漏らさないため、
 *   「未設定を明示する値」をクエリパラメータに作らない。
 */
export function filterCustomersByGroup(
  items: Customer[],
  groupId: number | null
): Customer[] {
  if (groupId === null) return items;
  return items.filter((c) => c.group_id === groupId);
}

/**
 * id の昇順(`d-watch-api.yaml` に明記。D-052 / D-055 と同じく契約に書く)。
 *
 * ★名前順にしないこと★ — 日本語の並び順はDBの照合順序とJSの `localeCompare` で
 *   一致しないため、`USE_FIXTURES` の true/false で順序が変わりうる。
 *   セレクトリストの並びが環境で変わると「どちらが正しいか」を誰も決められない。
 */
export function sortGroups(items: Group[]): Group[] {
  return [...items].sort((a, b) => a.id - b.id);
}

/** `sortGroups()` と同じ規則(id 昇順) */
export function sortCustomers(items: Customer[]): Customer[] {
  return [...items].sort((a, b) => a.id - b.id);
}

/**
 * ページングしない一覧のレスポンスを組み立てる。
 *
 * ★`items` を切らない★ — セレクトリストは全件が要る(このファイル冒頭)。
 * ★`meta` は返す★ — `CLAUDE.md` §9.3。
 */
export function masterListResponse<T>(items: T[]): ListResponse<T> {
  return {
    items,
    meta: { total: items.length, page: 1, per_page: items.length },
  };
}