メニュー管理
メニュー管理
Section titled “メニュー管理”Document ID: SRS-MST-002 Parent: SRS-ROOT-001 v0.5 Version: 0.2 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-005 依存される: SRS-RES-002 v0.5, SRS-RES-003, SRS-RES-005 v0.4
本書は SRS-ROOT-001 v0.5 に従う。
店舗スコープのメニューマスタを管理する。Phase 1 で扱うのは以下の 4 実体。
menu_category— 1 段カテゴリmenu— 予約可能なメニュー本体menu_staff_eligibility— スタッフ対応ホワイトリストmenu_equipment_requirement— 設備要件(equipment_group_id参照、quantity 列を持つ)
本SRSは予約作成時に必要な以下の canonical source である。
- 料金スナップショット契約
- 所要時間契約
- 担当可能スタッフ判定契約
- 設備要件契約(group + 数量)
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 設備自動割当が機能する
- As a 受付, I want to 退役メニューを新規予約候補から外したい, so that 過去履歴を壊さず現行運用だけ整理できる
- As a 店長, I want to カテゴリ単位で表示順を整えたい, so that 予約入力 UI の探索コストを下げられる
3. ユースケース
Section titled “3. ユースケース”3.1 主シナリオ(メニュー作成)
Section titled “3.1 主シナリオ(メニュー作成)”- オペレーターが「設定 > メニュー」を開く
- カテゴリを選択、または新規作成する
name,duration_minutes,price_excl_tax,tax_rate_pct,buffer_minutes_after,staff_assignment_policyを入力する- 保存時にサーバが
tax_amount,price_incl_taxをstore_settings.rounding_policyに従って算出する - 新メニューをカテゴリ末尾へ追加する
operator_action_logにmenu.createを記録する
3.2 代替フロー
Section titled “3.2 代替フロー”- AF-1 カテゴリ作成:
menu_categoryを追加し、そのカテゴリにメニューを紐付ける - AF-2 スタッフ対応更新:
PUT /api/admin/menus/:menuId/staff-eligibilityで staff 集合を bulk replace - AF-3 設備要件更新:
PUT /api/admin/menus/:menuId/equipment-requirementsで{ equipment_group_id, quantity }[]を bulk replace - AF-4 メニュー並び替え: 同一カテゴリ内で reorder
- AF-5 カテゴリ並び替え:
menu_category.display_orderを collection ETag 付きで更新 - AF-6 退役:
terminated_atを立てて新規予約候補から外す - AF-7 復活:
terminated_at = NULLに戻し、カテゴリ末尾へ戻す
3.3 例外フロー
Section titled “3.3 例外フロー”- EF-1
If-Match不一致: 409MENU.VERSION_MISMATCH - EF-2 reorder の collection ETag 不一致: 409
MENU.COLLECTION_ETAG_MISMATCH - EF-3
duration_minutes < 1: 422MENU.DURATION_INVALID - EF-4
buffer_minutes_after > 240: 422MENU.BUFFER_INVALID - EF-5
price_excl_tax < 0: 422MENU.PRICE_INVALID - EF-6
staff_assignment_policy='whitelist'かつ eligibility 0 件: 保存自体は許可、ただし RES-002 で予約投入不可 - EF-7 設備要件で
quantity < 1または同一 group 重複: 422MENU.EQUIPMENT_REQUIREMENT_INVALID - EF-8 他店舗メニュー操作: 404(RLS により非可視)
4. UI仕様
Section titled “4. UI仕様”4.1 画面の目的
Section titled “4.1 画面の目的”カテゴリとメニューを 1 画面で管理し、予約入力で使う順序と利用可否を確定する。
4.2 主要要素
Section titled “4.2 主要要素”- カテゴリ一覧
- 選択カテゴリ配下のメニュー一覧
- メニュー作成・編集ダイアログ
- カテゴリ作成・編集ダイアログ
- スタッフ eligibility 編集モーダル
- 設備 requirement 編集モーダル(group + quantity)
- 退役 / 復活ボタン
- reorder UI
4.3 バリデーション
Section titled “4.3 バリデーション”- カテゴリ名: 1..100 文字、trim 後空文字不可
- メニュー名: 1..200 文字、trim 後空文字不可
duration_minutes: 1..1440buffer_minutes_after: 0..240tax_rate_pct: 8 or 10(zod レベルでガード、DB CHECK は §7.16 で撤去済み)price_excl_tax: 0 以上staff_assignment_policy:any | whitelist- 設備要件:
quantity1..99、同一equipment_group_id重複不可
5. API仕様
Section titled “5. API仕様”5.1 エンドポイント一覧
Section titled “5.1 エンドポイント一覧”| Method | Path | 用途 | permission | 楽観ロック |
|---|---|---|---|---|
| GET | /api/admin/menu-categories | カテゴリ一覧 | admin:menu:read | - |
| POST | /api/admin/menu-categories | カテゴリ作成 | admin:menu:create | - |
| PATCH | /api/admin/menu-categories/:categoryId | カテゴリ更新 | admin:menu:update | If-Match: <version> |
| PUT | /api/admin/menu-categories/order | カテゴリ並び替え | admin:menu:update | If-Match: <collection_etag> |
| GET | /api/admin/menus?limit=&cursor=&category_id=&terminated= | メニュー一覧 | admin:menu:read | - |
| GET | /api/admin/menus/:menuId | メニュー詳細 | admin:menu:read | - |
| POST | /api/admin/menus | メニュー作成 | admin:menu:create | - |
| PATCH | /api/admin/menus/:menuId | メニュー更新 | admin:menu:update | If-Match: <version> |
| PUT | /api/admin/menus/order | 同一カテゴリ内 reorder | admin:menu:update | If-Match: <collection_etag> |
| POST | /api/admin/menus/:menuId/retire | 退役 | admin:menu:retire | If-Match: <version> |
| POST | /api/admin/menus/:menuId/restore | 復活 | admin:menu:retire | If-Match: <version> |
| GET | /api/admin/menus/:menuId/staff-eligibility | 対応スタッフ読取 | admin:menu:read | - |
| PUT | /api/admin/menus/:menuId/staff-eligibility | 対応スタッフ更新 | admin:menu:update | If-Match: <version> |
| GET | /api/admin/menus/:menuId/equipment-requirements | 設備要件読取 | admin:menu:read | - |
| PUT | /api/admin/menus/:menuId/equipment-requirements | 設備要件更新 | admin:menu:update | If-Match: <version> |
menu_category の delete API は Phase 1 で提供しない。
5.2 設備要件 request
Section titled “5.2 設備要件 request”PUT /api/admin/menus/:menuId/equipment-requirements{ requirements: [ { equipment_group_id: uuid, quantity: number /* 1..99 */ } ]}5.3 エラーコード
Section titled “5.3 エラーコード”| code | 意味 | HTTP |
|---|---|---|
MENU.NOT_FOUND | 行なし / RLS越境 | 404 |
MENU.FORBIDDEN | 権限不足 | 403 |
MENU.VERSION_MISMATCH | 行 ETag 不一致 | 409 |
MENU.COLLECTION_ETAG_MISMATCH | reorder ETag 不一致 | 409 |
MENU.DURATION_INVALID | duration 不正 | 422 |
MENU.BUFFER_INVALID | buffer 不正 | 422 |
MENU.PRICE_INVALID | price 不正 | 422 |
MENU.EQUIPMENT_REQUIREMENT_INVALID | 設備要件不正 | 422 |
MENU.CATEGORY_NOT_FOUND | category 不在 | 422 |
6. データモデル影響
Section titled “6. データモデル影響”6.1 スキーマ
Section titled “6.1 スキーマ”menu_category
Section titled “menu_category”| カラム | 型 | 制約 |
|---|---|---|
id | uuid | PK |
store_id | uuid | NOT NULL, FK |
name | varchar(100) | NOT NULL, CHECK btrim(name) <> '' |
display_order | integer | NOT NULL, CHECK >= 1 |
version | integer | NOT NULL DEFAULT 1, CHECK >= 1 |
created_at | timestamptz | NOT NULL |
updated_at | timestamptz | NOT NULL |
| 制約 | UNIQUE(store_id, id) | |
| 制約 | UNIQUE(store_id, display_order) |
| カラム | 型 | 制約 |
|---|---|---|
id | uuid | PK |
store_id | uuid | NOT NULL, FK |
category_id | uuid | NOT NULL, FK (store_id, category_id) → menu_category(store_id, id) |
name | varchar(200) | NOT NULL, CHECK btrim(name) <> '' |
duration_minutes | integer | NOT NULL, CHECK 1..1440 |
buffer_minutes_after | smallint | NOT NULL DEFAULT 0, CHECK 0..240 |
price_excl_tax | bigint | NOT NULL, CHECK >= 0 |
tax_rate_pct | smallint | NOT NULL(DB CHECK は親 §7.16 に従い無し、zod でガード) |
tax_amount | bigint | NOT NULL, CHECK >= 0 |
price_incl_tax | bigint | NOT NULL, CHECK >= 0 |
staff_assignment_policy | varchar(20) | NOT NULL DEFAULT 'any', CHECK IN ('any','whitelist') |
display_order | integer | NOT NULL, CHECK >= 1 |
terminated_at | timestamptz | NULL |
version | integer | NOT NULL DEFAULT 1, CHECK >= 1 |
created_at | timestamptz | NOT NULL |
updated_at | timestamptz | NOT NULL |
| 制約 | UNIQUE(store_id, id) | |
| 制約 | UNIQUE(store_id, category_id, display_order) WHERE terminated_at IS NULL | |
| 制約 | CHECK(price_incl_tax = price_excl_tax + tax_amount) |
menu_staff_eligibility
Section titled “menu_staff_eligibility”| カラム | 型 | 制約 |
|---|---|---|
store_id | uuid | PK, FK |
menu_id | uuid | PK, 複合 FK → menu(store_id, id) |
staff_id | uuid | PK, 複合 FK → staff(store_id, id) |
独立 ID なし。親 §7.14 / §7.2.2 に従う。
menu_equipment_requirement
Section titled “menu_equipment_requirement”| カラム | 型 | 制約 |
|---|---|---|
store_id | uuid | PK, FK |
menu_id | uuid | PK, 複合 FK → menu(store_id, id) |
equipment_group_id | uuid | PK, 複合 FK → equipment_group(store_id, id) |
quantity | smallint | NOT NULL, CHECK 1..99 |
行 1 件 = 「equipment_group_id の設備が quantity 台必要」。
複数行は AND(全て必要)と解釈する。
6.2 マイグレーション計画
Section titled “6.2 マイグレーション計画”0014_menu_master_and_eligibility.sql で以下を 1 ファイルにまとめる。
0006_reservation.sqlの placeholdermenuを本番 schema へ昇格(category_id,duration_minutes,tax_*,staff_assignment_policy,display_order,terminated_at,version追加)menu_category新規作成menu_staff_eligibility新規作成menu_equipment_requirement新規作成(equipment_group_id参照前提だが、equipment_groupの作成は0017で行う。本 migration では先行 FK を pending 状態にするか、0017後に dependant FK を遅延適用する)- RLS ENABLE + FORCE
appロールへSELECT/INSERT/UPDATEを GRANT(DELETE なし、§7.5 物理削除なし)- permission backfill 実行
permission backfill (admin:menu:* 4 keys) は 0014 内で同時実施する。
migration 順序の現実的整理:
0014(menu, menu_category, menu_staff_eligibility, menu_equipment_requirement の RLS / GRANT のみ、equipment_groupへの FK は0017で追加)0017(equipment_group + equipment.group_id) でmenu_equipment_requirement.equipment_group_idへの FK を有効化
7. 業務ルール
Section titled “7. 業務ルール”7.1 lifecycle
Section titled “7.1 lifecycle”- Phase 1 のメニュー有効 / 無効は
terminated_atのみ active booleanは導入しない- 退役メニューは新規予約候補に出さない
- 既存
reservation_menu_lines.menu_id参照は保持する
7.2 価格スナップショット契約(RES-002 提供)
Section titled “7.2 価格スナップショット契約(RES-002 提供)”予約作成時、RES-002 は menu から以下を 必ず reservation_menu_lines にコピーする。
name→menu_name_snapshot(RES-002 v0.5 で追加)duration_minutesprice_excl_taxtax_amountprice_incl_taxtax_rate_pctbuffer_minutes_after(予約レベルreservation.buffer_minutes_afterの最大値計算用)
メニュー改定後も既存予約の金額・時間・名前は不変。
7.3 staff_assignment_policy の意味
Section titled “7.3 staff_assignment_policy の意味”| 値 | 意味 | 予約作成時 |
|---|---|---|
any | 制限なし | 全 staff 可。menu_staff_eligibility 行は無視 |
whitelist | ホワイトリスト | menu_staff_eligibility に存在する staff のみ可 |
whitelist かつ 0 件は 設定上は許可。ただし RES-002 は RESERVATION.MENU_NOT_ELIGIBLE_FOR_STAFF でブロックする。
7.4 複数メニュー時の staff eligibility
Section titled “7.4 複数メニュー時の staff eligibility”1 予約に複数 menu がある場合、指定 staff はすべての menuを満たさなければならない。
for all selected menus m: m.staff_assignment_policy='any' OR exists(menu_staff_eligibility where menu_id=m.id and staff_id=reservation.staff_id)
7.5 menu_equipment_requirement の AND ロジック + 自動割当
Section titled “7.5 menu_equipment_requirement の AND ロジック + 自動割当”- 1 menu に requirement 行が N 件ある場合、N 件すべてが必要
- 各行は
(equipment_group_id, quantity)単位で「この group からquantity台が必要」を意味する - RES-002 は予約保存前に「各要件の group 内に空き concrete equipment を
quantity台確保できるか」を判定する - 自動割当アルゴリズム:
- 各 requirement について、対象 group の active equipment(
bookable=true AND terminated_at IS NULL)をdisplay_order ASC, id ASCで列挙 - 候補時間帯に EXCLUDE 制約と衝突しない equipment を
quantity台選ぶ - 不足なら
RESERVATION.NO_EQUIPMENT_AVAILABLE422
- 各 requirement について、対象 group の active equipment(
- RES-005 の EXCLUDE 制約は concrete
equipment_id単位で機能するため、group 概念とは独立
7.6 reorder
Section titled “7.6 reorder”- カテゴリ reorder は store 全体 collection ETag
- menu reorder は同一
category_id内 collection ETag - no-op reorder は version / updated_at / audit を動かさない
7.7 監査
Section titled “7.7 監査”menu.create / update / retire / restore / reordermenu_category.create / update / reordermenu_staff_eligibility.replacemenu_equipment_requirement.replace
8. 非機能要件
Section titled “8. 非機能要件”GET /api/admin/menusは cursor-based。limit既定 50、上限 100- reorder と bulk replace は 1 transaction
- メニュー件数上限は Phase 1 で 1000 を目安。それ以上は cursor で十分対応
9. セキュリティ・認可
Section titled “9. セキュリティ・認可”| key | 用途 | 初期付与 |
|---|---|---|
admin:menu:read | menu / category 読取 | 全 role |
admin:menu:create | menu / category 作成 | owner / manager |
admin:menu:update | menu / category 更新、eligibility / requirement 更新、reorder | owner / manager |
admin:menu:retire | menu 退役・復帰 | owner / manager |
RLS / FORCE / 複合 FK / GRANT 明示は全 4 テーブルで必須。
10. 受け入れ基準(Given-When-Then)
Section titled “10. 受け入れ基準(Given-When-Then)”- Given category が存在 / When menu を作成 / Then category 末尾に
display_order付きで追加される - Given active menu が多数存在 / When 一覧取得 / Then cursor pagination で返る
- Given
price_excl_tax = 0/ When 保存 / Then 作成成功する - Given
buffer_minutes_after = 240/ When 保存 / Then 作成成功する - Given
buffer_minutes_after = 241/ When 保存 / ThenMENU.BUFFER_INVALID - Given
duration_minutes = 0/ When 保存 / ThenMENU.DURATION_INVALID - Given row version=3 / When
If-Match: 3で更新 / Then version=4 で更新される - Given row version=3 / When
If-Match: 2で更新 / Then 409MENU.VERSION_MISMATCH - Given category A の active menu 並び / When reorder / Then 同一 category 内で 1-based 連番に再採番される
- Given category collection ETag stale / When category reorder / Then 409
MENU.COLLECTION_ETAG_MISMATCH - Given menu を退役 / When active list 取得 / Then 候補に出ない
- Given retired menu / When restore / Then category 末尾へ戻る
- Given
staff_assignment_policy='any'かつ eligibility 行あり / When RES-002 が staff 判定 / Then eligibility 行は無視される - Given
staff_assignment_policy='whitelist'かつ staff A 行あり / When staff A で予約作成 / Then eligibility check を通る - Given
staff_assignment_policy='whitelist'かつ staff A 行なし / When staff A で予約作成 / ThenRESERVATION.MENU_NOT_ELIGIBLE_FOR_STAFF - Given eligibility 更新 API / When
staff_idsを差し替え / Then junction table が payload と一致する - Given requirement 更新 API / When
{group_id, quantity}を差し替え / Then junction table が payload と一致する - Given group A から quantity=2 要件 / When group A に空き 2 台あり / Then 予約成功し 2 台が自動割当される
- Given group A から quantity=2 要件 / When 空き 1 台のみ / Then
RESERVATION.NO_EQUIPMENT_AVAILABLE - Given menu で予約作成済 / When menu の価格を後日更新 / Then 既存
reservation_menu_lines.price_*は変わらず、menu_name_snapshotも変わらない - Given 店舗 X セッション / When 店舗 Y menu を取得 / Then 404 or 0 件
- Given no-op reorder / When 同じ順序を送信 / Then version も audit も増えない
11. テスト計画
Section titled “11. テスト計画”- Unit: price calculation, policy evaluation, reorder payload validation, equipment auto-allocation
- Integration: CRUD, retire/restore, eligibility replace, requirement replace, RLS, optimistic lock
- Cross-SRS integration: RES-002 での price snapshot / eligibility check / equipment 自動割当
- Contract: OpenAPI / zod fixture
12. 関連ジョブ(graphile-worker)
Section titled “12. 関連ジョブ(graphile-worker)”- なし
13. Open Questions
Section titled “13. Open Questions”| # | 内容 | 扱い |
|---|---|---|
| OQ-MST-002-01 | スタッフ指名料(親 OQ-02) | Phase 2 |
| OQ-MST-002-02 | category delete を Phase 2 で提供するか | Phase 2 |
| OQ-MST-002-03 | duration=0 のオプションメニュー | Phase 2 |
| OQ-MST-002-04 | カテゴリ階層 (1 段 → 多段) の拡張 | Phase 2 以降 |
14. 変更履歴
Section titled “14. 変更履歴”| Version | Date | Author | Change |
|---|---|---|---|
| 0.1 | 2026-05-05 | Codex / yudai | 初版ドラフト |
| 0.2 | 2026-05-05 | yudai (with Codex co-design) | Round 2 反映: migration 番号 0014 確定、menu_equipment_requirement を equipment_group_id + quantity 参照へ変更、menu_name_snapshot の RES-002 提供契約を §7.2 に明記、tax_rate_pct DB CHECK 撤去(親 §7.16 連動)、staff_assignment_policy enum 確定、自動割当アルゴリズムを §7.5 に明記 |