コンテンツにスキップ

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

Document ID: SRS-RES-002 Parent: SRS-ROOT-001 v0.3 Version: 0.4 Status: Draft Depends on: SRS-TEN-001(店舗作成、store_settings.tentative_expire_hours 参照), SRS-TEN-002(オペレーター登録・Passkey), SRS-TEN-003(ロールと店舗アサイン), SRS-MST-001(スタッフ管理), SRS-MST-002(メニュー管理), SRS-MST-003(営業時間), SRS-MST-004(シフト), SRS-MST-005(設備管理), SRS-CUS-001(顧客登録・検索), SRS-RES-001(予約カレンダー表示), SRS-RES-004(予約状態遷移、reservation.status の canonical source), SRS-RES-005(二重予約判定)

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


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


  • 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. 顧客を検索して選択(既存)、または「新規顧客を追加」で氏名・連絡先を入力
  4. メニューを 1 つ以上選択(各メニューは所要時間・料金を保持)
  5. 必要に応じて設備(シャンプー台/セット面等)を紐付け
  6. 所要時間の合計から終了時刻が自動計算・表示。手動調整も可
  7. 備考(任意)を入力
  8. 状態「確定」で「登録」
  9. システムが二重予約チェック(スタッフ並列上限・設備重複)を実施(詳細は SRS-RES-005)
  10. OK なら reservation + reservation_menu(+ reservation_equipment)を 1 トランザクションで作成、カレンダーに即時反映
  • AF-1. フリー(指名なし)で作成reservation.staff_id = NULL で保存可。カレンダーの「未指名」列に表示、当日割当
  • AF-2. 新規顧客をその場で作成:同一トランザクションで customer も INSERT。氏名のみ必須(親SRS §7.13 customer_required_fields = ['name']
  • AF-3. 仮予約として保存:初期状態を「仮予約」で登録(電話での取り置き等)
  • AF-4. 顧客詳細画面から起動:日時はその場で選択、それ以外は主シナリオと同じ
  • EF-1. 並列予約上限超過:同一スタッフの同時刻予約数が受付可能数を超える → エラー、保存されない(詳細 SRS-RES-005)
  • EF-2. 営業時間外・定休日:警告表示 → admin:reservation:override_business_hours を持つ者は「承知の上で保存」可、持たない者は保存不可
  • EF-3. シフト外:警告のみ、保存可
  • EF-4. メニュー担当不可スタッフmenu_eligibility 違反):予約に指定されたスタッフが、選択メニューのいずれかの担当資格を持たない場合エラー、保存不可
  • EF-5. 権限不足:403
  • EF-6. 楽観ロック衝突(同時編集):409、再読込を促す
  • EF-7. 設備重複:同一設備の同時刻占有が重なる → エラー、保存されない(SRS-RES-005)

