二重予約判定
二重予約判定
Section titled “二重予約判定”Document ID: SRS-RES-005
Parent: SRS-ROOT-001 v0.3
Version: 0.3
Status: Draft
Depends on: SRS-MST-001(スタッフ管理), SRS-MST-002(メニュー管理), SRS-MST-005(設備管理), SRS-CUS-001(顧客登録・検索), SRS-RES-002(予約作成), SRS-RES-004(予約状態遷移、reservation.status の canonical source)
依存される: SRS-RES-002(予約作成、EF-1 / EF-7 / GWT-4 / GWT-5 / GWT-11 / GWT-12), SRS-RES-003(予約編集・移動), SRS-RES-006(オンライン予約受付), SRS-RES-007(複数チャネル予約統合), SRS-PAY-002(キャンセル料)
本書は SRS-ROOT-001 v0.3 に従う。
予約作成・編集・チャネル統合で共通に使える二重予約判定ロジックの契約を固定する。
本家 SALON BOARD は「1予約 = 1スタッフ」「受付可能数で同一スタッフの並列予約を制御」するモデルを採用しており、Phase 1 はこれに準拠する。判定の対象軸(スタッフ/設備/顧客)・時間範囲の算出・受付可能数・race condition 対策・更新時の自己除外を一本化し、呼び出し側(RES-002/003/006/007)が判定ロジックを重複実装しないようにする。
判定そのものは domain 層の純関数 + DB 制約(設備のみ)に寄せ、各呼び出し側の API は warnings / errors を受け取って UI/レスポンスに変換するだけにする。
2. ユーザーストーリー
Section titled “2. ユーザーストーリー”- As a 受付, I want to 同じスタッフの並列予約が上限を超えそうなときにブロックしてほしい, so that 実際に対応できない予約を入れてトラブルにならない
- As a 店長, I want to カラー放置中に同じスタイリストが別の客のカットを受けられるようにしてほしい, so that スタッフの空き時間を有効活用して売上を伸ばせる
- As a 店長, I want to シャンプー台が空いているか自動で判定してほしい, so that 設備配分の手作業が減る
- As a 受付, I want to 同じ顧客の時間が別予約と被るケースは警告だけにしてほしい, so that 親子同伴・掛け持ちの正当ケースが通る
- As a オペレーター, I want to 予約を登録する前に「この時間で空いてるか」を軽く確認できるようにしてほしい, so that 無駄な保存エラーを避けられる
3. ユースケース
Section titled “3. ユースケース”3.1 呼び出しコンテキスト
Section titled “3.1 呼び出しコンテキスト”| ID | 呼び出し元 | タイミング | 入力 | 出力 |
|---|---|---|---|---|
| UC-1 | SRS-RES-002 予約作成 | 保存時 | 新規予約候補(staff/equipment/customer/時間枠) | errors + warnings |
| UC-2 | SRS-RES-003 予約編集・移動 | 保存時 | 予約候補 + exclude_reservation_id | errors + warnings |
| UC-3 | UI プレビュー | ダイアログ内の時間変更のたび | 予約候補 | warnings のみ(保存前は errors も警告扱い) |
| UC-4 | SRS-RES-006 LIFF 予約 | 空き枠表示/確定時 | 時間候補 | 空きかどうかのブール+理由コード |
| UC-5 | SRS-RES-007 チャネル統合 | 取り込み時 | 取り込み候補 | 取り込み可否+衝突リスト |
3.2 主シナリオ(UC-1 / 2)
Section titled “3.2 主シナリオ(UC-1 / 2)”- 呼び出し側が判定入力(時間枠・スタッフ・設備明細・顧客ID・除外ID)を組み立てる
- domain 関数
checkReservationConflicts(input)を呼ぶ - 判定結果
{ errors: ConflictError[], warnings: ConflictWarning[] }が返る - 呼び出し側は
errorsがあれば保存しない(RES-002 の EF-1 / EF-7 経路、409 返却)warningsのみなら UI に警告表示、ユーザー確認(ack_*フラグ)後に保存続行
- DB INSERT 時、設備の EXCLUDE 制約が最終関門として働く(race condition で判定後に別トランザクションが先に挿入したケースを拾う)
- スタッフ並列上限は SELECT FOR UPDATE で排他制御(§7.7)
3.3 代替フロー
Section titled “3.3 代替フロー”- AF-1. 自己除外:編集時は
exclude_reservation_idを渡す。自分自身の既存行は判定対象外 - AF-2. 指名なし予約:
staff_id = NULLの予約はスタッフ判定の対象外(§7.3) - AF-3. 終端・論理削除予約:終端ステータスまたは
deleted_at IS NOT NULLの予約は判定対象外。具体的な値域は SRS-RES-004 §7.8 を canonical source として参照(status IN ('tentative','confirmed','in_service','service_completed') AND deleted_at IS NULLが判定対象) - AF-4. プレビュー:UC-3 では error も warning として返す(保存前なので区別する意味が薄い)
3.4 例外フロー
Section titled “3.4 例外フロー”- EF-1. DB EXCLUDE 制約で衝突(設備):アプリ層判定を通り抜けたが制約で弾かれた場合、Postgres が
23P01を返す → API 層でRESERVATION.DOUBLE_BOOKED_EQUIPMENTに変換 - EF-2. 不整合な入力:domain 関数の入力バリデーションで
InvalidInputを投げる(422)
4. UI仕様
Section titled “4. UI仕様”注記:親SRS §7.12 に従い最低限の粒度。UI は呼び出し側 SRS(RES-002/003)で定義され、本SRS は呼び出し側に返す warning/error の意味論のみを定める。
4.1 返却される warning / error の UI 表現ガイド
Section titled “4.1 返却される warning / error の UI 表現ガイド”| コード | 種別 | UI ガイド |
|---|---|---|
RESERVATION.STAFF_CONCURRENT_LIMIT | error | ダイアログ内インラインエラー。衝突した予約時刻・受付可能数を示す |
RESERVATION.DOUBLE_BOOKED_EQUIPMENT | error | 同上、設備名で示す |
RESERVATION.CUSTOMER_DOUBLE_BOOKED | warning | 確認ダイアログ(ack_customer_double_booked チェック) |
RESERVATION.STAFF_OFF_SHIFT | warning | インライン注意表示(保存ブロックしない) |
RESERVATION.OUT_OF_BUSINESS_HOURS | warning / error | permission 依存(RES-002 EF-2) |
5. API仕様
Section titled “5. API仕様”5.1 domain 関数(契約ファースト)
Section titled “5.1 domain 関数(契約ファースト)”本SRS の一次成果物は packages/domain/reservation の純関数。API エンドポイントは 5.2 のプレビュー用のみ。RES-002/003 の POST/PUT は内部で同関数を呼ぶ。
// packages/domain/reservation/conflict.ts(方向性)export type ConflictInput = { storeId: Uuid; candidate: { startsAt: Instant; // 予約の開始 endsAt: Instant; // 予約の終了(顧客表示値、バッファ含まず) bufferMinutesAfter: number; // バッファ(メニュー最大値のスナップショット) staffId: Uuid | null; // null = 指名なし → スタッフ判定スキップ customerId: Uuid; equipmentSegments: EquipmentSegment[]; // 設備占有区間 }; excludeReservationId?: Uuid; // 編集時の自己除外 maxConcurrentPerStaff: number; // store_settings から引き込む existingStaffReservations: ExistingReservation[]; // repo から引き込んだスタッフ関連予約 existingCustomerReservations: ExistingReservation[]; // 顧客関連予約 // 設備は DB EXCLUDE 制約で判定するため existing は不要};
export type EquipmentSegment = { equipmentId: Uuid; startsAt: Instant; endsAt: Instant;};
export type ExistingReservation = { id: Uuid; startsAt: Instant; endsAt: Instant; bufferMinutesAfter: number;};
export type ConflictOutput = { errors: ConflictError[]; warnings: ConflictWarning[];};
// 純関数:IO なし、時刻計算とカウント判定のみexport function checkReservationConflicts(input: ConflictInput): ConflictOutput;Instantは Temporal.Instant(または Temporal polyfill)。タイムゾーン依存の計算はしないexisting*は呼出側が事前に repo 経由で引き込む。domain 層は IO を持たない(親SRS モノレポ方針、DDD軽量版)- 設備は EXCLUDE 制約で DB 側に寄せるため、domain 関数の判定対象外(INSERT 時に制約がキャッチ)
5.2 プレビュー API(UC-3 / UC-4 用)
Section titled “5.2 プレビュー API(UC-3 / UC-4 用)”| Method | Path | 用途 | 必要 permission |
|---|---|---|---|
| POST | /api/admin/reservations/conflict-check | 保存前の空き確認 | admin:reservation:create |
| POST | /api/portal/reservations/conflict-check | LIFF の空き枠判定(UC-4) | portal 認証 |
Request:RES-002 の POST と同形(ただし status・notes・override_business_hours などの作成専用項目は無視)。
Response:
{ data: { bookable: boolean; // errors が空か errors: ConflictError[]; warnings: ConflictWarning[]; }}5.3 エラー / 警告コード
Section titled “5.3 エラー / 警告コード”| code | 種別 | 意味 |
|---|---|---|
RESERVATION.STAFF_CONCURRENT_LIMIT | error | スタッフの並列予約数が受付可能数を超過 |
RESERVATION.DOUBLE_BOOKED_EQUIPMENT | error | 設備の時間重複 |
RESERVATION.CUSTOMER_DOUBLE_BOOKED | warning | 同一顧客の時間重複(§7.5) |
RESERVATION.STAFF_OFF_SHIFT | warning | シフト外(保存ブロックしない、RES-002 EF-3) |
RESERVATION.OUT_OF_BUSINESS_HOURS | warning / error | 営業時間外・定休日(RES-002 EF-2、permission 依存) |
各エラー/警告は detail に衝突相手の reservation_id / staff_id / equipment_id / 時刻区間 / 現在並列数を含める。UI で「どの予約と被ったか」「あと何枠か」を表示するために必要。
6. データモデル影響
Section titled “6. データモデル影響”6.1 スタッフ判定:DB 制約なし(アプリ層のみ)
Section titled “6.1 スタッフ判定:DB 制約なし(アプリ層のみ)”受付可能数(同一スタッフの並列上限 N)を EXCLUDE 制約では表現できない(EXCLUDE は「重複を許さない」ものであり「N 件まで許す」を表現できない)。スタッフ判定はアプリ層のカウントベース判定 + SELECT FOR UPDATE で排他制御(§7.7)とする。
6.2 設備判定:EXCLUDE 制約
Section titled “6.2 設備判定:EXCLUDE 制約”reservation_equipment に EXCLUDE 制約を張る。設備は「1台を同時刻に2予約が使うことはない」ため EXCLUDE が効く。
ALTER TABLE reservation_equipment ADD CONSTRAINT no_equipment_overlap EXCLUDE USING GIST ( store_id WITH =, equipment_id WITH =, tstzrange(starts_at, ends_at, '[)') WITH && ) WHERE (is_active = true);キャンセル済み予約は is_active = false にして EXCLUDE 対象から外す。
6.3 顧客重複チェック(制約ではなくクエリ)
Section titled “6.3 顧客重複チェック(制約ではなくクエリ)”同一顧客の時間重複は基本許可(RES-002 §7.8)のため DB 制約は置かない。domain 関数内で、以下クエリ相当の existingCustomerReservations から判定:
SELECT id, starts_at, ends_at, buffer_minutes_afterFROM reservationWHERE store_id = :storeId AND customer_id = :customerId AND status IN ('tentative','confirmed','in_service','service_completed') -- 進行系のみ、SRS-RES-004 §7.8 canonical AND deleted_at IS NULL AND (id <> :excludeReservationId OR :excludeReservationId IS NULL) AND tstzrange(starts_at, ends_at, '[)') && tstzrange(:candStart, :candEnd, '[)')6.4 受付可能数の保持
Section titled “6.4 受付可能数の保持”store_settings に以下を追加:
| 列 | 型 | デフォルト | 意味 |
|---|---|---|---|
max_concurrent_reservations_per_staff | smallint NOT NULL | 1 | スタッフ1名の同時刻最大予約数 |
Phase 1 は店舗共通の1値。スタッフ個別の上書き(staff.max_concurrent_override)は Phase 2 で検討。
6.5 マイグレーション計画
Section titled “6.5 マイグレーション計画”- BC-RES 初回マイグレーション(RES-002 と同一)で以下を含める:
reservation_equipment.is_active列- 設備 EXCLUDE 制約
no_equipment_overlap
store_settings.max_concurrent_reservations_per_staffの追加責務は SRS-TEN-001 v0.3 が canonical source(BC-TEN マイグレーションで作成済み)。本SRSではマイグレーション責務を持たない(v0.3 で M-B 対応)- Drizzle のマイグレーションファイルは
packages/db/migrations配下に生成(親SRS §8 技術スタック)
7. 業務ルール
Section titled “7. 業務ルール”7.1 判定対象の3軸
Section titled “7.1 判定対象の3軸”| 軸 | 対象 | ブロック/警告 | 実装 |
|---|---|---|---|
| スタッフ | reservation.staff_id が一致する予約を時間重複でカウント | error(受付可能数超過時) | アプリ層 + SELECT FOR UPDATE |
| 設備 | reservation_equipment.equipment_id が一致 | error(ブロック) | DB EXCLUDE |
| 顧客 | reservation.customer_id が一致 | warning(許可) | アプリ層クエリ |
7.2 判定対象の時間範囲
Section titled “7.2 判定対象の時間範囲”- スタッフ軸:
reservation.starts_at 〜 reservation.ends_at + reservation.buffer_minutes_after- バッファはスタッフ拘束時間に含める(RES-002 §7.2 と整合:表示 ends_at にはバッファを含めないが、判定では加算)
- 候補側(新規予約)も同様にバッファ込みの区間で判定
- 設備軸:
reservation_equipment.starts_at 〜 ends_at(設備占有は独立時間枠、バッファは関係しない) - 顧客軸:
reservation.starts_at 〜 reservation.ends_at(表示値、バッファ含めない)。同伴・掛け持ちはバッファ時間帯に被っても実害がないため
区間の境界は半開区間 [)(開始時刻含む、終了時刻含まない)。11:00-12:00 と 12:00-13:00 は重複しない。
7.3 スタッフ判定のロジック(受付可能数ベース)
Section titled “7.3 スタッフ判定のロジック(受付可能数ベース)”candidateRange = [candidate.startsAt, candidate.endsAt + candidate.bufferMinutesAfter)
overlapping = existingStaffReservations .filter(r => r.id !== excludeReservationId) .filter(r => rangeOverlaps( candidateRange, [r.startsAt, r.endsAt + r.bufferMinutesAfter) ))
if overlapping.length >= maxConcurrentPerStaff: → error STAFF_CONCURRENT_LIMIT具体例(受付可能数 = 2、田中の予約状況):
既存予約 A: 13:00-14:30 (カット+カラー, buffer 10min) → 拘束 13:00-14:40既存予約 B: 14:00-14:30 (カット, buffer 0min) → 拘束 14:00-14:30 (カラー放置中に別客カット)
新規候補 C: 14:00-14:30 (カット, buffer 0min) → 拘束 14:00-14:30
14:00-14:30 の区間で: A の拘束区間 [13:00, 14:40) と重複 → カウント 1 B の拘束区間 [14:00, 14:30) と重複 → カウント 2 計 2 件 >= 受付可能数 2 → ❌ STAFF_CONCURRENT_LIMIT
新規候補 D: 14:35-15:00 (カット, buffer 0min) → 拘束 14:35-15:00
14:35-15:00 の区間で: A の拘束区間 [13:00, 14:40) と重複 → カウント 1 B の拘束区間 [14:00, 14:30) と重複なし 計 1 件 < 受付可能数 2 → ✅ OK7.4 スタッフ未指名の扱い
Section titled “7.4 スタッフ未指名の扱い”reservation.staff_id = NULL(指名なし)の予約はスタッフ判定の対象外。
- 指名なし予約が何件重なっても、スタッフ軸ではブロックしない(割当前なので拘束対象のスタッフがいない)
- 後日スタッフを割り当てた時点(RES-003 の編集フロー)で改めて判定が走る
7.5 顧客重複の警告仕様
Section titled “7.5 顧客重複の警告仕様”- 同一顧客の時間重複は warning として返す(エラーではない)
- 呼出側は
warningsにCUSTOMER_DOUBLE_BOOKEDが含まれていたらユーザー確認を経てack_customer_double_booked=trueを立てて再送(RES-002 §7.8 の ack フロー) - 監査ログには ack=true の痕跡を残す(RES-002 §7.7 の
customer_double_bookedフラグ)
7.6 終端・論理削除の除外
Section titled “7.6 終端・論理削除の除外”- 判定対象外の条件は SRS-RES-004 §7.8 を canonical source として参照する。具体的には終端ステータス(
paid/declined/cancelled_by_customer/cancelled_by_store/no_show/expired)またはdeleted_at IS NOT NULLの予約はすべて判定対象外 - スタッフ・顧客軸:
existingStaffReservations/existingCustomerReservationsのクエリで進行系のみ抽出(§6.3) - 設備軸:
reservation_equipment.is_active = falseで EXCLUDE 対象外。is_activeの更新責務は SRS-RES-004 §7.9 で確定(キャンセル系遷移ハンドラ内で UPDATE)
7.7 race condition 対策
Section titled “7.7 race condition 対策”スタッフ軸(アプリ層カウント):
並列の2リクエストが同時に「あと1枠空いてる」と判断して両方 INSERT するケースを防ぐ。
-- 予約作成トランザクション内、判定前に実行SELECT id FROM staff WHERE store_id = :storeId AND id = :staffId FOR UPDATE;staff 行をロックすることで、同一スタッフへの並行 INSERT がシリアライズされる。トランザクションが短い(予約作成 p95 300ms)ので、待ち時間は実用上問題ない。
設備軸(DB EXCLUDE 制約):制約自体が race-safe。追加の排他制御は不要。
顧客軸(警告のみ):race 許容。ack 後の二重送信で同じ警告が再度返ってもユーザー体験的に問題なし。
7.8 自己除外(編集時)
Section titled “7.8 自己除外(編集時)”excludeReservationIdが指定された場合- スタッフ・顧客軸:
existingクエリでreservation.id <> :excludeReservationIdで除外 - 設備軸:既存の
reservation_equipment行を UPDATE(行 id 維持)するフローで、EXCLUDE 制約が自動的に自行を除外する
- スタッフ・顧客軸:
- RES-003(編集)では設備の
reservation_equipmentの行 id を維持する実装が必要。本節を参照
7.9 シフト外・営業時間外の判定位置
Section titled “7.9 シフト外・営業時間外の判定位置”- これらは本SRS の判定対象外(呼出側が別途判定)。本SRS は「時間区間の重複・並列数」に限定
- ただし呼出側の利便性のため、同じ domain 層に
checkBusinessHours(storeId, range)/checkShiftCoverage(staffId, range)を兄弟として置く。本SRSでは契約のみ言及、詳細は SRS-MST-003 / SRS-MST-004 で定義
7.10 判定パフォーマンス
Section titled “7.10 判定パフォーマンス”- 単一予約作成あたりの判定で読み込む
existingは当該日 ± 1日に絞る(跨日予約は美容室ではレアだが cutover 日対応) - インデックス:
reservation(store_id, staff_id, starts_at)btree — スタッフ並列判定reservation(store_id, customer_id, starts_at)btree — 顧客重複判定
- 目標:判定処理自体は p95 30ms 以下(予約作成 API 全体の p95 300ms のうち)
8. 非機能要件
Section titled “8. 非機能要件”親SRS §6 に従う。特記事項:
- 性能:§7.10 参照
- 可観測性:判定結果(error / warning の件数とコード分布)を構造化ログに出し、“許可された警告” の比率を観測可能にする。顧客重複 warning の ack 率が高すぎる場合、警告 UI が無視されているシグナルとして Phase 3 で検討
9. セキュリティ・認可
Section titled “9. セキュリティ・認可”9.1 使用する permission キー
Section titled “9.1 使用する permission キー”| key | 用途 | プリセット初期値 |
|---|---|---|
admin:reservation:create | プレビュー API(admin)利用 | RES-002 と共通 |
- プレビュー API は作成権限と同一の permission で守る(作成できない人に空き情報を見せる意味が薄い)
- LIFF(portal)側は portal 認証のみで通す。店舗公開情報の範囲内
9.2 RLS
Section titled “9.2 RLS”本SRS で新規テーブルは作成しない。reservation / reservation_equipment の RLS は RES-002 マイグレーションで ENABLE + FORCE 済み。
10. 受け入れ基準(Given-When-Then)
Section titled “10. 受け入れ基準(Given-When-Then)”- GWT-1 並列上限=1 で重複ブロック:Given 受付可能数=1、スタッフ A が 10:00-11:00 の予約を持つ / When 同スタッフで 10:30-11:30 の予約を作成 / Then
RESERVATION.STAFF_CONCURRENT_LIMITが返る - GWT-2 並列上限=2 で2本目は通る:Given 受付可能数=2、スタッフ A が 13:00-14:30 (buffer 10min) の予約を持つ / When 同スタッフで 14:00-14:30 を作成 / Then 成功(カラー放置中に別客カット)
- GWT-3 並列上限=2 で3本目はブロック:Given 受付可能数=2、スタッフ A が 13:00-14:30 と 14:00-14:30 の予約を持つ / When 同スタッフで 14:00-14:20 を作成 / Then
RESERVATION.STAFF_CONCURRENT_LIMIT - GWT-4 バッファ込みで判定:Given 受付可能数=1、スタッフ A の 10:00-11:00 予約 (buffer 10min) / When 同スタッフで 11:05-11:30 を作成 / Then ブロック(拘束区間は 11:10 まで)
- GWT-5 バッファ後は通る:Given 受付可能数=1、スタッフ A の 10:00-11:00 予約 (buffer 10min) / When 同スタッフで 11:10-12:00 を作成 / Then 成功(半開区間
[)) - GWT-6 指名なしはスキップ:Given スタッフ A の 10:00-11:00 予約あり / When
staff_id = NULL(指名なし)で 10:30-11:30 を作成 / Then 成功
- GWT-7 設備重複ブロック:Given シャンプー台 1 が 10:00-10:30 占有 / When 同設備を 10:15-10:45 で指定 / Then
RESERVATION.DOUBLE_BOOKED_EQUIPMENT - GWT-8 設備の接続境界:Given シャンプー台 1 が 10:00-10:30 占有 / When 同設備を 10:30-11:00 で指定 / Then 成功(半開区間)
キャンセル・除外
Section titled “キャンセル・除外”- GWT-9 終端は無視:Given スタッフ A の 10:00-11:00 予約が
status=cancelled_by_store/ When 同スタッフで 10:30-11:30 を作成(受付可能数=1) / Then 成功(paid/expired/declined/cancelled_by_customer/no_showでも同様、SRS-RES-004 §7.8) - GWT-10 自己除外(編集):Given 予約 R(10:00-11:00)が存在 / When R を
excludeReservationId=R.idで 10:30-11:30 に移動 / Then 自分自身は重複扱いにならず成功
- GWT-11 顧客重複は警告:Given 顧客 C の予約 10:00-11:00 が存在 / When 同顧客で 10:30-11:30 の予約を
ack_customer_double_booked=true付きで作成 / Then 成功、レスポンスにwarnings: [CUSTOMER_DOUBLE_BOOKED]
プレビュー・race condition
Section titled “プレビュー・race condition”- GWT-12 プレビュー API(bookable=true):Given 空き時間 / When
POST /api/admin/reservations/conflict-check/ Then{ bookable: true, errors: [], warnings: [] } - GWT-13 プレビュー API(bookable=false):Given 設備衝突あり / When プレビュー API / Then
{ bookable: false, errors: [DOUBLE_BOOKED_EQUIPMENT] } - GWT-14 race condition(スタッフ):Given 受付可能数=1、2つのリクエストが同時に同スタッフ同時刻を INSERT / When 片方がコミット / Then もう片方は SELECT FOR UPDATE で待たされ、判定時点で「上限到達」となりエラー
- GWT-15 race condition(設備):Given 2つのリクエストが同時に同設備同時刻を INSERT / When 片方がコミット / Then もう片方は EXCLUDE 制約で失敗し
RESERVATION.DOUBLE_BOOKED_EQUIPMENTに変換される
テナント分離
Section titled “テナント分離”- GWT-16 RLS テナント分離:Given 店舗 X のセッション / When 店舗 Y の予約時間と被る時間で判定 / Then 店舗 Y の予約は
existingに入らず「空き」と判定される
11. テスト計画
Section titled “11. テスト計画”- Unit(
packages/domain/reservation/conflict.ts):純関数の table-driven test で以下を網羅- スタッフ軸:受付可能数 1/2/3 のケース × 重複あり/なし/境界値(バッファ0・バッファあり・半開境界)/ 指名なし / キャンセル
- 顧客軸:重複あり(warning)/ 自己除外 / キャンセル
- 入力バリデーション
- Integration(
apps/api+ Testcontainers Postgres):- RES-002 からの呼び出し経路で GWT-1〜16 を網羅
- EXCLUDE 制約の race condition テスト(2トランザクション同時 INSERT で片方が失敗することを確認)
- SELECT FOR UPDATE の排他テスト
- RLS 分離テスト(GWT-16)
- Contract:プレビュー API の zod schema を FE/BE 間で照合
12. 関連ジョブ(graphile-worker)
Section titled “12. 関連ジョブ(graphile-worker)”- なし(判定はリクエスト同期処理)
13. Open Questions
Section titled “13. Open Questions”| # | 内容 | 締切の目安 |
|---|---|---|
| OQ-RES-005-02 | Phase 2 で明細レベルのスタッフ割当を導入する場合の判定モデル変更。予約レベルの staff_id + 受付可能数ベースから、明細区間ベースへの切替が必要。domain 関数のインタフェースに影響 | Phase 2 企画時 |
| OQ-RES-005-03 | スタッフ個別の受付可能数上書き(staff.max_concurrent_override)を Phase 2 で入れるか。新人=1・ベテラン=3 のような運用ニーズ | Phase 2 企画時 |
| OQ-RES-005-04 | is_active=false の reservation_equipment 行が蓄積した場合の GIST インデックス肥大化。定期 VACUUM か物理削除ジョブか | Phase 3 運用時 |
解消済み(v0.2 で決着):
v0.1 OQ:→ 不要。本家互換モデル採用により、スタッフ判定はreservation_menuにstarts_at/ends_atを持たせるかreservationレベルで完結。reservation_menuへのスキーマ追加なしv0.1 OQ: EXCLUDE 制約でスタッフ判定するか→ 不採用。受付可能数(N件まで許可)は EXCLUDE では表現できないため、アプリ層カウント + SELECT FOR UPDATE に決定v0.1 OQ: 並行施術(同一予約内で別スタッフが同時作業)→ Phase 1 スコープ外。本家互換で「1予約=1スタッフ、放置中は別予約として並列」で運用
解消済み(v0.3 で決着):
OQ-RES-005-01→ SRS-RES-004 §7.9 が canonical。状態遷移ハンドラ内で更新する責務を本SRSから RES-004 に委任reservation_equipment.is_activeの更新タイミング
14. 変更履歴
Section titled “14. 変更履歴”| Version | Date | Author | Change |
|---|---|---|---|
| 0.1 | 2026-04-22 | yudai | 初版起票(Draft)。明細レベルのスタッフ割当前提で EXCLUDE 制約ベースの判定を設計 |
| 0.2 | 2026-04-23 | yudai | 本家 SALON BOARD 互換へモデル変更。スタッフ判定を予約レベルの staff_id × 受付可能数(カウントベース)に簡素化。EXCLUDE 制約はスタッフ軸では不採用(N 件許可を表現不能)、設備軸のみ維持。reservation_menu へのスキーマ追加(starts_at/ends_at/is_active)を撤回。store_settings.max_concurrent_reservations_per_staff を追加。race condition 対策を SELECT FOR UPDATE に変更。§7.3 に受付可能数ベースの判定ロジック・具体例を記載 |
| 0.3 | 2026-04-25 | yudai | SRS-RES-004 v0.2 と同期改訂。reservation.status の canonical source を SRS-RES-004 §7.8 に委譲。判定対象の値域を tentative/confirmed/in_service/service_completed の進行系4状態 + deleted_at IS NULL に確定(旧 cancelled / no_show の語彙を新コードに置換)。OQ-RES-005-01(is_active 更新タイミング)を SRS-RES-004 §7.9 の確定により解消。§3.3 AF-3、§6.3 クエリ、§7.6、GWT-9 を更新 |