予約作成(管理画面 手動)
予約作成(管理画面 手動)
Section titled “予約作成(管理画面 手動)”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_code、reservation_menu_lines.menu_name_snapshot を Phase 1 から保持する。
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 主シナリオ(管理画面からの通常作成)”- オペレーターがカレンダー画面で空きセル(スタッフ × 時間)をクリック
- 予約作成ダイアログが開き、選択したスタッフ・開始時刻が初期値に入る
- 顧客を検索して選択(既存)、または「新規顧客を追加」で氏名・カナを入力(CUS-001)
- メニューを 1 つ以上選択(各メニューは所要時間・料金・
equipment_group要件を保持) - 所要時間の合計から終了時刻が自動計算
- 備考(任意)を入力
- 状態「確定」で「登録」
- システムが二重予約チェック(スタッフ並列上限・設備重複)を実施(SRS-RES-005 v0.4)
- 設備自動割当:
menu_equipment_requirementの(equipment_group_id, quantity)ごとに、group 内の空き concreteequipment_idをquantity個選びreservation_equipment_assignmentsに INSERT - OK なら
reservations+reservation_menu_lines(menu_name_snapshot付き)+reservation_equipment_assignmentsを 1 トランザクションで作成 reservation.codeをアプリ層で短コード生成しUNIQUE(store_id, code)衝突時はリトライoperator_action_log(action='reservation.create')にactor_kind='operator'で記録(diff にforce_business_hours/force_staff_concurrencyフラグを含める)
3.2 代替フロー
Section titled “3.2 代替フロー”- AF-1 フリー(指名なし)で作成:
reservation.staff_id = NULLで保存可 - AF-2 新規顧客をその場で作成: 同一トランザクションで CUS-001 の
customerも INSERT - AF-3 仮予約として保存: 初期状態を
tentativeで登録、tentative_expires_atをスナップショット - AF-4 顧客詳細画面から起動: 日時はその場で選択、それ以外は主シナリオと同じ
3.3 例外フロー
Section titled “3.3 例外フロー”- EF-1 並列予約上限超過: 同一スタッフの同時刻予約数が
store_settings.max_concurrent_reservations_per_staffを超 → 409RESERVATION.STAFF_CONCURRENT_LIMIT(force_staff_concurrencypermission 持ちは押し通し可) - EF-2 営業時間外・定休日:
force_business_hourspermission を持つ者は warning 経由で保存可、持たない者は 422RESERVATION.OUT_OF_BUSINESS_HOURS - EF-3 シフト外: 警告のみ、保存可(RES-005 §7.1)
- EF-4 担当不可メニュー(
menu_staff_eligibilityホワイトリスト違反): 422RESERVATION.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_EQUIPMENT(force_staff_concurrencyでも bypass 不可) - EF-9 顧客時間重複: warning のみ、
ack_customer_double_booked=trueで保存可
4. UI仕様
Section titled “4. UI仕様”4.1 画面の目的
Section titled “4.1 画面の目的”カレンダー上で発火する予約作成ダイアログ。1 顧客 1 予約を 1 画面で完結させる。
4.2 主要要素
Section titled “4.2 主要要素”- 担当スタッフセレクタ(1 名選択 or 「指名なし」)
- 顧客セレクタ(既存検索 / 新規登録の切替)
- メニューセレクタ(複数選択可)
- 日時セレクタ(開始・終了の自動計算)
- 状態セレクタ(
confirmed/tentative) - 備考フィールド
- 設備は表示のみ(自動割当結果を表示)
- 保存ボタン / キャンセルボタン
- force* 確認ダイアログ(営業時間外、staff 並列超過時)
4.3 バリデーション
Section titled “4.3 バリデーション”- 顧客: 選択必須(新規登録モードでは name + name_kana 必須)
- メニュー: 1 件以上
- 開始時刻:
Asia/Tokyoで有効なtimestamptz - 終了時刻: 開始時刻より後
- 担当スタッフ: 指定時は全選択メニューの
menu_staff_eligibilityを満たすこと
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:
store_idはセッションから解決(body に含めない)customer:{ kind: 'existing', id: uuid }|{ kind: 'new', name, name_kana, phone?, email? }staff_id?: uuid | nullmenu_lines:[{ menu_id, duration_minutes?, sort_order }]1 件以上starts_at: ISO8601ends_at?: ISO8601(省略時はstarts_at + SUM(duration_minutes))status: 'confirmed' | 'tentative'notes?: stringforce_business_hours?: booleanforce_staff_concurrency?: booleanack_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, ...}]
5.3 エラーコード
Section titled “5.3 エラーコード”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. データモデル影響
Section titled “6. データモデル影響”6.1 reservations(業務実体、UUIDv7)
Section titled “6.1 reservations(業務実体、UUIDv7)”主要列:
id uuid PKstore_id uuid NOT NULLcustomer_id uuid NOT NULLstaff_id uuid NULLcode 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 timestamptzbuffer_minutes_after smallint NOT NULL DEFAULT 0— メニュー群最大値スナップショットtentative_expires_at timestamptz NULL— RES-004 §6.1 不変条件 CHECKnotes textcreated_by uuid(TEN-002 後 operator_id)version integer NOT NULL DEFAULT 1created_at,updated_atdeleted_at timestamptz NULL
CHECK: (status='tentative' AND tentative_expires_at IS NOT NULL) OR (status<>'tentative' AND tentative_expires_at IS NULL)
6.2 reservation_menu_lines
Section titled “6.2 reservation_menu_lines”- 既存列に加え
menu_name_snapshot varchar(200) NOT NULLを追加 tax_rate_pct CHECK IN (8,10)は親 §7.16 / TEN-001 v0.4 (0011) で撤去済
6.3 reservation_equipment_assignments
Section titled “6.3 reservation_equipment_assignments”- 既存スキーマを維持(0006 + 0009 EXCLUDE 制約は実装済)
equipment_idは concrete 設備を指すis_active boolean NOT NULL DEFAULT true
6.4 マイグレーション計画
Section titled “6.4 マイグレーション計画”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.diffGIN index 追加reservation_event_actor_polymorphismCHECK 修正(RES-004 §6.2 連動)force_*permission rename + backfill(RES-005 §9.1 連動)
7. 業務ルール
Section titled “7. 業務ルール”7.1 料金スナップショット
Section titled “7.1 料金スナップショット”予約作成時点で MST-002 §7.2 に従い、以下を reservation_menu_lines にコピー:
menu_name_snapshot(新規必須)duration_minutesprice_excl_tax,tax_amount,price_incl_tax,tax_rate_pctbuffer_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'::intervalconfirmed直入り:tentative_expires_at = NULL- 設定値変更は既存予約に遡及しない
7.4 source / channel_code の役割分離
Section titled “7.4 source / channel_code の役割分離”source: 内部技術 origin(debug / observability 用)channel_code: 業務的予約経路 taxonomy(営業分析、HPB 等の Phase 2 拡張用)- Phase 1 は両方
admin_manual固定だが、Phase 2 で channel_code はphone / hpb / liff / walk_inを追加予定
7.5 設備自動割当
Section titled “7.5 設備自動割当”menu_equipment_requirementの(equipment_group_id, quantity)ごとに、group 内 active equipment(bookable=true AND terminated_at IS NULL)から候補時間帯に EXCLUDE 制約と衝突しないequipment_idをquantity個選ぶ- 候補列挙順は
display_order ASC, id ASC - 不足なら
RESERVATION.NO_EQUIPMENT_AVAILABLE422 - INSERT 時に EXCLUDE 制約が race 競合を catch(
23P01→RESERVATION.DOUBLE_BOOKED_EQUIPMENT409 変換)
7.6 楽観ロック
Section titled “7.6 楽観ロック”versionは INSERT 時 1、UPDATE で +1- UPDATE は
WHERE id=? AND version=?、不一致はRESERVATION.CONFLICT
7.7 監査
Section titled “7.7 監査”reservation.createをoperator_action_logに記録、actor_kind='operator'diffJSONB トップレベルキー:force_business_hours: boolean(押し通した場合 true)force_staff_concurrency: boolean(押し通した場合 true)customer_double_booked: boolean(warning ack 経由)
- GIN index で diff 検索可能
7.8 時間重複ポリシー
Section titled “7.8 時間重複ポリシー”- 同一スタッフ: 受付可能数で制御(RES-005 §7.3)、
force_staff_concurrencyで押し通し可 - 同一設備: EXCLUDE 制約で禁止、force でも bypass 不可
- 同一顧客: 警告のみ、
ack_customer_double_bookedで確定
8. 非機能要件
Section titled “8. 非機能要件”- 予約作成 API は p95 300ms 以下(親 §6.1)
9. セキュリティ・認可
Section titled “9. セキュリティ・認可”9.1 使用する permission キー
Section titled “9.1 使用する permission キー”| key | 用途 | プリセット初期値 |
|---|---|---|
admin:reservation:create | 予約作成 | 全 role |
admin:reservation:force_business_hours | 営業時間外押し通し | owner / manager |
admin:reservation:force_staff_concurrency | スタッフ並列上限押し通し | owner / manager |
admin:customer:create | AF-2 新規顧客登録 | 全 role |
9.2 RLS
Section titled “9.2 RLS”reservations, reservation_menu_lines, reservation_equipment_assignments は store_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_EQUIPMENT409 - GWT-13 バッファ分離: メニュー「カラー 60 分 buffer 10 分」+「カット 30 分 buffer 0」 →
ends_at = 13:30、buffer_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'で記録される
11. テスト計画
Section titled “11. テスト計画”- 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): 予約作成 → 会計の縦串
12. 関連ジョブ(graphile-worker)
Section titled “12. 関連ジョブ(graphile-worker)”expire-tentative-reservations(cron 5 分) — RES-004 §7.6 / SRS-WRK-001 で詳細
13. Open Questions
Section titled “13. Open Questions”| # | 内容 | 扱い |
|---|---|---|
| OQ-RES-002-01 | code の形式と人間可読性のバランス | Phase 1 は短コード、Phase 2 でレジ・帳票連携時に再検討 |
| OQ-RES-002-02 | channel_code 拡張時の値域確定 | Phase 2 SRS-RES-007 |
解消済み:
OQ-RES-002-CSV→ CSV 一括インポートは Phase 2 へ持越し(CUS-001 OQ-CUS-001-CSV)
14. 変更履歴
Section titled “14. 変更履歴”| Version | Date | Author | Change |
|---|---|---|---|
| 0.1〜0.4 | 2026-04-22 〜 2026-04-25 | yudai | 初版〜v0.4(既存履歴) |
| 0.5 | 2026-05-05 | yudai (with Codex co-design) | Parent v0.5 同期。reservation.code / channel_code を追加(ERD §4 整合)。reservation_menu_lines.menu_name_snapshot 必須追加。force_double_booking → force_staff_concurrency リネーム、equipment EXCLUDE は force でも bypass 不可と明記。menu_eligibility 旧名を menu_staff_eligibility に統一。設備自動割当を equipment_group + quantity 連動に確定。source と channel_code の意味分離を §7.4 に明記。GIN index for operator_action_log.diff、actor_kind 採用、migration 番号 0019 確定 |