注記:本書の §4 は親SRS §7.12 に従い最低限の粒度で記述する。デザインシステム導入SRS(仮 SRS-UI-001)リリース時に一括改訂される前提。ビジュアル・タイポグラフィ・コンポーネント選定はここで決めない。

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

  • 担当スタッフセレクタ(1名選択 or 「指名なし」、予約レベル)
  • 顧客セレクタ(既存検索 / 新規登録の切替)
  • メニューセレクタ(複数選択可、明細行ごとに「所要時間」を編集可)
  • 設備セレクタ(明細行ごと or 予約全体、設備の時間枠を指定)
  • 日時セレクタ(開始時刻・終了時刻、自動計算/手動調整)
  • 状態セレクタ(確定 / 仮予約
  • 備考フィールド
  • 保存ボタン / キャンセルボタン
  • 顧客:選択必須(新規登録モードでは氏名のみ必須)
  • メニュー:1 件以上
  • 開始時刻:店舗タイムゾーン(Asia/Tokyo)で有効な timestamptz
  • 終了時刻:開始時刻より後
  • 担当スタッフ:指定時は全選択メニューの menu_eligibility を満たすこと
  1. 空きセルクリック or 顧客画面「予約を作成」 → ダイアログ開く
  2. 顧客/メニュー/必要なら設備
  3. 警告が出ても override_business_hours を持つ者は続行可
  4. 保存 → ダイアログ閉じる、カレンダー即時更新

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

5.2 zodスキーマ(方向性、確定は実装時)

Section titled “5.2 zodスキーマ(方向性、確定は実装時)”
  • RequestPOST /api/admin/reservations
    • store_id はセッションから解決(body に含めない)
    • customer{ kind: 'existing', id: uuid } | { kind: 'new', name: string, phone?: string, email?: string }
    • staff_id?: uuid | null(null or 省略 = 指名なし)
    • menu_lines: 1 件以上
      • menu_id: uuid
      • duration_minutes?: number(省略時はメニュー既定)
      • sort_order: number
    • equipment_assignments?: 0 件以上
      • equipment_id: uuid
      • starts_at: ISO8601
      • ends_at: ISO8601
    • starts_at: ISO8601
    • ends_at?: ISO8601(省略時は starts_at + SUM(duration_minutes)
    • status: 'confirmed' | 'tentative'
    • notes?: string
    • override_business_hours?: boolean(true のとき EF-2 の警告を押し通す。permission 必須)
    • ack_customer_double_booked?: boolean(同一顧客時間重複警告を承認。§7.6)
  • Response(成功)
    • { data: { reservation: {...}, reservation_menu: [...], reservation_equipment: [...] }, warnings?: Warning[] }
    • Warning{ code: 'CUSTOMER_DOUBLE_BOOKED', reservation_ids: uuid[] }
  • RESERVATION.STAFF_CONCURRENT_LIMIT — スタッフの並列予約上限超過(409)
  • RESERVATION.DOUBLE_BOOKED_EQUIPMENT — 設備の時間重複(409)
  • RESERVATION.OUT_OF_BUSINESS_HOURS — 営業時間外・定休日(権限不足時、422)
  • RESERVATION.STAFF_OFF_SHIFT — シフト外(情報用、保存自体はブロックしない)
  • RESERVATION.MENU_NOT_ELIGIBLE_FOR_STAFF — 担当不可メニュー(422)
  • RESERVATION.CUSTOMER_INVALID — 新規顧客バリデーション失敗(422)
  • RESERVATION.CONFLICT — 楽観ロック衝突(409)

6.1 スキーマ(初期案、詳細は実装時に確定)

Section titled “6.1 スキーマ(初期案、詳細は実装時に確定)”

reservation(業務実体、UUIDv7)

  • id, store_id, customer_id
  • staff_id uuid NULL — 担当スタッフ(NULL = 指名なし、カレンダーの「未指名」列に表示)
  • status, starts_at, ends_at
  • buffer_minutes_after smallint NOT NULL DEFAULT 0 — 二重予約判定用のバッファ。選択メニューの menu.buffer_minutes_after のうち最大値をスナップショット。顧客向け ends_at には含めない
  • tentative_expires_at timestamptz NULL — 仮予約の自動失効時刻。status='tentative' のときのみ NOT NULL(SRS-RES-004 §6.1 の不変条件 CHECK 制約で強制)。status='confirmed' 直入りでは NULL。計算ロジックは §7.3
  • source(Phase 1 は admin_manual 固定)
  • notes, created_by(operator_id), created_at, updated_at
  • version(楽観ロック)
  • deleted_at(親SRS §7.5 に従い、必要な箇所のみ)

reservation_menu(業務実体、UUIDv7)

  • id, store_id, reservation_id, menu_id
  • duration_minutes(メニューからスナップショット)
  • price_excl_tax, price_incl_tax, tax_amount, tax_rate_pct(料金スナップショット、§7.1)
  • sort_order
  • 料金・所要時間はすべて予約作成時点のメニュー値をスナップショット(将来のメニュー改定に影響されない)

reservation_equipment(業務実体、UUIDv7)— N:M の独立テーブル

  • id, store_id, reservation_id
  • equipment_id
  • starts_at, ends_at(設備占有の独立時間枠)
  • is_active boolean NOT NULL DEFAULT true(キャンセル時に false に落とす、SRS-RES-005 §6 参照)
  • 二重予約判定は EXCLUDE 制約で DB 側に寄せる(§7.4)

複合 FK は (store_id, target_id) で親SRS §7.1 に従う。

  • BC-RES 初回マイグレーションで reservation, reservation_menu, reservation_equipment を同時作成
  • RLS を ENABLE + FORCE、app ロールに SELECT/INSERT/UPDATE/DELETE を GRANT
  • app.current_store_id ポリシーを適用

予約作成時点で menu.price_excl_tax / price_incl_tax / tax_amount / tax_rate_pctreservation_menu にコピーする。メニュー改定後も既存予約の金額は不変。

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

Section titled “7.2 所要時間・終了時刻・バッファ”
  • 顧客向け表示(reservation.ends_at):starts_at + SUM(reservation_menu.duration_minutes)バッファは含めない
  • reservation.buffer_minutes_after:選択メニューの menu.buffer_minutes_after最大値をスナップショット。施術後のスタッフ拘束時間(片付け・準備)を表す
  • オペレーターが ends_at を手動上書きした場合はその値を保持
  • 二重予約判定の時刻ends_at + buffer_minutes_after を使う(内部都合のバッファは客への終了時刻表示には混ぜない)

7.3 状態の初期値と仮予約の失効スナップショット

Section titled “7.3 状態の初期値と仮予約の失効スナップショット”

UI で選択:confirmed(確定)または tentative(仮予約)。10 状態の完全仕様は SRS-RES-004 §7.1 を canonical source として参照

tentative_expires_at のスナップショット(SRS-RES-004 v0.2 連動):

  • status='tentative' で作成:tentative_expires_at = created_at + store_settings.tentative_expire_hours * '1 hour'::interval
  • status='confirmed' で作成:tentative_expires_at = NULL
  • 不変条件 CHECK 制約は SRS-RES-004 §6.1 が canonical((status='tentative') ⇔ (tentative_expires_at IS NOT NULL)
  • 設定値変更(例:24h→48h)は既存予約に遡及しない(スナップショット)

Phase 1 では admin_manual 固定。phone / hpb / liff / walk_in は後続 SRS で追加。

7.5 設備の時間枠と重複禁止制約

Section titled “7.5 設備の時間枠と重複禁止制約”

設備の占有時間は reservation_equipment.starts_at / ends_at で独立に持つ。二重予約判定は EXCLUDE 制約で DB 側に寄せる(SRS-RES-005 §6 参照)。キャンセル済み予約は is_active = false で EXCLUDE 対象外にする。

reservation.version を持ち、UPDATE 時は WHERE id = ? AND version = ? で照合。不一致なら RESERVATION.CONFLICT

reservation.create として operator_action_log に記録(親SRS §7.8)。target = reservation_id、diff は作成レコード全体。

診断用フラグdiff の JSONB トップレベルキー、検索容易化のため):

  • override_business_hours: boolean — EF-2 の押し通しで作成した場合 true
  • customer_double_booked: boolean — §7.6 の警告を承認(ack_customer_double_booked=true)で作成した場合 true

operator_action_log.diff に GIN インデックスを張り、diff @> '{"override_business_hours": true}' で運用検索を可能にする。

  • 同一スタッフの時間重複は受付可能数で制御:同時刻の予約本数が store_settings.max_concurrent_reservations_per_staff を超えたらエラー(SRS-RES-005 に委譲)。カラー放置中に別客のカットを入れる等の並列施術を許容する
  • 同一設備の時間重複は禁止(EF-7、SRS-RES-005 に委譲)
  • 同一顧客の時間重複は基本許可、UI/API で警告のみ
    • API:作成は成功、レスポンスに warnings: [{ code: 'CUSTOMER_DOUBLE_BOOKED', reservation_ids: [...] }] を含める
    • UI:ダイアログ保存時に警告ダイアログ、ack_customer_double_booked=true で確定
    • DB に制約は置かない(親子同伴・掛け持ち等の正当ケースあり)

親SRS §6 に従う。特記事項なし。性能:予約作成 API は p95 300ms 以下(§6.1)。


key用途プリセット初期値
admin:reservation:create予約作成の基本owner / manager / staff / receptionist 全員 ON
admin:reservation:override_business_hoursEF-2 の押し通しowner / manager のみ ON
admin:reservation:adjust_duration終了時刻の手動調整owner / manager / staff / receptionist 全員 ON
admin:customer:createAF-2 新規顧客登録owner / manager / staff / receptionist 全員 ON

チェックは packages/authrequirePermission() 経由でルート定義に宣言的に貼る(親SRS §7.7.3)。

reservation, reservation_menu, reservation_equipmentstore_id RLS ENABLE + FORCE(親SRS §7.1)。


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

Section titled “10. 受け入れ基準(Given-When-Then)”
  • GWT-1 主シナリオ:Given カレンダー画面で空きセルをクリック / When 顧客・メニューを選択して「登録」 / Then 予約が confirmed で作成されカレンダーに即時表示される
  • GWT-2 フリー作成:Given ダイアログでスタッフ未指定(staff_id = NULL) / When 登録 / Then 「未指名」列に予約が入る
  • GWT-3 新規顧客同時作成:Given 「新規顧客を追加」で氏名のみ入力 / When 登録 / Then customerreservation が同一トランザクションで作成される
  • GWT-4 並列予約上限(超過):Given 受付可能数=2 のスタッフに既に同時刻 2 件の予約あり / When 同スタッフ同時刻で登録 / Then RESERVATION.STAFF_CONCURRENT_LIMIT が返り、レコードは作成されない
  • GWT-5 並列予約上限(範囲内):Given 受付可能数=2 のスタッフに既に同時刻 1 件の予約あり / When 同スタッフ同時刻で登録 / Then 作成成功(カラー放置中に別客カットのケース)
  • GWT-6 営業時間外・権限なし:Given staff プリセットで営業時間外に登録 / When 保存 / Then RESERVATION.OUT_OF_BUSINESS_HOURS エラー
  • GWT-7 営業時間外・権限あり:Given manager プリセットで警告 / When override_business_hours=true 付きで保存 / Then 予約作成成功、operator_action_log.diff.override_business_hours = true が記録される
  • GWT-8 料金スナップショット:Given メニュー「カット ¥5,000」で予約作成 / When 予約後にメニュー単価を ¥6,000 に改定 / Then 既存の reservation_menu.price_incl_tax は ¥5,000 のまま
  • GWT-9 担当不可メニュー:Given menu_eligibility でスタッフ A はカラー不可 / When スタッフ A でカラーを含む予約を登録 / Then RESERVATION.MENU_NOT_ELIGIBLE_FOR_STAFF
  • GWT-10 RLSテナント分離:Given 店舗 X のセッション / When 店舗 Y の reservation を SELECT 試行 / Then 0 件
  • GWT-11 顧客掛け持ち警告:Given 同一顧客が同時刻に別予約あり / When ack_customer_double_booked=true で登録 / Then 作成成功、レスポンスに warnings: [{ code: 'CUSTOMER_DOUBLE_BOOKED', ... }]operator_action_log.diff.customer_double_booked = true
  • GWT-12 設備二重予約:Given 同一設備が同時刻に他予約で占有 / When 重なる時間枠で登録 / Then EXCLUDE 制約により RESERVATION.DOUBLE_BOOKED_EQUIPMENT、レコード作成失敗
  • GWT-13 バッファ分離:Given メニュー「カラー 60分 buffer_after 10分」+「カット 30分 buffer_after 0分」 / When 12:00 開始で作成 / Then reservation.ends_at = 13:30(バッファ含まず)、reservation.buffer_minutes_after = 10(メニュー群の最大値)、二重予約判定は 13:40 まで有効
  • GWT-14 仮予約の失効時刻スナップショット:Given store_settings.tentative_expire_hours = 24 / When status='tentative' で 14:00 に作成 / Then reservation.tentative_expires_at = 翌日14:00、SRS-RES-004 §6.1 の CHECK 制約に違反しない
  • GWT-15 確定直入りでの NULL:Given 管理画面手動 / When status='confirmed' で作成 / Then reservation.tentative_expires_at = NULL、CHECK 制約違反なし
  • GWT-16 設定値変更の非遡及:Given tentative_expire_hours=24 で tentative 作成済の予約 / When 設定を 48 に変更 / Then 既存予約の tentative_expires_at は変わらない(スナップショット)

  • Unit (packages/domain):予約長計算、料金スナップショット、status 初期化、ends_at 既定値の算出、バッファ最大値の選出、tentative_expires_at の計算(status と整合する条件付き)
  • Integration (apps/api + Testcontainers Postgres):主シナリオ・AF-1〜4・EF-1〜7・GWT-1〜16 を網羅、tentative_expires_at の CHECK 制約違反テストも含む
  • Contract:zodスキーマの fixture を FE/BE 間で照合
  • E2E (Playwright):Phase 2 のクリティカルフロー入り候補(予約作成 → 会計の縦串の一部)

  • expire_tentative_reservations:5 分間隔の cron。status='tentative' AND tentative_expires_at < now() AND deleted_at IS NULL の予約を expired に遷移。SRS-RES-004 §7.6 が canonical source。RES-002 では tentative_expires_at のスナップショット責務のみ(§7.3)
  • 予約リマインダー送信は SRS-MSG-002 に委譲

#内容締切の目安
(Phase 1 で残る OQ なし)

解消済み(v0.4 で決着):

  • OQ-RES-002-02 仮予約の自動取消ジョブを Phase 1 に入れるかPhase 1 で採用。SRS-RES-004 §7.6 が canonical(expire_tentative_reservations cron)。本SRSは tentative_expires_at のスナップショット責務のみ(§7.3)。tentative→confirmed/declined/cancelled_by_customer 遷移時に tentative_expires_at=NULL に戻す責務は SRS-RES-004 §7.10 のトランザクション境界で確定

解消済み(v0.3 で決着):

  • OQ-01 バッファ時間の保持場所 → §7.2:予約レベルで buffer_minutes_after(選択メニューの最大値をスナップショット)。表示には含めず二重予約判定でのみ加算
  • OQ-03 指名なしの内部表現 → v0.3 で reservation.staff_id = NULL に簡素化(本家 SALON BOARD 互換)。明細レベルのスタッフ割当は Phase 2 差別化機能として検討
  • OQ-04 同一顧客の時間重複 → §7.8:基本許可、UI/API で警告(ack_customer_double_booked
  • OQ-05 設備紐付け → §6.1:別テーブル reservation_equipment(N:M、独立時間枠、EXCLUDE 制約)
  • OQ-06 override の監査ログ表現 → §7.7:diff JSONB にフラグ、GIN インデックスで検索容易化

VersionDateAuthorChange
0.12026-04-22yudai初版起票(Draft)。親SRS v0.2 に従う
0.22026-04-22yudaiOQ-01/03/04/05/06 を解消。reservation_menuassignment_kind / buffer_minutes_after を追加、reservation_equipment を別テーブル(N:M、EXCLUDE 制約)に確定、§7.7 監査に JSONB フラグ + GIN 検索を追記、§7.8 時間重複ポリシー / §7.9 スタッフ割当種別を新設、GWT-10〜12 追加
0.32026-04-23yudai本家 SALON BOARD 互換へモデル変更。スタッフ割当を予約レベルに引き上げ(reservation.staff_id)、reservation_menu から staff_id / assignment_kind / buffer_minutes_after を除去。バッファを reservation.buffer_minutes_after(メニュー最大値スナップショット)に移動。§7.9(assignment_kind)を廃止。§7.8 に受付可能数(並列予約上限)を導入。エラーコード DOUBLE_BOOKED_STAFFSTAFF_CONCURRENT_LIMIT に変更。GWT-4/5 を並列予約上限テストに差替え、GWT-13 バッファ分離を追加
0.42026-04-25yudaiSRS-RES-004 v0.2 と同期改訂reservation.tentative_expires_at 列を追加(§6.1)、計算ロジックを §7.3 に明記(不変条件 CHECK は SRS-RES-004 §6.1 が canonical)。§7.3 の「11 状態」を「10 状態 / SRS-RES-004 §7.1 参照」に修正。§12 で expire_tentative_reservations ジョブを Phase 1 採用と確定し OQ-RES-002-02 解消。GWT-14〜16(失効スナップショット・confirmed 直入り NULL・設定値変更の非遡及)を追加