コンテンツにスキップ

予約作成(管理画面 手動)

Document ID: SRS-RES-002 Parent: SRS-ROOT-001 v0.5 Version: 0.5 Status: Implemented (API + Web UI) Last Updated: 2026-05-06 Depends on: SRS-TEN-001 v0.4, SRS-TEN-002 v0.2, SRS-TEN-003 v0.2, SRS-MST-001 v0.6, SRS-MST-002 v0.2, SRS-MST-003 v0.2, SRS-MST-004 v0.2, SRS-MST-005 v0.2, SRS-CUS-001 v0.2, SRS-RES-001 v0.2, SRS-RES-004 v0.3, SRS-RES-005 v0.4

本書は SRS-ROOT-001 v0.5 に従う。


管理画面から、オペレーターが顧客・メニュー・スタッフ・日時を指定して 1 件の予約を作成できるようにする。Phase 1 ではチャネルは管理画面(手動)のみ。LIFF / 電話取り込み / HPB 連携は後続 SRS(SRS-RES-006 / 007)に委譲する。

設備割当は menu_equipment_requirement(equipment_group_id, quantity) から 自動割当する。reservation.code(人間可読 UNIQUE)、channel_codereservation_menu_lines.menu_name_snapshot を Phase 1 から保持する。


  • As a 受付, I want to カレンダーの空き時間をクリックして予約を素早く登録する, so that 電話対応中に顧客を待たせずに記録できる
  • As a スタイリスト, I want to 自分の予約枠に常連顧客を直接入れる, so that 管理者を介さず再来店を確定できる
  • As a 店長, I want to スタッフ未指名でも予約を受け付けて後で割り当てる, so that フリー客を逃さない

3.1 主シナリオ(管理画面からの通常作成)

Section titled “3.1 主シナリオ(管理画面からの通常作成)”
  1. オペレーターがカレンダー画面で空きセル(スタッフ × 時間)をクリック
  2. 予約作成ダイアログが開き、選択したスタッフ・開始時刻が初期値に入る
  3. 顧客を検索して選択(既存)、または「新規顧客を追加」で氏名・カナを入力(CUS-001)
  4. メニューを 1 つ以上選択(各メニューは所要時間・料金・equipment_group 要件を保持)
  5. 所要時間の合計から終了時刻が自動計算
  6. 備考(任意)を入力
  7. 状態「確定」で「登録」
  8. システムが二重予約チェック(スタッフ並列上限・設備重複)を実施(SRS-RES-005 v0.4)
  9. 設備自動割当: menu_equipment_requirement(equipment_group_id, quantity) ごとに、group 内の空き concrete equipment_idquantity 個選び reservation_equipment_assignments に INSERT
  10. OK なら reservations + reservation_menu_linesmenu_name_snapshot 付き)+ reservation_equipment_assignments を 1 トランザクションで作成
  11. reservation.code をアプリ層で短コード生成し UNIQUE(store_id, code) 衝突時はリトライ
  12. operator_action_log(action='reservation.create')actor_kind='operator' で記録(diff に force_business_hours / force_staff_concurrency フラグを含める)
  • AF-1 フリー(指名なし)で作成: reservation.staff_id = NULL で保存可
  • AF-2 新規顧客をその場で作成: 同一トランザクションで CUS-001 の customer も INSERT
  • AF-3 仮予約として保存: 初期状態を tentative で登録、tentative_expires_at をスナップショット
  • AF-4 顧客詳細画面から起動: 日時はその場で選択、それ以外は主シナリオと同じ
  • EF-1 並列予約上限超過: 同一スタッフの同時刻予約数が store_settings.max_concurrent_reservations_per_staff を超 → 409 RESERVATION.STAFF_CONCURRENT_LIMITforce_staff_concurrency permission 持ちは押し通し可)
  • EF-2 営業時間外・定休日: force_business_hours permission を持つ者は warning 経由で保存可、持たない者は 422 RESERVATION.OUT_OF_BUSINESS_HOURS
  • EF-3 シフト外: 警告のみ、保存可(RES-005 §7.1)
  • EF-4 担当不可メニュー(menu_staff_eligibility ホワイトリスト違反): 422 RESERVATION.MENU_NOT_ELIGIBLE_FOR_STAFF
  • EF-5 権限不足: 403
  • EF-6 楽観ロック衝突: 409 RESERVATION.CONFLICT
  • EF-7 設備自動割当不足: 422 RESERVATION.NO_EQUIPMENT_AVAILABLE(group 内に空きなし)
  • EF-8 設備 EXCLUDE 制約違反(race 競合): 409 RESERVATION.DOUBLE_BOOKED_EQUIPMENTforce_staff_concurrency でも bypass 不可
  • EF-9 顧客時間重複: warning のみ、ack_customer_double_booked=true で保存可

