- Shipped
- August 11, 2026 at 1:16 AM UTC
- Author
- Kamo
- Commit
- 69116e8
Promoting a member to pay for themselves was unconditional, one-way and unsafe to repeat. It now refuses when the organization's policy says the organization pays for everyone — previously the only control over self-payment was the irreversibility of the gesture itself. It is idempotent. Promoting the same member twice inserted a second Account and a second AccountMember row, tripping the unique on member_id and answering 500; a double-click, a retry after a network blip, or an owner re-running the flow to move more licensees all hit it. It now reuses the account and subscription the member already has and carries on to the transfer. demoteBillingOwner is the inverse that never existed. An owner who promoted someone could not undo it, and if that member left, their personally-owned Account kept holding seats targeting the organization with no way to reclaim them. Licensees move back onto the organization's subscription and the self-payer's is stood down; their Account and payment methods are left alone, because reclaiming seats the organization depends on is the owner's business and deleting someone's billing history is not. Matching is on the shared User rather than the member id, since promotion mirrors the member onto the top-level org. Also fixes the endpoint ratchet, which was ALREADY FAILING on main before this work — so SecurityService's build was red and nothing was deploying. The scan could not see two real guards: extractUserId reads userID out of the *** session, and requireTopLevelOwner goes through CallerSessionResolver and further requires the caller own the top-level org. Teaching it both let 25 stale baseline entries be deleted, which is exactly what the file asks for — fixing an endpoint means removing its line, and that is what makes the number fall. runSweep is baselined with its real reason: internal-auth guarded through a header parameter the body-scan cannot reach.