Project

General

Profile

Feature #1542

Updated by Redmine Admin 1 day ago

**Surface:** backend (`api/`) 
 **Depends on:** nothing — see the note below 
 **Blocks:** "API: per-viewer visibility matrix and per-card drag capability" 

 Display-data work only. **No authorization changes in this ticket** — the visibility matrix and drag capability are a separate child, deliberately, because a mistake there leaks another developer's or another tenant's tickets and deserves its own review and test suite. 

 > ### ⚠ Corrected wire names — read before starting 
 > The spec originally said the aggregator emits nothing for assignee/priority. **That was wrong.** `ticket_list` has always carried `assignee` and `priority` as flat **display-name strings**. Retyping them to objects would break every existing consumer, so #1541 added the structured data under **new** keys: 
 > 
 > - `assignee_ref` → `{id, display_name}` (nullable) 
 > - `priority_ref` → `{id, key, name, position}` (nullable) 
 > 
 > **Map from `assignee_ref` / `priority_ref`, not from `assignee` / `priority`.** The flat strings still exist and are untouched — do not remove them, and do not map from them. Our outward `TaskSummaryOut` field names are independent of these wire names; pick what reads best on our side. 
 > Shipped in aggregator PR https://github.com/omdev-tech/aggregator/pull/24 — read `docs/ENRICHMENT.md` there for the authoritative shape. 

 1. Map `assignee_ref`, `priority_ref` `assignee`, `priority` and the existing parent reference from the aggregator through the hexagon: `infrastructure/tasks/aggregator.py:43` → application → `TaskSummaryOut` (`presentation/api/tasks.py:84`). **Priority is greenfield in this repo** — no DTO, no domain object, no i18n keys exist — so give it a real domain representation with a documented ordering. `priority_ref.position` is the rank to order by. ordering, not a passthrough string. 
 2. Add the **board fetch mode**: the view being replaced is paginated, but a board loads whole columns. Return every board column in one response, capped per status, each column carrying `total_count` and `has_more`. 

 **Why this does not hard-depend on the aggregator child.** The aggregator deploys on its own pipeline, so the dashboard will at some point run against an aggregator that has not shipped the new keys. fields. Once this ticket carries the pre-change-payload AC below, that scenario is *covered by a test* rather than avoided by sequencing. The sequencing — so the aggregator work becomes a data-quality follow-through, not a release blocker. Start the aggregator child first; the two may land in either order. 

 This is dashboard gotcha #2 in `CLAUDE.md`: a non-optional field left `None` → `model_validate` raises → 500 instead of a schema-valid empty envelope. 

 **Known upstream limitation to carry, not to fix here:** `priority_ref.position` comes from a single per-poll `/enumerations/issue_priorities.json` call in the aggregator and degrades to `null` if that call fails. Our ordering must tolerate a null position. Also, a Redmine version that omits `type` from `assigned_to` makes a group assignee indistinguishable from a user, so `assignee_ref` may occasionally name a group — do not assume it is a person. 

 **Proposed cap: N = 50 tickets per status.** The Confirm with the PO before building; the number must end up in one named constant, documented. 

 ## Acceptance criteria 
 - [ ] `TaskSummaryOut` exposes assignee `assignee` (id, display name — nullable), priority `priority` (id/key, name, position name — nullable) and parent `parent` (id, subject — nullable), alongside everything the list already returns. 
 - [ ] Mapping reads `assignee_ref` / `priority_ref`; a test asserts the flat `assignee` / `priority` strings are NOT the source, and that both still pass through untouched for any existing consumer. 
 - [ ] Board mode returns all board statuses in ONE response, capped at N per status, each column carrying `total_count` and `has_more`. Test: seed 120 tickets in one status → that column holds exactly N items, `total_count` 120, `has_more` true; a 3-ticket status has `has_more` false. 
 - [ ] **Pre-change aggregator resilience:** a unit test feeds a captured pre-change aggregator payload (`assignee_ref`/`priority_ref` (assignee/priority keys entirely absent) and a second with explicit nulls; both return a schema-valid 200 with null fields and no `model_validate` error. The test fails if either field is made required. 
 - [ ] A `priority_ref` whose `position` is null still maps and orders deterministically (test). 
 - [ ] A degraded/down/500 aggregator still returns the schema-valid empty envelope with the requested params stamped, matching the existing `_normalize_*` guards — never a 500. 
 - [ ] Call-count assertion: building a 120-ticket board issues exactly **one** aggregator call and zero per-ticket detail lookups (mocked transport asserts the count). 
 - [ ] Priority has a domain representation with a documented ordering; an unknown or absent priority degrades to `null` rather than raising. 
 - [ ] Aggregator DTOs do not reach the domain layer; mapping stays in infrastructure/application. 
 - [ ] The endpoint neither computes, stores nor returns any complexity or price field. 
 - [ ] Gates green: `python -m pytest -q` (**run serially — one shared Postgres**), `ruff check .`, `mypy src`.

Back