Project

General

Profile

Actions

Feature #1522

closed
RA

Feature #1510: Implement internal user roles with ticket progression and assignment controls

Domain foundation: internal role columns + target-aware progression module

Feature #1522: Domain foundation: internal role columns + target-aware progression module

Added by Redmine Admin 4 days ago. Updated 4 days ago.

Status:
Shipped
Priority:
Normal
Assignee:
-
Start date:
07/31/2026
Due date:
% Done:

0%

Estimated time:
spec_ref:

docs/superpowers/specs/2026-07-31-internal-roles-design.md

build_status:
build_number:
ci_run_url:
scan_status:
scan_report_url:
deploy_status:
preprod_url:
deployed_at:
branch:
pr_url:
security_key:
severity:
paused:

Description

**Surface:** backend-only

Add `internal_role` and `redmine_user_id` to `clients` (migration + ORM + domain entity + repository mapping), expose `internal_role` on `/auth/me`, and pin the whole progression rule in the pure module `api/src/client_dashboard/domain/tasks/progression.py`.

The module exposes `next_status(current, allowed)`, `may_assign(...)`, a **target-aware** transition predicate, and a `can_move_forward(...)` convenience derived from the same rule. No route, tool or UI consumes it in this subtask — this is the shared vocabulary the other six build on.

Mirrors the existing `domain/projects/claude_access.py` pattern: pure, no I/O.

## Acceptance criteria
- [ ] Alembic revision `0020_internal_roles` adds nullable `internal_role` (developer|cto) and nullable `redmine_user_id` (int) to `clients`; `alembic heads` is run and `down_revision` confirmed against the **actual** current head (expected `0019_claude_access`) — verified, never assumed — with exactly one head after the change.
- [ ] `alembic upgrade head` then `downgrade -1` round-trips cleanly on an ephemeral `pgvector/pgvector:pg16`.
- [ ] ORM model, domain entity and repository carry both fields; existing rows keep NULL and every existing client behaviour is unchanged.
- [ ] `/auth/me` returns `internal_role` (null for an ordinary client), with no UI consuming it yet.
- [ ] `is_admin` is proven independent of `internal_role` in both directions: a CTO is not an admin (403 on `/admin/overview`), and setting a role never touches the platform flag.
- [ ] `domain/tasks/progression.py` is pure — no I/O, no SQLAlchemy, no FastAPI, no infrastructure/presentation imports.
- [ ] Truth table under unit test: developer + assignee + `target == next_status` → allowed; **developer + assignee + any non-next target (backward, a skip, or Shipped(13)) → refused**; developer not the assignee → refused; cto → any target in `allowed_statuses`, forward or backward → allowed; `is_admin` → same as cto; any caller with `target == 16` or `current == 16` → refused.
- [ ] Edge cases: terminal status / no next status in `allowed_statuses` → `next_status` returns None; Blocked(14) is never a "next" and is never skipped over.
- [ ] `may_assign` returns True only for cto and admin; developer and ordinary client → False.
- [ ] `python -m pytest -q` (serial), `ruff check .`, `mypy src` green.

RA Updated by Redmine Admin 4 days ago Actions #1

  • Status changed from Backlog to Submitted
  • spec_ref updated (diff)

RA Updated by Redmine Admin 4 days ago Actions #2

  • Status changed from Submitted to Shipped

Shipped to production in master build #107 (2026-07-31). Migration `0020_internal_roles` applied on prod; `domain/tasks/progression.py` live and pure. The predicate is target-aware — the correction that came out of PO review.

Actions

Also available in: PDF Atom