カレンダー上で発火する予約作成ダイアログ。1 顧客 1 予約を 1 画面で完結させる。

  • 担当スタッフセレクタ(1 名選択 or 「指名なし」)
  • 顧客セレクタ(既存検索 / 新規登録の切替)
  • メニューセレクタ(複数選択可)
  • 日時セレクタ(開始・終了の自動計算)
  • 状態セレクタ(confirmed / tentative
  • 備考フィールド
  • 設備は表示のみ(自動割当結果を表示)
  • 保存ボタン / キャンセルボタン
  • force* 確認ダイアログ(営業時間外、staff 並列超過時)
  • 顧客: 選択必須(新規登録モードでは name + name_kana 必須)
  • メニュー: 1 件以上
  • 開始時刻: Asia/Tokyo で有効な timestamptz
  • 終了時刻: 開始時刻より後
  • 担当スタッフ: 指定時は全選択メニューの menu_staff_eligibility を満たすこと

MethodPath用途必要 permission
POST/api/admin/reservations予約作成admin:reservation:create

Request:

  • store_id はセッションから解決(body に含めない)
  • customer: { kind: 'existing', id: uuid } | { kind: 'new', name, name_kana, phone?, email? }
  • staff_id?: uuid | null
  • menu_lines: [{ menu_id, duration_minutes?, sort_order }] 1 件以上
  • starts_at: ISO8601
  • ends_at?: ISO8601(省略時は starts_at + SUM(duration_minutes)
  • status: 'confirmed' | 'tentative'
  • notes?: string
  • force_business_hours?: boolean
  • force_staff_concurrency?: boolean
  • ack_customer_double_booked?: boolean

Response(成功):

  • data.reservation.{id, code, channel_code, status, version}
  • data.reservation_menu_lines[].{id, menu_id, menu_name_snapshot, duration_minutes, price_*, tax_*}
  • data.reservation_equipment_assignments[].{id, equipment_id, starts_at, ends_at, is_active}
  • warnings?: [{code, ...}]
  • RESERVATION.STAFF_CONCURRENT_LIMIT (409)
  • RESERVATION.DOUBLE_BOOKED_EQUIPMENT (409、force でも bypass 不可)
  • RESERVATION.OUT_OF_BUSINESS_HOURS (422)
  • RESERVATION.STAFF_OFF_SHIFT (warning)
  • RESERVATION.MENU_NOT_ELIGIBLE_FOR_STAFF (422)
  • RESERVATION.NO_EQUIPMENT_AVAILABLE (422)
  • RESERVATION.CUSTOMER_INVALID (422)
  • RESERVATION.CONFLICT (409)
  • RESERVATION.STAFF_NOT_BOOKABLE (422、MST-001 §6.3.1 連動)

6.1 reservations(業務実体、UUIDv7)

Section titled “6.1 reservations(業務実体、UUIDv7)”

主要列:

  • id uuid PK
  • store_id uuid NOT NULL
  • customer_id uuid NOT NULL
  • staff_id uuid NULL
  • code varchar(20) NOT NULL — 店舗内 UNIQUE 表示用(UNIQUE(store_id, code))。アプリ層でランダム生成、衝突時リトライ
  • channel_code varchar(40) NOT NULL DEFAULT 'admin_manual' — 値域は将来拡張、Phase 1 は admin_manual 固定。source 列とは役割が異なる
  • source varchar(40) NOT NULL DEFAULT 'admin_manual' — 内部技術 origin(Phase 1 は admin_manual 固定)
  • status varchar(40) — 10 状態(RES-004)
  • starts_at timestamptz, ends_at timestamptz
  • buffer_minutes_after smallint NOT NULL DEFAULT 0 — メニュー群最大値スナップショット
  • tentative_expires_at timestamptz NULL — RES-004 §6.1 不変条件 CHECK
  • notes text
  • created_by uuid (TEN-002 後 operator_id)
  • version integer NOT NULL DEFAULT 1
  • created_at, updated_at
  • deleted_at timestamptz NULL

CHECK: (status='tentative' AND tentative_expires_at IS NOT NULL) OR (status<>'tentative' AND tentative_expires_at IS NULL)

  • 既存列に加え menu_name_snapshot varchar(200) NOT NULL を追加
  • tax_rate_pct CHECK IN (8,10) は親 §7.16 / TEN-001 v0.4 (0011) で撤去
  • 既存スキーマを維持(0006 + 0009 EXCLUDE 制約は実装済)
  • equipment_id は concrete 設備を指す
  • is_active boolean NOT NULL DEFAULT true

0019_reservation_phase1_finalize.sql で:

  • reservation_menu_lines.menu_name_snapshot 追加(既存行は menu join で backfill)
  • reservations.code 追加(既存行は短コード生成 backfill)
  • reservations.channel_code 追加(DEFAULT で backfill)
  • UNIQUE(store_id, code) 追加
  • operator_action_log.diff GIN index 追加
  • reservation_event_actor_polymorphism CHECK 修正(RES-004 §6.2 連動)
  • force_* permission rename + backfill(RES-005 §9.1 連動)

予約作成時点で MST-002 §7.2 に従い、以下を reservation_menu_lines にコピー:

  • menu_name_snapshot新規必須
  • duration_minutes
  • price_excl_tax, tax_amount, price_incl_tax, tax_rate_pct
  • buffer_minutes_after

7.2 所要時間・終了時刻・バッファ

Section titled “7.2 所要時間・終了時刻・バッファ”
  • reservation.ends_at = starts_at + SUM(reservation_menu_lines.duration_minutes)(バッファ含まず)
  • reservation.buffer_minutes_after = メニュー群の最大値
  • 二重予約判定の時刻は ends_at + buffer_minutes_after
  • オペレーターが ends_at 手動上書き可

7.3 仮予約失効スナップショット(RES-004 v0.3 連動)

Section titled “7.3 仮予約失効スナップショット(RES-004 v0.3 連動)”
  • tentative 作成時: tentative_expires_at = created_at + store_settings.tentative_expire_hours * '1 hour'::interval
  • confirmed 直入り: tentative_expires_at = NULL
  • 設定値変更は既存予約に遡及しない
  • source: 内部技術 origin(debug / observability 用)
  • channel_code: 業務的予約経路 taxonomy(営業分析、HPB 等の Phase 2 拡張用)
  • Phase 1 は両方 admin_manual 固定だが、Phase 2 で channel_code は phone / hpb / liff / walk_in を追加予定
  • menu_equipment_requirement(equipment_group_id, quantity) ごとに、group 内 active equipment(bookable=true AND terminated_at IS NULL)から候補時間帯に EXCLUDE 制約と衝突しない equipment_idquantity 個選ぶ
  • 候補列挙順は display_order ASC, id ASC
  • 不足なら RESERVATION.NO_EQUIPMENT_AVAILABLE 422
  • INSERT 時に EXCLUDE 制約が race 競合を catch(23P01RESERVATION.DOUBLE_BOOKED_EQUIPMENT 409 変換)
  • version は INSERT 時 1、UPDATE で +1
  • UPDATE は WHERE id=? AND version=?、不一致は RESERVATION.CONFLICT
  • reservation.createoperator_action_log に記録、actor_kind='operator'
  • diff JSONB トップレベルキー:
    • force_business_hours: boolean(押し通した場合 true)
    • force_staff_concurrency: boolean(押し通した場合 true)
    • customer_double_booked: boolean(warning ack 経由)
  • GIN index で diff 検索可能
  • 同一スタッフ: 受付可能数で制御(RES-005 §7.3)、force_staff_concurrency で押し通し可
  • 同一設備: EXCLUDE 制約で禁止、force でも bypass 不可
  • 同一顧客: 警告のみ、ack_customer_double_booked で確定

  • 予約作成 API は p95 300ms 以下(親 §6.1)

key用途プリセット初期値
admin:reservation:create予約作成全 role
admin:reservation:force_business_hours営業時間外押し通しowner / manager
admin:reservation:force_staff_concurrencyスタッフ並列上限押し通しowner / manager
admin:customer:createAF-2 新規顧客登録全 role

reservations, reservation_menu_lines, reservation_equipment_assignmentsstore_id RLS ENABLE + FORCE。


10. 受け入れ基準(Given-When-Then)

Section titled “10. 受け入れ基準(Given-When-Then)”
  • GWT-1 主シナリオ: 空きセルクリック → 顧客・メニュー選択 → 登録 → confirmed で作成
  • GWT-2 フリー作成: staff_id = NULL → 「未指名」列に作成
  • GWT-3 新規顧客同時作成: 同一トランザクションで customer + reservation 作成
  • GWT-4 並列予約上限超過: RESERVATION.STAFF_CONCURRENT_LIMIT、保存されない
  • GWT-5 並列予約上限内: 受付可能数 = 2 のスタッフに既存 1 件 → 同時刻 2 件目作成成功
  • GWT-6 営業時間外・権限なし: staff プリセット → RESERVATION.OUT_OF_BUSINESS_HOURS
  • GWT-7 営業時間外・権限あり: manager + force_business_hours=true → 成功、diff.force_business_hours=true
  • GWT-8 料金スナップショット: メニュー単価改定後も既存 reservation_menu_lines.price_* 不変
  • GWT-9 担当不可メニュー: menu_staff_eligibility 違反 → RESERVATION.MENU_NOT_ELIGIBLE_FOR_STAFF
  • GWT-10 RLS テナント分離: 店舗 X セッションで店舗 Y reservation SELECT → 0 件
  • GWT-11 顧客掛け持ち警告: ack_customer_double_booked=true で成功、warnings[]CUSTOMER_DOUBLE_BOOKED
  • GWT-12 設備二重予約: 同一設備同時刻で重なる → RESERVATION.DOUBLE_BOOKED_EQUIPMENT 409
  • GWT-13 バッファ分離: メニュー「カラー 60 分 buffer 10 分」+「カット 30 分 buffer 0」 → ends_at = 13:30buffer_minutes_after=10、判定は 13:40 まで
  • GWT-14 仮予約失効スナップショット: status='tentative' で 14:00 作成 → tentative_expires_at=翌日14:00
  • GWT-15 確定直入り NULL: status='confirmed'tentative_expires_at=NULL
  • GWT-16 設定値変更非遡及: tentative_expire_hours=24→48 → 既存予約 tentative_expires_at 不変
  • GWT-17 code 生成: 作成成功時に reservation.code が短コードで保存される、UNIQUE(store_id, code) 違反時はリトライ
  • GWT-18 menu_name_snapshot: メニュー名変更後も既存 reservation_menu_lines.menu_name_snapshot は旧名を保持
  • GWT-19 設備自動割当: メニュー要件 group A quantity=2、空き 2 台 → 2 台が auto-assign される
  • GWT-20 設備自動割当不足: 空き 1 台のみ → RESERVATION.NO_EQUIPMENT_AVAILABLE
  • GWT-21 force_staff_concurrency で staff 上限超過 → 成功、diff.force_staff_concurrency=true
  • GWT-22 force_staff_concurrency でも equipment overlap → 409 RESERVATION.DOUBLE_BOOKED_EQUIPMENT(bypass 不可)
  • GWT-23 actor_kind: operator_action_log 行が actor_kind='operator' で記録される

  • Unit: 予約長計算、料金スナップショット、status 初期化、tentative_expires_at 計算、equipment auto-assign 純関数
  • Integration: 主シナリオ・AF-1〜4・EF-1〜9・GWT-1〜23 を網羅、CHECK 制約違反テスト含む
  • Contract: zod fixture を FE/BE 間で照合
  • E2E (Phase 2): 予約作成 → 会計の縦串

  • expire-tentative-reservations (cron 5 分) — RES-004 §7.6 / SRS-WRK-001 で詳細

#内容扱い
OQ-RES-002-01code の形式と人間可読性のバランスPhase 1 は短コード、Phase 2 でレジ・帳票連携時に再検討
OQ-RES-002-02channel_code 拡張時の値域確定Phase 2 SRS-RES-007

解消済み:

  • OQ-RES-002-CSV → CSV 一括インポートは Phase 2 へ持越し(CUS-001 OQ-CUS-001-CSV)

VersionDateAuthorChange
0.1〜0.42026-04-22 〜 2026-04-25yudai初版〜v0.4(既存履歴)
0.52026-05-05yudai (with Codex co-design)Parent v0.5 同期。reservation.code / channel_code を追加(ERD §4 整合)。reservation_menu_lines.menu_name_snapshot 必須追加。force_double_bookingforce_staff_concurrency リネーム、equipment EXCLUDE は force でも bypass 不可と明記。menu_eligibility 旧名を menu_staff_eligibility に統一。設備自動割当を equipment_group + quantity 連動に確定。sourcechannel_code の意味分離を §7.4 に明記。GIN index for operator_action_log.diff、actor_kind 採用、migration 番号 0019 確定