予約作成(管理画面 手動)
予約作成(管理画面 手動)
Section titled “予約作成(管理画面 手動)”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)に委譲する。
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 フリー客を逃さない
3. ユースケース
Section titled “3. ユースケース”3.1 主シナリオ(管理画面からの通常作成)
Section titled “3.1 主シナリオ(管理画面からの通常作成)”- オペレーターがカレンダー画面で、空いているセル(スタッフ × 時間)をクリック
- 予約作成ダイアログが開き、選択したスタッフ・開始時刻が初期値に入る
- 顧客を検索して選択(既存)、または「新規顧客を追加」で氏名・連絡先を入力
- メニューを 1 つ以上選択(各メニューは所要時間・料金を保持)
- 必要に応じて設備(シャンプー台/セット面等)を紐付け
- 所要時間の合計から終了時刻が自動計算・表示。手動調整も可
- 備考(任意)を入力
- 状態「確定」で「登録」
- システムが二重予約チェック(スタッフ並列上限・設備重複)を実施(詳細は SRS-RES-005)
- OK なら
reservation+reservation_menu(+reservation_equipment)を 1 トランザクションで作成、カレンダーに即時反映
3.2 代替フロー
Section titled “3.2 代替フロー”- AF-1. フリー(指名なし)で作成:
reservation.staff_id = NULLで保存可。カレンダーの「未指名」列に表示、当日割当 - AF-2. 新規顧客をその場で作成:同一トランザクションで
customerも INSERT。氏名のみ必須(親SRS §7.13customer_required_fields = ['name']) - AF-3. 仮予約として保存:初期状態を「仮予約」で登録(電話での取り置き等)
- AF-4. 顧客詳細画面から起動:日時はその場で選択、それ以外は主シナリオと同じ
3.3 例外フロー
Section titled “3.3 例外フロー”- 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. UI仕様
Section titled “4. UI仕様”注記:本書の §4 は親SRS §7.12 に従い最低限の粒度で記述する。デザインシステム導入SRS(仮 SRS-UI-001)リリース時に一括改訂される前提。ビジュアル・タイポグラフィ・コンポーネント選定はここで決めない。
4.1 画面の目的
Section titled “4.1 画面の目的”カレンダー上で発火する予約作成ダイアログ。1 顧客 1 予約を 1 画面で完結させる。
4.2 主要要素(列挙)
Section titled “4.2 主要要素(列挙)”- 担当スタッフセレクタ(1名選択 or 「指名なし」、予約レベル)
- 顧客セレクタ(既存検索 / 新規登録の切替)
- メニューセレクタ(複数選択可、明細行ごとに「所要時間」を編集可)
- 設備セレクタ(明細行ごと or 予約全体、設備の時間枠を指定)
- 日時セレクタ(開始時刻・終了時刻、自動計算/手動調整)
- 状態セレクタ(
確定/仮予約) - 備考フィールド
- 保存ボタン / キャンセルボタン
4.3 バリデーション
Section titled “4.3 バリデーション”- 顧客:選択必須(新規登録モードでは氏名のみ必須)
- メニュー:1 件以上
- 開始時刻:店舗タイムゾーン(
Asia/Tokyo)で有効なtimestamptz - 終了時刻:開始時刻より後
- 担当スタッフ:指定時は全選択メニューの
menu_eligibilityを満たすこと
4.4 操作フロー
Section titled “4.4 操作フロー”- 空きセルクリック or 顧客画面「予約を作成」 → ダイアログ開く
- 顧客/メニュー/必要なら設備
- 警告が出ても
override_business_hoursを持つ者は続行可 - 保存 → ダイアログ閉じる、カレンダー即時更新
5. API仕様
Section titled “5. API仕様”5.1 エンドポイント一覧
Section titled “5.1 エンドポイント一覧”| Method | Path | 用途 | 必要 permission |
|---|---|---|---|
| POST | /api/admin/reservations | 予約作成 | admin:reservation:create |
5.2 zodスキーマ(方向性、確定は実装時)
Section titled “5.2 zodスキーマ(方向性、確定は実装時)”- Request(
POST /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: uuidduration_minutes?: number(省略時はメニュー既定)sort_order: number
equipment_assignments?: 0 件以上equipment_id: uuidstarts_at: ISO8601ends_at: ISO8601
starts_at: ISO8601ends_at?: ISO8601(省略時はstarts_at + SUM(duration_minutes))status: 'confirmed' | 'tentative'notes?: stringoverride_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[] }等
5.3 エラーコード
Section titled “5.3 エラーコード”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. データモデル影響
Section titled “6. データモデル影響”6.1 スキーマ(初期案、詳細は実装時に確定)
Section titled “6.1 スキーマ(初期案、詳細は実装時に確定)”reservation(業務実体、UUIDv7)
id,store_id,customer_idstaff_id uuid NULL— 担当スタッフ(NULL = 指名なし、カレンダーの「未指名」列に表示)status,starts_at,ends_atbuffer_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.3source(Phase 1 はadmin_manual固定)notes,created_by(operator_id),created_at,updated_atversion(楽観ロック)deleted_at(親SRS §7.5 に従い、必要な箇所のみ)
reservation_menu(業務実体、UUIDv7)
id,store_id,reservation_id,menu_idduration_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_idequipment_idstarts_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 に従う。
6.2 マイグレーション計画
Section titled “6.2 マイグレーション計画”- BC-RES 初回マイグレーションで
reservation,reservation_menu,reservation_equipmentを同時作成 - RLS を ENABLE + FORCE、
appロールに SELECT/INSERT/UPDATE/DELETE を GRANT app.current_store_idポリシーを適用
7. 業務ルール
Section titled “7. 業務ルール”7.1 料金スナップショット
Section titled “7.1 料金スナップショット”予約作成時点で menu.price_excl_tax / price_incl_tax / tax_amount / tax_rate_pct を reservation_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'::intervalstatus='confirmed'で作成:tentative_expires_at = NULL- 不変条件 CHECK 制約は SRS-RES-004 §6.1 が canonical(
(status='tentative') ⇔ (tentative_expires_at IS NOT NULL)) - 設定値変更(例:24h→48h)は既存予約に遡及しない(スナップショット)
7.4 source
Section titled “7.4 source”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 対象外にする。
7.6 楽観ロック
Section titled “7.6 楽観ロック”reservation.version を持ち、UPDATE 時は WHERE id = ? AND version = ? で照合。不一致なら RESERVATION.CONFLICT。
7.7 監査
Section titled “7.7 監査”reservation.create として operator_action_log に記録(親SRS §7.8)。target = reservation_id、diff は作成レコード全体。
診断用フラグ(diff の JSONB トップレベルキー、検索容易化のため):
override_business_hours: boolean— EF-2 の押し通しで作成した場合 truecustomer_double_booked: boolean— §7.6 の警告を承認(ack_customer_double_booked=true)で作成した場合 true
operator_action_log.diff に GIN インデックスを張り、diff @> '{"override_business_hours": true}' で運用検索を可能にする。
7.8 時間重複ポリシー
Section titled “7.8 時間重複ポリシー”- 同一スタッフの時間重複は受付可能数で制御:同時刻の予約本数が
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 に制約は置かない(親子同伴・掛け持ち等の正当ケースあり)
- API:作成は成功、レスポンスに
8. 非機能要件
Section titled “8. 非機能要件”親SRS §6 に従う。特記事項なし。性能:予約作成 API は p95 300ms 以下(§6.1)。
9. セキュリティ・認可
Section titled “9. セキュリティ・認可”9.1 使用する permission キー
Section titled “9.1 使用する permission キー”| key | 用途 | プリセット初期値 |
|---|---|---|
admin:reservation:create | 予約作成の基本 | owner / manager / staff / receptionist 全員 ON |
admin:reservation:override_business_hours | EF-2 の押し通し | owner / manager のみ ON |
admin:reservation:adjust_duration | 終了時刻の手動調整 | owner / manager / staff / receptionist 全員 ON |
admin:customer:create | AF-2 新規顧客登録 | owner / manager / staff / receptionist 全員 ON |
チェックは packages/auth の requirePermission() 経由でルート定義に宣言的に貼る(親SRS §7.7.3)。
9.2 RLS
Section titled “9.2 RLS”reservation, reservation_menu, reservation_equipment は store_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
customerとreservationが同一トランザクションで作成される - GWT-4 並列予約上限(超過):Given 受付可能数=2 のスタッフに既に同時刻 2 件の予約あり / When 同スタッフ同時刻で登録 / Then
RESERVATION.STAFF_CONCURRENT_LIMITが返り、レコードは作成されない - GWT-5 並列予約上限(範囲内):Given 受付可能数=2 のスタッフに既に同時刻 1 件の予約あり / When 同スタッフ同時刻で登録 / Then 作成成功(カラー放置中に別客カットのケース)
- GWT-6 営業時間外・権限なし:Given
staffプリセットで営業時間外に登録 / When 保存 / ThenRESERVATION.OUT_OF_BUSINESS_HOURSエラー - GWT-7 営業時間外・権限あり:Given
managerプリセットで警告 / Whenoverride_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 でカラーを含む予約を登録 / ThenRESERVATION.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/ Whenstatus='tentative'で 14:00 に作成 / Thenreservation.tentative_expires_at = 翌日14:00、SRS-RES-004 §6.1 の CHECK 制約に違反しない - GWT-15 確定直入りでの NULL:Given 管理画面手動 / When
status='confirmed'で作成 / Thenreservation.tentative_expires_at = NULL、CHECK 制約違反なし - GWT-16 設定値変更の非遡及:Given
tentative_expire_hours=24で tentative 作成済の予約 / When 設定を 48 に変更 / Then 既存予約のtentative_expires_atは変わらない(スナップショット)
11. テスト計画
Section titled “11. テスト計画”- 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 のクリティカルフロー入り候補(予約作成 → 会計の縦串の一部)
12. 関連ジョブ(graphile-worker)
Section titled “12. 関連ジョブ(graphile-worker)”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 に委譲
13. Open Questions
Section titled “13. Open Questions”| # | 内容 | 締切の目安 |
|---|---|---|
| (Phase 1 で残る OQ なし) |
解消済み(v0.4 で決着):
OQ-RES-002-02 仮予約の自動取消ジョブを Phase 1 に入れるか→ Phase 1 で採用。SRS-RES-004 §7.6 が canonical(expire_tentative_reservationscron)。本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:diffJSONB にフラグ、GIN インデックスで検索容易化
14. 変更履歴
Section titled “14. 変更履歴”| Version | Date | Author | Change |
|---|---|---|---|
| 0.1 | 2026-04-22 | yudai | 初版起票(Draft)。親SRS v0.2 に従う |
| 0.2 | 2026-04-22 | yudai | OQ-01/03/04/05/06 を解消。reservation_menu に assignment_kind / buffer_minutes_after を追加、reservation_equipment を別テーブル(N:M、EXCLUDE 制約)に確定、§7.7 監査に JSONB フラグ + GIN 検索を追記、§7.8 時間重複ポリシー / §7.9 スタッフ割当種別を新設、GWT-10〜12 追加 |
| 0.3 | 2026-04-23 | yudai | 本家 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_STAFF → STAFF_CONCURRENT_LIMIT に変更。GWT-4/5 を並列予約上限テストに差替え、GWT-13 バッファ分離を追加 |
| 0.4 | 2026-04-25 | yudai | SRS-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・設定値変更の非遡及)を追加 |