7720db39fa
Plan 3 of #1303 changed the join-request lookup key from per-user to per-tenant (GetPendingRequestByTenantAndType), but didn't touch existing data or add a structural constraint. Result: any (org, tenant) that had multiple pending rows under the old per-user model now has ambiguous "which one wins" semantics — the LIMIT 1 query returns one row, the others stay pending and resurface in admin lists after the first is approved/rejected. Two fixes: 1. Migration 000045 up.sql now collapses pre-existing pending rows to one per (org, tenant, type), keeping the earliest by created_at. The losers are flagged status='rejected' with a '[Plan 3] superseded' review_message so the audit trail shows it was a system cleanup, not a human reject. down.sql drops the new index but leaves the data mutation in place — we don't have enough information to safely re-promote the discarded rows. 2. Add a partial unique index on (organization_id, tenant_id, request_type) WHERE status='pending' to both the PG migration and the SQLite init schema. This makes the invariant structural so SubmitJoinRequest / RequestRoleUpgrade cannot regress. Approved/rejected history is intentionally unconstrained so the audit trail remains complete.