コンテンツにスキップ

0016. 横乗り (当日ルート単位の同乗者)

コンテキストと課題

回収業務では、ドライバー 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_idDB の check 制約 でも担保し、候補算出 computeRideAlongCandidates でも除外する。
  • 複数可・重複不可: テナント全メンバー (admin / normal / driver) から 複数選択可。 同一ルートへ同じ人を二重登録しないよう unique (tenant_id, dispatch_date, driver_id, profile_id)
  • グルーピング: groupRideAlongsByDriverdriver_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 と同様)。

影響 (Consequences)

良い点

  • 同乗者を配車ルートから 独立 させたことで、dispatch_stops の生成 RPC・stop_order・D&D・ 完了状態に一切影響を与えずに付与・表示できる (責務分離)。
  • 紐付けを driver_id × dispatch_date にしたことで、ボード上では driverId で束ねるだけで列ヘッダーの チップになり、実装がシンプル。
  • 本人除外・二重登録防止を DB 制約 (check / unique) と純粋ロジック (候補算出) の 両方 で担保。
  • 越境参照検証を WITH CHECK の exists で行うため、他テナントの profilesdriver_id / profile_id に紐付けられない (FK は存在のみ保証で越境を防げないため)。

トレードオフ / 注意点

  • 横乗りは「同行者」の記録であり、車両割当や capacity 計算には関与しない (別の車両を使わない前提)。 車両を伴う増車が必要な場合は配車ルート側の操作になる。
  • ストップ単位ではなくルート単位のため、「特定の回収先だけ同行」のような粒度は表現しない (運用上の割り切り)。→ この割り切りは per-stop 横乗り (mig158) で解消。下記「追補 (2026-07-24)」参照
  • 列ヘッダーにチップが増えると視覚的に密になりうるが、当日 1 ルートあたりの同乗者は少数の想定。

根拠 (典拠)

  • supabase/migrations/00000000000057_dispatch_ride_alongs.sql:25-37 — テーブル定義 (driver_id / profile_idprofiles on delete cascade、created_by nullable、driver_id <> profile_id check、unique (tenant_id, dispatch_date, driver_id, profile_id))。
  • supabase/migrations/00000000000057_dispatch_ride_alongs.sql:42-46ensure_collector_tenant トリガ (collector テナント担保)。
  • supabase/migrations/00000000000057_dispatch_ride_alongs.sql:51-91 — RLS (SELECT テナント全件 / 書込 admin・normal + WITH CHECK で driver_idprofile_id の同一テナントを exists 検証 / service_role)。
  • apps/web/src/lib/dispatch-ride-along.ts:33-41groupRideAlongsByDriver (driverId 単位グルーピング・登録順保持)。
  • apps/web/src/lib/dispatch-ride-along.ts:48-56computeRideAlongCandidates (全メンバー − ドライバー本人 − 既存 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・二次搬出・応援は非破壊)。

追加の決定

  1. per-stop 横乗り (dispatch_stop_ride_alongs・mig158): 「特定の回収先だけ同行」を、当日配車ボードで 回収先 (stop) 単位 に設定できるようにする。full-day 横乗り (driver × date 単位) とは 別表で併存 する。
  2. データモデル: 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)
  3. 本人除外は mig57 と同思想: その stop の担当ドライバー本人は横乗りに登録できない (profile_id = stop.driver_id を fill トリガで raise)。
  4. 横乗りルートの可視化: 横乗りする本人が、同乗するドライバーの 本日ルートを閲覧専用 で見られる (CollectorDriverHome)。full-day + per-stop を統合し、自分が実際に同乗するドライバーのルートだけ を表示する。

セキュリティ / 不変条件

  • サーバ所有列 / 越境: tenant_id/created_by/created_atdispatch_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/myRideAlongStopIdsprofileId === 自分 のみを集計 (myProfileId=null は空集合)。横乗り関係クエリのエラー時は fail-closed (空集合=失効反映)。閲覧は read-only。再割当で生じた自己同乗の残行 (driverId === profileId) は表示ヘルパーで除外。横乗りルートは既取得の tenant-wide allStops (応援と共用) から 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.tsdriverIdsIRideToday/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)」— 機械的不変条件。