コンテンツにスキップ

顧客担当スタッフリンク (operator_staff_link)

顧客担当スタッフリンク (operator_staff_link)

Section titled “顧客担当スタッフリンク (operator_staff_link)”

Document ID: SRS-CUS-004 Parent: SRS-ROOT-001 v0.5 Version: 0.1 Status: Draft Last Updated: 2026-05-05 Depends on: SRS-TEN-002 v0.2, SRS-TEN-003 v0.2, SRS-MST-001 v0.6 依存される: SRS-MST-001(retire 同期), SRS-RES-001 (self-scope), SRS-CUS-001/002 (self-scope), SRS-RES-003 (self-scope)

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


オペレーター(認証主体)と業務スタッフ(施術担当者)の対応関係を operator_staff_link で管理する。 親 SRS §4.1 の **「スタイリストは自分関連の予約・顧客のみ閲覧」**ペルソナ要件を Phase 1 で成立させるための基盤。

Phase 1 では link は API で管理されるが、self-scope の業務適用範囲は最小限(カレンダーの「自分担当のみフィルタ」のオプション提供のみ)。Phase 2 で予約 / 顧客 / メモの read 権限境界に拡張する。


  • As a owner, I want to オペレーターを業務スタッフに紐付けたい, so that 「自分担当」フィルタを使える
  • As a スタイリスト, I want to カレンダーで自分担当の予約のみ表示できるオプションが欲しい, so that 朝確認の効率を上げる
  • As a manager, I want to オペレーターの retire 時に link を解除したい, so that orphan link を残さない

  1. owner / manager が「設定 > オペレーター」を開く
  2. 対象 operator の detail で「業務スタッフを紐付け」を選ぶ
  3. 同店舗の active staff 一覧から選択
  4. POST /api/admin/operator-staff-links で link を作成
  5. 以後、当該 operator は self-scope オプションを使える
  • AF-1 link 解除: DELETE /api/admin/operator-staff-links/:linkId(実装は ended_at セット)
  • AF-2 staff retire 時: MST-001 の retire ハンドラが listener で active link の ended_at をセット
  • AF-3 同 operator の link 切替: 旧 link を end → 新 link を作成
  • EF-1 同 operator が同 store で複数 active link: 409 OPERATOR_STAFF_LINK.MULTIPLE_ACTIVE
  • EF-2 link 対象 staff が他店舗: 404
  • EF-3 link 対象 operator が他店舗 link 不在: 404 OPERATOR_STAFF_LINK.OPERATOR_NOT_LINKED
  • EF-4 self-scope 適用済みの operator の link を解除: 警告のみ、解除可(次 request から self-scope 失効)

operator detail の中で、業務スタッフ紐付けを管理する。

  • operator detail panel
  • 「業務スタッフを紐付け」ドロップダウン
  • 現在の link 状態表示
  • link 履歴(過去の link は ended_at で表示)
  • 同 operator が同 store で active link を持つのは 1 件のみ
  • staff は同 store の active staff のみ選択可

MethodPath用途permission
GET/api/admin/operator-staff-links?operator_id=&staff_id=&active_only=link 一覧admin:operator_staff_link:read
POST/api/admin/operator-staff-linkslink 作成admin:operator_staff_link:write
DELETE/api/admin/operator-staff-links/:linkIdlink 終了admin:operator_staff_link:write
GET/api/admin/auth/me/staff-link自分の active link 取得session のみ

POST /api/admin/operator-staff-links:

  • operator_id: uuid
  • staff_id: uuid

response:

  • data.link.{operator_id, store_id, staff_id, created_at, ended_at}
  • OPERATOR_STAFF_LINK.MULTIPLE_ACTIVE
  • OPERATOR_STAFF_LINK.OPERATOR_NOT_LINKED
  • OPERATOR_STAFF_LINK.STAFF_NOT_FOUND
  • OPERATOR_STAFF_LINK.NOT_FOUND

カラム制約
store_iduuidNOT NULL, FK
operator_iduuidNOT NULL, 複合 FK → operator_store_link(store_id, operator_id)
staff_iduuidNOT NULL, 複合 FK → staff(store_id, id)
created_attimestamptzNOT NULL
ended_attimestamptzNULL
PK(store_id, operator_id, COALESCE(ended_at, 'epoch')) ではなく実装は id uuid PK + UNIQUE(store_id, operator_id) WHERE ended_at IS NULL

