Nobody has started this yet — be first.
Business impact
Same failure mode as the last-owner-demotion ticket (a tenant left with zero owners, and no self-service way back), reached through a different door: DELETE /api/tenant/members/<id>. An admin cleaning up "inactive" members, or an owner removing what they think is a duplicate/old account, can remove the tenant's only owner membership entirely -- not demoted, gone -- with the same result: nobody left who can manage billing or close the tenant. Arguably worse than the demotion case, because deleting a membership feels more clearly like "offboarding a person," and it is easier to not notice you just removed the last owner than to notice you are changing the last owner's role.
Problem
remove_member deletes any membership matching (id, tenant_id=g.tenant_uuid) with no check on whether it is the tenant's last owner.
Current behavior
Deleting the sole owner membership for a tenant succeeds with a plain 204, and the tenant is left with zero owner memberships and zero members with billing access -- remove_member never checks the deleted membership's role before committing the delete.
Expected behavior
Deleting the tenant's only owner membership is rejected (409, clear error message). Deleting any other membership (a non-owner, or an owner when other owners exist) is unaffected.
Steps to reproduce
Why this matters
Same underlying gap as the demotion ticket -- an invariant over a set of rows ("this tenant has at least one owner") that only application code, at the moment of the specific mutation that could violate it, can enforce. These are deliberately two separate tickets even though the concept is identical: change_member_role and remove_member are two independent functions, and fixing one does not fix the other.
Suggested approach
Same shape of check as the demotion ticket: before deleting, if the membership being removed is an owner, confirm at least one other owner membership exists for this tenant. If that check was already written for change_member_role, look for a way to share it between both call sites rather than writing it out twice.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_10_last_owner_removal.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b fix/last-owner-removalFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/last-owner-removalSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inacmeglobexmake seed
*.localhost subdomains resolve to 127.0.0.1 out of the box in curl and every modern browser -- no /etc/hosts editing needed:
curl -s -X POST http://acme.localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
--cookie-jar /tmp/acme.cookies --cookie /tmp/acme.cookies \
-d '{"email": "owner@acme.test", "password": "owner-acme"}'
curl -s http://acme.localhost:5000/api/tenant/settings --cookie /tmp/acme.cookies
For tooling that cannot fake subdomains (some HTTP clients, CI), send X-Tenant-Slug instead -- honored only when TRUST_TENANT_HEADER=true (the Docker dev setup enables it; production does not):
curl -s http://localhost:5000/api/tenant/settings -H "X-Tenant-Slug: acme"
You do not strictly need Docker to work the tickets, but -- unlike a project that can fake its backing services -- this one genuinely needs a real Postgres and Redis, because the whole point is proving isolation against the real database, not a mock. Without Docker: cp .env.example .env, point DATABASE_URL/ADMIN_DATABASE_URL/TEST_DATABASE_URL/TEST_ADMIN_DATABASE_URL/REDIS_URL at a local Postgres/Redis (docker/init-db.sql documents the exact roles/databases the app expects -- notably the saas_admin/BYPASSRLS role migrations need and the plain saas role every request uses), then:
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
.venv/bin/flask db upgrade
.venv/bin/pytest practice_tickets/tests/test_ticket_07_webhook_signature_raw_body.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 15, clean pass/fail summary
./practice_tickets/run_tickets.sh 07 12 13 # just a few, by number
All 15 tests fail at the start; as you fix each ticket in app/ (or, for TICKET-12, in migrations/versions/0002_row_level_security.py, plus re-applying it), its test -- and only its test -- should flip to passing. make test runs the project's own main suite; several of its tests (RBAC, tenant-spoofing, and billing-webhook tests among them) fail at the start for the same underlying reasons as specific tickets -- fixing those tickets fixes those failures too, not a separate task.
Level 1
Fix a bug
Read existing behaviour, correct it.