- Shipped
- September 7, 2026 at 2:08 AM UTC
- Author
- Kamo
- Commit
- 4d5d419
A single OWNER_MEMBER_ID could say "this number belongs to Sarah" and nothing else. Real desks are not shaped like that: a support line is answered by four people, and a salesperson carries both their direct line and the shared main number. One owner column forces an administrator to choose between "nobody can see it" and "it belongs to the wrong person". MEMBER_PHONE_NUMBER makes assignment many-to-many and puts the one genuinely singular fact on the join row. IS_PRIMARY is per NUMBER, not per member. It answers the question with exactly one correct value — when a text arrives on this line, whose thread does it land in? VoipNumberOwnershipService used to answer that by refusing whenever two members claimed a number, which was correct and useless: the customer's reply went nowhere. It is deliberately NOT "this member's default number"; a member can be primary on several, and which one they SEND from is a per-message choice, because conflating the two would give away inbound ownership every time somebody changed their outgoing number. Both constraints live in the database, because application-level uniqueness loses a race: unique (MEMBER_ID, ORG_PHONE_NUMBER_ID), and a PARTIAL unique index on (ORG_PHONE_NUMBER_ID) WHERE IS_PRIMARY. Partial, so a line nobody is on call for yet is legal and files its inbound against the organization. Assign promotes the first member automatically — an administrator putting one person on one number plainly means them to receive it. Unassigning the primary promotes the longest-standing remaining holder rather than leaving the line unowned; somebody is still answering it. OWNER_MEMBER_ID survives as a projection of who is primary, maintained here, because inbound routing still derives from it. /capability now returns every line a member may send from, and conversation creation accepts one — validated against the member's own numbers, never substituted. Numbers are shared, so quietly falling back to a different one would send from a line the member may not be on and put the reply in a thread nobody expected. DDL applied by hand, including a backfill from the column it replaces.