実装上の選択:

  • 独立 id uuid を持ち(履歴を 1 行ごとに識別可能)
  • UNIQUE(store_id, operator_id) WHERE ended_at IS NULL で active 1 件制約
  • UNIQUE(store_id, staff_id) WHERE ended_at IS NULL で同 staff に同時 1 operator 制約(self-scope の意味論を破らないため)
カラム制約
iduuidPK
store_iduuidNOT NULL, FK
operator_iduuidNOT NULL, 複合 FK
staff_iduuidNOT NULL, 複合 FK
created_attimestamptzNOT NULL
ended_attimestamptzNULL
制約UNIQUE(store_id, id)
制約UNIQUE(store_id, operator_id) WHERE ended_at IS NULL
制約UNIQUE(store_id, staff_id) WHERE ended_at IS NULL

0013_operator_membership_and_staff_link.sql (TEN-003 と共有) で:

  1. operator_store_link 作成(TEN-003 担当)
  2. operator_staff_link 作成(本SRS担当)
  3. RLS ENABLE + FORCE
  4. app ロールへ SELECT/INSERT/UPDATE GRANT(DELETE なし、ended_at で論理削除)
  5. permission backfill: admin:operator_staff_link:read, admin:operator_staff_link:write

  • 同 store 内で同 operator は同時に 1 link のみ active
  • 同 store 内で同 staff も同時に 1 operator にのみ link 可
  • 切替は「旧 link end → 新 link create」の順で行う
  • staff.retire ハンドラ(MST-001)は active operator_staff_linkended_at = now() をセットする
  • listener で実装、retire の transaction 内で実行
  • listener 失敗時は staff retire も rollback
  • カレンダー (RES-001) の「自分担当のみ」フィルタ表示で利用
  • 予約・顧客・メモの read 権限境界には Phase 1 では適用しない(API はすべて店舗全体 view を返す)
  • Phase 2 で self-scope 強制を導入する場合は別 SRS で OQ 解消
  • operator_staff_link.create / end
  • operator_staff_link.end_by_staff_retire(retire listener 起動時)

  • link API は p95 200ms
  • self-link API(/api/admin/auth/me/staff-link)はキャッシュ可

key用途初期付与
admin:operator_staff_link:readlink 一覧・self-link 確認owner / manager / staff
admin:operator_staff_link:writelink 作成・終了owner / manager

/api/admin/auth/me/staff-link は session のみで permission 不要(自分の link を見る権利は本人にある)。


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

Section titled “10. 受け入れ基準(Given-When-Then)”
  • GWT-1: link 作成 → operator_staff_link 1 行追加、ended_at IS NULL
  • GWT-2: 同 operator に 2 件目 active link 作成 → 409 OPERATOR_STAFF_LINK.MULTIPLE_ACTIVE
  • GWT-3: 同 staff に別 operator の active link 作成 → 409
  • GWT-4: link 終了 → ended_at セット、UNIQUE 部分インデックスで再作成可能
  • GWT-5: staff retire → 該当 active link が ended_at = now() で終了
  • GWT-6: 自分の link 取得 → session のみで返る
  • GWT-7: 別店舗 staff への link → 404
  • GWT-8: link なし operator が /api/admin/reservations を呼ぶ → 全件返る(self-scope 強制なし、Phase 2 OQ)

  • Unit: link 1:1 制約
  • Integration: retire listener、UNIQUE 部分インデックス、RLS

  • なし(retire listener は MST-001 のハンドラ内で同期実行)

#内容扱い
OQ-CUS-004-01self-scope 強制を Phase 2 で予約・顧客 read に拡張Phase 2 SRS
OQ-CUS-004-021 operator が複数 staff を兼任するケースPhase 2

VersionDateAuthorChange
0.12026-05-05yudai (with Codex co-design)初版。operator_staff_link スキーマ、retire listener、self-scope の Phase 1 範囲を確定。migration 0013 で TEN-003 と共有