予約状態遷移(10状態の状態機械)
予約状態遷移(10状態の状態機械)
Section titled “予約状態遷移(10状態の状態機械)”Document ID: SRS-RES-004 Parent: SRS-ROOT-001 v0.5 Version: 0.3 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-RES-002 v0.5, SRS-RES-005 v0.4 依存される: SRS-RES-002, SRS-RES-003, SRS-REG-001, SRS-REG-002, SRS-PAY-001/002, SRS-MSG-002, SRS-LIFF-002
本書は SRS-ROOT-001 v0.5 に従う。
予約の 10 状態 を定義し、状態遷移を一元管理する。Phase 1 の主要変更点:
operator_action_log二重書き責務を transition handler の 1 トランザクションに明記reservation_event_actor_polymorphismCHECK の論理欠陥を修正(trigger='operator' AND operator_id IS NOT NULL強制)actor_kind enum('operator','system')をoperator_action_logに追加(TEN-002 連動)OQ-RES-004-04(paid 予約の修正・返金)を REG-001 のvisit_refundで closed- permission resolver は
packages/auth(pure)、Hono middleware はapps/api(layer 分離) - 8 個の細粒度 transition permission を維持(統合キーは廃止)
判定ロジック(重複・営業時間外)は本SRSの対象外で SRS-RES-005 / SRS-MST-003 が持つ。本SRSは「いま何の状態か」「次はどこへ動けるか」「誰が動かせるか」のみを扱う。
本SRSは reservation.status の canonical source。RES-005 の「判定対象外条件」など他SRSが status 値域に依存する箇所は本SRSを参照する。
2. ユーザーストーリー
Section titled “2. ユーザーストーリー”- As a 受付, I want to LIFF/HPB から入った仮予約を確認して確定 or お断りに振り分けたい, so that 顧客が予定を確実に押さえられる
- As a 受付, I want to 来店した顧客を 1 クリックで「施術中」に移したい, so that カレンダー上の進捗が一目で分かる
- As a 店長, I want to 来店時刻を過ぎても来ない顧客を「無断キャンセル」として記録したい
- As a オーナー, I want to 仮予約が一定時間放置されたら自動でキャンセルにしたい
- As a 店長, I want to 誤って進めた状態を 1 段階戻せるようにしたい
3. ユースケース
Section titled “3. ユースケース”3.1 状態の概観
Section titled “3.1 状態の概観”10 状態を進行系 4 + 終端系 6 に分類:
- 進行系:
tentative,confirmed,in_service,service_completed - 終端系:
paid,declined,cancelled_by_customer,cancelled_by_store,no_show,expired
3.2 主シナリオ
Section titled “3.2 主シナリオ”tentative ─┐ ├─→ confirmed → in_service → service_completed → paid(新規確定)─┘3.3 代替フロー
Section titled “3.3 代替フロー”- AF-1〜AF-7: v0.2 と同じ
- AF-8 巻き戻し: 中間状態の 1 段階戻し(reopen)
3.4 例外フロー
Section titled “3.4 例外フロー”- EF-1 不正遷移: 422
RESERVATION_STATE.INVALID_TRANSITION - EF-2 権限不足: 403
- EF-3 楽観ロック衝突: 409
- EF-4 no_show 猶予未経過: 422
RESERVATION_STATE.GRACE_PERIOD_NOT_PASSED - EF-5 reopen フラグ未指定: 422
RESERVATION_STATE.REOPEN_FLAG_REQUIRED - EF-6 transition handler の
operator_action_log二重書き失敗: 全体 rollback
4. UI仕様
Section titled “4. UI仕様”v0.2 と同じ。状態バッジ・アクションメニュー・キャンセル理由入力・巻き戻し導線・no_show ボタンの可視性。
5. API仕様
Section titled “5. API仕様”5.1 エンドポイント一覧
Section titled “5.1 エンドポイント一覧”| Method | Path | 用途 | 必要 permission |
|---|---|---|---|
| POST | /api/admin/reservations/:id/transitions | 状態遷移 | resolver で動的解決(§9.1) |
| GET | /api/admin/reservations/:id/transitions/available | 可能な遷移一覧 | admin:reservation:read |
| GET | /api/admin/reservations/:id/events | 遷移ログタイムライン | admin:reservation:read_timeline |
5.1.1 認可解決の責務分離(親 §7.7.3)
Section titled “5.1.1 認可解決の責務分離(親 §7.7.3)”packages/auth/src/reservation-transitions.tsresolveReservationTransitionPermission({ to, reopen, fromStatus }): PermissionKey(pure 純関数)
apps/api/src/middleware/require-reservation-transition-permission.ts- body 検証 → resolver 呼出 →
requirePermission(key)
- body 検証 → resolver 呼出 →
- handler 内で permission 判定をしない
5.2 zodスキーマ
Section titled “5.2 zodスキーマ”{ to: 'confirmed' | 'declined' | 'in_service' | 'service_completed' | 'paid' | 'cancelled_by_customer' | 'cancelled_by_store' | 'no_show'; reopen?: boolean; reason_code?: string; notes?: string; version: number;}to: 'tentative' / 'expired' は API 経由不可(前者は予約作成時、後者は自動ジョブ)。
5.3 エラーコード
Section titled “5.3 エラーコード”RESERVATION_STATE.INVALID_TRANSITION(422)RESERVATION_STATE.GRACE_PERIOD_NOT_PASSED(422)RESERVATION_STATE.REOPEN_FLAG_REQUIRED(422)RESERVATION.CONFLICT(409)
6. データモデル影響
Section titled “6. データモデル影響”6.1 reservations への変更(v0.2 から維持)
Section titled “6.1 reservations への変更(v0.2 から維持)”status は text + CHECK IN (10 values)。tentative_expires_at 列の不変条件 CHECK も維持。
6.2 reservation_event の actor CHECK 修正(v0.3 で修正)
Section titled “6.2 reservation_event の actor CHECK 修正(v0.3 で修正)”ALTER TABLE reservation_event DROP CONSTRAINT IF EXISTS reservation_event_actor_polymorphism;
ALTER TABLE reservation_event ADD CONSTRAINT reservation_event_actor_polymorphism CHECK ( (trigger = 'operator' AND operator_id IS NOT NULL AND customer_id IS NULL) OR (trigger = 'customer' AND operator_id IS NULL AND customer_id IS NOT NULL) OR (trigger = 'system' AND operator_id IS NULL AND customer_id IS NULL) );v0.2 の CHECK は trigger='operator' AND operator_id IS NOT NULL を強制していなかった。v0.3 で修正。
6.3 operator_action_log への変更(TEN-002 連動)
Section titled “6.3 operator_action_log への変更(TEN-002 連動)”actor_kind enum('operator','system') 列を追加(SRS-TEN-002 §6.1 で詳述)。本SRSは「reservation transition」記録時の使い方を定める:
actor_kind='operator',operator_id NOT NULL: operator / customer 起点(customer 起点は audit log 対象外、§7.7 参照)actor_kind='system',operator_id NULL: 自動失効ジョブ等
6.4 store_settings の責務委譲
Section titled “6.4 store_settings の責務委譲”tentative_expire_hours, no_show_grace_minutes の列定義は SRS-TEN-001 v0.4 (0011) が canonical。本SRSは値の参照のみ。
6.5 マイグレーション計画
Section titled “6.5 マイグレーション計画”0019_reservation_phase1_finalize.sql で:
reservation_event_actor_polymorphismCHECK 修正operator_action_log.diffGIN index 追加force_*permission rename- 11 transition permission の backfill(既存 0010 の延長として実施)
7. 業務ルール
Section titled “7. 業務ルール”7.1 10 状態の定義
Section titled “7.1 10 状態の定義”v0.2 と同じ表(code 1〜10、進行系・終端系)。
7.2 状態の不変条件
Section titled “7.2 状態の不変条件”v0.2 と同じ。終端遷移不可、tentative への遷移は予約作成時のみ、expired は自動のみ、tentative → cancelled_by_store 禁止、no_show 猶予条件、判定対象 status 4 値。
7.3 遷移マトリクス
Section titled “7.3 遷移マトリクス”v0.2 と同じ(10 × 10 セル、✅ ↩ ⚙ -)。
7.4 declined と cancelled_by_store の境界
Section titled “7.4 declined と cancelled_by_store の境界”v0.2 と同じ。
7.5 reopen / 巻き戻し
Section titled “7.5 reopen / 巻き戻し”v0.2 と同じ。終端からの reopen は Phase 1 でサポートしない(代替運用フロー: 新規予約再作成)。
7.6 自動遷移ジョブ
Section titled “7.6 自動遷移ジョブ”v0.2 と同じ。expire_tentative_reservations 5 分 cron。SRS-WRK-001 の fan-out 規約に従い、expire-tentative-reservations(system scheduler)→ expire-tentative-for-store(store ごと)の 2 段構成で実装。
7.7 監査の連動(v0.3 で責務明記)
Section titled “7.7 監査の連動(v0.3 で責務明記)”すべての遷移で同一トランザクション内に以下を実行:
reservationsをWHERE id=? AND version=?で UPDATEreservation_eventINSERToperator_action_logINSERT(trigger='customer'の場合は skip)
operator_action_log への INSERT は handler の責務として明示。reservation_event だけ書いて operator_action_log を書き忘れる実装は禁止。
7.8 SRS-RES-005 との整合(判定対象の確定)
Section titled “7.8 SRS-RES-005 との整合(判定対象の確定)”判定対象 status:
status IN ('tentative','confirmed','in_service','service_completed')AND deleted_at IS NULL終端は判定対象外(paid を含む、§7.8 v0.2 と同じ)。
7.9 reservation_equipment_assignments.is_active 同期
Section titled “7.9 reservation_equipment_assignments.is_active 同期”| 遷移先 | is_active |
|---|---|
declined / cancelled_by_customer / cancelled_by_store / no_show / expired | 当該予約の全行を false に UPDATE |
paid | 変更しない(履歴として有効、EXCLUDE 対象継続) |
| 上記以外 | 変更しない |
7.10 トランザクション境界
Section titled “7.10 トランザクション境界”1 回の遷移リクエストは 1 トランザクション:
reservationsUPDATEstatus更新tentative_expires_atをNULLに更新(tentativeから他状態への遷移時のみ)version + 1updated_at = now()
reservation_eventINSERToperator_action_logINSERT(trigger=‘customer’ は skip)- キャンセル系なら
reservation_equipment_assignments.is_active=falseUPDATE
7.11 paid 予約の修正・返金(OQ-RES-004-04 closed)
Section titled “7.11 paid 予約の修正・返金(OQ-RES-004-04 closed)”paid状態の修正は SRS-REG-001 のvisit.refunded_amount+visit_refund子テーブルで表現paid → refundedのような新状態は導入しない- 詳細は SRS-REG-001 §7.2 / §7.4 参照
8. 非機能要件
Section titled “8. 非機能要件”- 状態遷移 API: p95 200ms 以下
expire_tentative_reservationsジョブ: 5 分以内に拾うreservation_event保持期間: 1 年(親 §6.3)
9. セキュリティ・認可
Section titled “9. セキュリティ・認可”9.1 使用する permission キー(8 transition keys 維持)
Section titled “9.1 使用する permission キー(8 transition keys 維持)”| key | 用途 | プリセット初期値 |
|---|---|---|
admin:reservation:read | 利用可能遷移一覧 / 詳細 | 全員 |
admin:reservation:read_timeline | /events イベントタイムライン | owner / manager / receptionist |
admin:reservation:approve_tentative | tentative→confirmed | owner / manager / receptionist |
admin:reservation:decline | tentative→declined | owner / manager |
admin:reservation:cancel_by_store | confirmed/in_service→cancelled_by_store | owner / manager |
admin:reservation:check_in | confirmed→in_service | 全員 |
admin:reservation:complete_service | in_service→service_completed | 全員 |
admin:reservation:finalize_payment | service_completed→paid | owner / manager / receptionist |
admin:reservation:mark_no_show | confirmed→no_show | owner / manager |
admin:reservation:cancel_by_customer_proxy | 顧客取消代理入力 | owner / manager / receptionist |
admin:reservation:reopen | 中間状態 1 段戻し | owner / manager |
resolver pseudocode:
function resolveReservationTransitionPermission({ from, to, reopen }) { if (reopen) return PERMISSIONS.ADMIN_RESERVATION_REOPEN; switch (to) { case 'confirmed': // from='tentative' → approve_tentative // from='in_service' → reopen (上で処理済) return PERMISSIONS.ADMIN_RESERVATION_APPROVE_TENTATIVE; case 'declined': return PERMISSIONS.ADMIN_RESERVATION_DECLINE; case 'in_service': return PERMISSIONS.ADMIN_RESERVATION_CHECK_IN; case 'service_completed': return PERMISSIONS.ADMIN_RESERVATION_COMPLETE_SERVICE; case 'paid': return PERMISSIONS.ADMIN_RESERVATION_FINALIZE_PAYMENT; case 'cancelled_by_customer': return PERMISSIONS.ADMIN_RESERVATION_CANCEL_BY_CUSTOMER_PROXY; case 'cancelled_by_store': return PERMISSIONS.ADMIN_RESERVATION_CANCEL_BY_STORE; case 'no_show': return PERMISSIONS.ADMIN_RESERVATION_MARK_NO_SHOW; }}9.2 RLS
Section titled “9.2 RLS”reservation_event は新規テーブルとしてすでに 0008 で導入済(store_id RLS ENABLE + FORCE)。app に SELECT/INSERT のみ GRANT、UPDATE/DELETE は GRANT しない。
10. 受け入れ基準(Given-When-Then)
Section titled “10. 受け入れ基準(Given-When-Then)”- GWT-1 標準導線: tentative→confirmed→in_service→service_completed→paid を順に遷移、各ステップで
reservation_event1 行追加 - GWT-2 不正遷移: tentative→in_service は 422
- GWT-3 不正遷移: tentative→cancelled_by_store は 422
- GWT-4 終端不可: paid からの全遷移は 422
- GWT-5 自動失効:
tentative_expires_at < now()の tentative がジョブで expired 化、actor_kind='system',operator_id=NULL - GWT-6 巻き戻し成功:
admin:reservation:reopen持ちで service_completed→in_service がreopen=trueで成功 - GWT-7 巻き戻し権限なし: 403
- GWT-8 巻き戻しフラグ未指定: 422
REOPEN_FLAG_REQUIRED - GWT-9 no_show 猶予未経過: 422
GRACE_PERIOD_NOT_PASSED - GWT-10 no_show 猶予経過後: 成功
- GWT-11 楽観ロック: 古い version → 409
- GWT-12 RES-005 整合: 終端は existing 判定対象外
- GWT-13 設備リリース: cancelled_by_store 後、equipment_assignments.is_active=false
- GWT-14 監査二重書き: operator 起点遷移で
reservation_eventとoperator_action_logが 1 行ずつ - GWT-15 customer 起点例外: customer 起点遷移で
reservation_eventのみ、operator_action_logには書かれない - GWT-16 RLS: 店舗 X セッションは店舗 Y
reservation_eventを読めない - GWT-17 利用可能遷移 API (confirmed): 候補リスト + permission フィルタ後 enabled 状態
- GWT-18 利用可能遷移 API (tentative): cancelled_by_store は INVALID なので返らない
- GWT-19 customer 起点遷移:
reservation_event.trigger='customer', customer_id NOT NULL - GWT-20 actor CHECK 違反:
trigger='operator' AND operator_id IS NULLで INSERT → DB エラー - GWT-21 tentative_expires_at 不変条件: tentative 作成時セット、
tentative→confirmedで同 tx 内 NULL 化 - GWT-22 actor_kind:
operator_action_log行がactor_kind='operator'または'system'で記録される - GWT-23 二重書き失敗 rollback:
operator_action_logINSERT 失敗時、reservationsUPDATE とreservation_eventINSERT も rollback
11. テスト計画
Section titled “11. テスト計画”- Unit (
packages/domain/reservation/state-machine.ts): 10×10 マトリクスの table-driven、終端不可、reopen、grace、reopen flag、resolver 8 keys - Unit (
packages/auth/reservation-transitions.ts): resolver 純関数の table-driven - Integration (
apps/api): GWT-1〜23、CHECK 違反、二重書き rollback、RLS - Job test:
expire_tentative_reservationsを fake clock で発火 - Contract:
/transitionsおよび/transitions/availableの zod schema
12. 関連ジョブ(graphile-worker)
Section titled “12. 関連ジョブ(graphile-worker)”expire-tentative-reservations(system scheduler, cron 5 分)expire-tentative-for-store(store-scoped fan-out)
詳細は SRS-WRK-001。
13. Open Questions
Section titled “13. Open Questions”| # | 内容 | 状態 |
|---|---|---|
| OQ-RES-004-01 | 終端からの reopen を Phase 2 で許すか | Open(Phase 1 不採用、運用後再検討) |
| OQ-RES-004-02 | confirmed の自動 no_show マーク | Open(Phase 2) |
| OQ-RES-004-03 | LIFF 顧客操作で起こせる遷移の権限境界 | Open(SRS-LIFF-002) |
| OQ-RES-004-04 | paid 予約の修正・返金 | Closed(REG-001 visit.refunded_amount + visit_refund で吸収) |
| OQ-RES-004-06 | reservation_event のサイズ管理(archive) | Open(Phase 3) |
| OQ-RES-004-08 | reservation_event の安定識別子 | Open(Phase 2 UI 拡充時) |
14. 変更履歴
Section titled “14. 変更履歴”| Version | Date | Author | Change |
|---|---|---|---|
| 0.1 | 2026-04-25 | yudai | 初版起票 |
| 0.2 | 2026-04-25 | yudai | 11→10 状態縮約、declined 一本化、no_show grace、actor 分離、認可ミドルウェア責務 |
| 0.3 | 2026-05-05 | yudai (with Codex co-design) | Parent v0.5 同期。reservation_event_actor_polymorphism CHECK の論理欠陥修正(trigger=‘operator’ で operator_id NOT NULL 強制)。operator_action_log.actor_kind 採用。operator_action_log 二重書き責務を §7.7 / §7.10 に明記。permission resolver は packages/auth、Hono middleware は apps/api(layer 分離)。OQ-RES-004-04 を visit_refund (REG-001) で closed。force_* 命名整理(reservation 系は force_business_hours / force_staff_concurrency)。migration 番号 0019 確定 |