0016. 横乗り (当日ルート単位の同乗者)¶
- ステータス: Accepted(実装済み)
- 関連: 0008 配車コースモデル、0006 工場スコープのアクセス制御
コンテキストと課題¶
回収業務では、ドライバー 1 人に対し 同乗者 (横乗り) が付くことがあります。新人の同行教育や 人手の要る回収先への増員などで、別の車両は使わず ドライバーの当日ルートに「同行する人」を 記録したいという要件です。
これを既存の配車モデル (dispatch_stops) で表現しようとすると噛み合いません。dispatch_stops は
回収先 (collection_site) への訪問 を stop_order で並べたもので、生成 RPC・ドラッグ&ドロップ
編集・完了状態を持ちます。同乗者は「どの回収先に行くか」ではなく「誰のルートに同行するか」だけが
本質で、ストップの並びや生成 RPC・D&D に 干渉させたくありません。
決定¶
横乗りを dispatch_ride_alongs テーブルに独立して持ち、紐付け単位を「ドライバーの当日ルート
(driver_id × dispatch_date)」とする。配車ルート (dispatch_stops / stop_order)・生成 RPC・D&D
には一切干渉しない、当日割当の 付与と表示のみ の機能です。
- データモデル: 1 行 = ある配車日・あるドライバーの当日ルートに同乗する 1 メンバー。
dispatch_ride_alongs (id, tenant_id, dispatch_date, driver_id → profiles, profile_id → profiles, created_by → profiles nullable, created_at)。両 FK はon delete cascade。 - 本人除外: そのルートのドライバー本人は横乗りに選べない (冗長)。
driver_id <> profile_idを DB の check 制約 でも担保し、候補算出computeRideAlongCandidatesでも除外する。 - 複数可・重複不可: テナント全メンバー (admin / normal / driver) から 複数選択可。
同一ルートへ同じ人を二重登録しないよう
unique (tenant_id, dispatch_date, driver_id, profile_id)。 - グルーピング:
groupRideAlongsByDriverがdriver_id単位で束ね、配車ボードの列ヘッダーに 同乗者チップとして表示する (driver_id × dispatch_dateがルート単位のため driverId で束ねれば その列の同乗者一覧になる)。 - 候補算出:
computeRideAlongCandidates= 全メンバー − そのルートのドライバー本人 − 既存の同乗者。 候補が空なら追加導線を出さない。 - RLS (
dispatch_stopsの mig25 / 41 / 55 に倣う):- SELECT: テナント全件 (
tenant_id = current_tenant_id())。driver も自テナントの横乗りを閲覧可。 - 書込 (ALL): admin / normal のみ。WITH CHECK で
driver_id/profile_idが同一テナントに 属することをexistsで検証する (ADR 0017 の 越境参照ハードニング (c) を踏襲)。 - tenant が collector であることは
ensure_collector_tenantトリガで担保 (dispatch_stops と同様)。
- SELECT: テナント全件 (
影響 (Consequences)¶
良い点¶
- 同乗者を配車ルートから 独立 させたことで、
dispatch_stopsの生成 RPC・stop_order・D&D・ 完了状態に一切影響を与えずに付与・表示できる (責務分離)。 - 紐付けを
driver_id × dispatch_dateにしたことで、ボード上では driverId で束ねるだけで列ヘッダーの チップになり、実装がシンプル。 - 本人除外・二重登録防止を DB 制約 (check / unique) と純粋ロジック (候補算出) の 両方 で担保。
- 越境参照検証を WITH CHECK の
existsで行うため、他テナントのprofilesをdriver_id/profile_idに紐付けられない (FK は存在のみ保証で越境を防げないため)。
トレードオフ / 注意点¶
- 横乗りは「同行者」の記録であり、車両割当や capacity 計算には関与しない (別の車両を使わない前提)。 車両を伴う増車が必要な場合は配車ルート側の操作になる。
- ストップ単位ではなくルート単位のため、「特定の回収先だけ同行」のような粒度は表現しない (運用上の割り切り)。→ この割り切りは per-stop 横乗り (mig158) で解消。下記「追補 (2026-07-24)」参照。
- 列ヘッダーにチップが増えると視覚的に密になりうるが、当日 1 ルートあたりの同乗者は少数の想定。
根拠 (典拠)¶
supabase/migrations/00000000000057_dispatch_ride_alongs.sql:25-37— テーブル定義 (driver_id/profile_id→profileson delete cascade、created_bynullable、driver_id <> profile_idcheck、unique (tenant_id, dispatch_date, driver_id, profile_id))。supabase/migrations/00000000000057_dispatch_ride_alongs.sql:42-46—ensure_collector_tenantトリガ (collector テナント担保)。supabase/migrations/00000000000057_dispatch_ride_alongs.sql:51-91— RLS (SELECT テナント全件 / 書込 admin・normal + WITH CHECK でdriver_id・profile_idの同一テナントをexists検証 / service_role)。apps/web/src/lib/dispatch-ride-along.ts:33-41—groupRideAlongsByDriver(driverId 単位グルーピング・登録順保持)。apps/web/src/lib/dispatch-ride-along.ts:48-56—computeRideAlongCandidates(全メンバー − ドライバー本人 − 既存 profileId)。apps/web/src/lib/db/dispatch-ride-alongs.ts:21-66— 読み書きフック (useDispatchRideAlongs/useAddRideAlong/useRemoveRideAlong、SELECT は date でフィルタ・書込は admin/normal)。apps/web/src/pages/collector/dispatch/DispatchBoardPage.tsx:274-276,1080-1105— 列ヘッダーへのチップ表示と追加導線 (RideAlongSection)。
追補 (2026-07-24): per-stop 横乗り + 横乗りルート可視化 (mig158)¶
上記 full-day 横乗り (dispatch_ride_alongs・mig57) は「トレードオフ / 注意点」で stop 単位の粒度は表現しない・同乗者は付与と表示のみ (本人のルート閲覧は無い) と割り切っていた。運用要望を受け、この 2 点を 加法で 補完する (既存 full-day 横乗り・配車ストップ・生成 RPC・D&D・二次搬出・応援は非破壊)。
追加の決定¶
- per-stop 横乗り (
dispatch_stop_ride_alongs・mig158): 「特定の回収先だけ同行」を、当日配車ボードで 回収先 (stop) 単位 に設定できるようにする。full-day 横乗り (driver × date 単位) とは 別表で併存 する。 - データモデル:
dispatch_stop_ride_alongs (id, tenant_id, dispatch_stop_id → dispatch_stops on delete cascade, profile_id → profiles on delete cascade, created_by → profiles on delete set null, created_at)・unique (tenant_id, dispatch_stop_id, profile_id)。 - 本人除外は mig57 と同思想: その stop の担当ドライバー本人は横乗りに登録できない (
profile_id = stop.driver_idを fill トリガで raise)。 - 横乗りルートの可視化: 横乗りする本人が、同乗するドライバーの 本日ルートを閲覧専用 で見られる (
CollectorDriverHome)。full-day + per-stop を統合し、自分が実際に同乗するドライバーのルートだけ を表示する。
セキュリティ / 不変条件¶
- サーバ所有列 / 越境:
tenant_id/created_by/created_atはdispatch_stop_ride_alongs_fill(SECURITY INVOKER・全列発火・search_path='') がサーバ導出。stop/profile の別テナント紐付けと担当ドライバー自己登録を raise。監査列は UPDATE で OLD 保持 (client 偽造/書換不可)。 - RLS: SELECT = 自テナント全 authenticated (driver 含む閲覧)。書込 = admin/normal のみ・越境 WITH CHECK で stop・profile の同一テナントを
exists二重検証 (外側列は完全修飾)。service_role policy は作らない (最小権限)。 - 可視化 scope (漏洩防止):
dispatch_stopsの SELECT はテナント全開放 (mig27) で RLS では止まらないため、表示面で自分の同乗先だけに絞る ことがすべて。driverIdsIRideToday/myRideAlongStopIdsはprofileId === 自分のみを集計 (myProfileId=nullは空集合)。横乗り関係クエリのエラー時は fail-closed (空集合=失効反映)。閲覧は read-only。再割当で生じた自己同乗の残行 (driverId === profileId) は表示ヘルパーで除外。横乗りルートは既取得の tenant-wideallStops(応援と共用) から driver 絞り込みで導出 (N+1 を作らない)。 - 二次搬出取引先の非表示 (同スライスで併載): 二次搬出 stop (
sourceSiteId != null) は配車ボード/本日ルート/工場ボードで取引先名を出さず回収先名のみ (stop-labels.tsの共通マスキング)。通常回収の 2 段表示は不変。
影響¶
- ADR 0016 本文の「stop 単位の粒度は表現しない」割り切りは per-stop 横乗り (mig158) で解消。full-day 横乗りはルート全体の同行に、per-stop は特定回収先だけの同行に使う (2 表併存)。
- 可視化により横乗り者が自分の同乗先ルートを PWA で確認できる。RLS でなく表示面で絞るため、scope の回帰は web ユニット/レンダーテストで固定する。
根拠 (典拠)¶
supabase/migrations/00000000000158_dispatch_stop_ride_alongs.sql— per-stop 表・fill トリガ (越境/自己登録 raise・監査列サーバ所有)・RLS (SELECT テナント全件 / 書込 admin・normal + WITH CHECK exists 二重検証 / service_role policy 無し)・FK on delete (stop/profile cascade・created_by set null)。apps/web/src/lib/dispatch-stop-ride-along.ts—driverIdsIRideToday/myRideAlongStopIds/groupStopRideAlongsByStopId(本人絞り込み・自己同乗除外)。apps/web/src/pages/CollectorDriverHome.tsx— 横乗りルート可視化 (自分の同乗先のみ・fail-closed・allStops 導出)。apps/web/src/lib/stop-labels.ts— 二次搬出取引先マスキング (stopNarrationLabel/destinationSiteDisplay)。scripts/proofs/mig158-dispatch-stop-ride-alongs.sql— R1-R11 (allow/deny・越境・自己登録拒否・監査列・WITH CHECK 独立固定 R10/R10b/R11・FK on-delete)。- AGENTS.md「横乗り可視化 + per-stop 横乗り(不変条件ラチェット・mig158)」— 機械的不変条件。