Task Breakdown: Team Owner Field & Team Permission — Backend
Mode: real-ticket coverage mapping, not a speculative breakdown. Tickets already exist under TF-3185, created manually ahead of this doc. This is a reconciliation of the RFC's Detail 4.D backend chunks (1–7, 11) against the actual
backend-labeled tickets, so the repo has a record of what's covered, what's a gap, and what's scope beyond the RFC. No effort estimates — tickets are already sized and mostly built.
Scope
Backend only (label: backend under TF-3185). FE (chunks 8–9), Mobile (chunk 10), and
QA tickets are out of scope for this doc.
Coverage verdict
All 12 in-RFC backend chunks are covered by an existing ticket. The migration backfill job (chunk 11 / CHG-004) is covered by TF-3564, split from its own Story TEAM-S20 (TF-3563) — see 2026-07-22 update below. Three additional tickets (TEAM-S18, TEAM-S19-NEG, TEAM-S20) exist in Jira but sit outside the RFC's traced 17-story scope; see Scope additions beyond the RFC.
Chunk → ticket mapping (RFC Detail 4.D, BE chunks)
| # | RFC chunk | Files (per RFC) | Ticket | Status | Coverage |
|---|---|---|---|---|---|
| 1 | Add team_owner_ids field + index migration | contact/base.go; db/migrations/033_*.json | TF-3448 — Adding a new field team_owners on Collection Contacts | Ready to Test | ✅ Covered |
| 2 | List filter $in (bypass $or expansion) | search_contact_request.go | TF-3450 — customers_customers_view enforces TEAM ONLY (TEAM-S02) | Ready to Test | ✅ Covered (bundled into the S02 enforcement ticket) |
| 3 | Extend EvaluatePermissions + team branch | permission_service.go | No standalone ticket — inferred to be implemented as shared logic inside TF-3450 (first enforcement ticket) and reused by 3451–3453 | Ready to Test (inferred) | ⚠️ Likely covered, not separately ticketed — worth a quick code check that EvaluatePermissions was actually extended once, not duplicated per-guard |
| 4 | 6 handler guards + single-get + assoc/delete | contact_handler.go; get_contact.go | TF-3450 (view/get), TF-3451 (manage), TF-3453 (delete), TF-3452 (searchassoc) | Ready to Test | ✅ Covered — one ticket per guarded action, matching TEAM-S02/S04/S06/S08 |
| 5 | Write validation (team_owner_ids ⊆ viewer teams) | contact_sync_request.go; handler | TF-3454 — USER sets Team Owner multi-select (TEAM-S10) | Ready to Test | ✅ Covered |
| 6 | Fallback + events + flag gate | permission_service.go; middleware/config | TF-3488 — Guard team-only level behind enabled_team_permission flag | Ready to Test | ⚠️ Partially covered — ticket description only names the flag gate ("if true, team-only works; if false, treated as everything"). It does not mention the Launchpad-outage fallback-to-OWNED-ONLY behavior or the cdp_team_permission_fallback / cdp_team_permission_denied events from Decision 2 / §3 Monitoring. Recommend confirming in code/PR whether these landed here, in TF-3449, or nowhere. |
| 7 | Notes parent-scope projection + service eval + resolveNotePermission team branch | contact_notes/*; contact_notes_service.go; contact_notes_handler.go | TF-3455 (view, S12), TF-3456 (manage, S14), TF-3457 (delete, S16) | Ready to Test | ✅ Covered |
| — | Teams-for-user(+descendants) LP client + cache (Decision 4, §2.4) | new HTTP client + Redis cache | TF-3449 — Integration between LP service and CDP service (consumes GET /private/teams) | Ready to Test | ✅ Covered — not a numbered Detail 4.D chunk but the dependency the RFC calls out as blocking; ticket path note: description says GET /private/teams, RFC §2.4 pins GET /private/users/{user_sso_id}/teams?include_descendants=true — confirm the implemented path matches the RFC contract, not just team info in general |
| 11 | Migration backfill (CHG-004) — CDP-owned one-off job | new job reading crm_people.crm_team_hierarchy_id → name-map → team_owner_ids | TF-3564 — Migration backfill job, split from Story TEAM-S20 | To Do | ✅ Covered — see note below |
Gap: migration backfill job (chunk 11 / CHG-004) — closed
No ticket under TF-3185 originally covered the CDP-owned backfill job that migrates
existing contacts' legacy crm_team_hierarchy_id into team_owner_ids. This was not a
minor item — the RFC's own success criteria require ≥95% of contacts to have a
non-empty team_owner_ids after migration (§1 Success Criteria #5), and the RFC
review's Priority Actions for GA explicitly call out running this backfill "on a QA
tenant before per-tenant enable." Without it, existing contacts stay Unassigned
(visible to everyone under TEAM ONLY) indefinitely, which undermines the whole feature
for pre-existing data.
Closed 2026-07-21: created TF-3548 (since superseded — see below), parented to Epic
TF-3185, labeled backend, status To Do.
2026-07-22 update — backfill Story and Task recreated
TF-3548 was created story-less (no owning Story, by design — chunk 11 isn't one of the
RFC's traced 17 stories). A jira-alignment-check pass flagged this as worth a decision one
way or the other rather than leaving it an implicit exception. Decision: give the
backfill its own Story-level tracking, same pattern as TEAM-S18/TEAM-S19-NEG below. Full
rationale in
delivery/decisions/0002-separate-migration-backfill-story-and-task.md.
- TF-3548 closed Won't Fix (2026-07-22) — no functional change, purely a traceability/ownership correction. Comment on the ticket records the supersession.
- TF-3563 — new Story, TEAM-S20, "Migrate existing contacts to
team_owner_idsvia one-off backfill job (CHG-004)". - TF-3564 — new Task, same scope/AC as TF-3548, parented to Epic TF-3185 and split from
TF-3563. Verified correct
Work item splitdirection (Task renders "split from" Story).
Scope additions beyond the RFC
Three Jira items exist under TF-3185 that are not part of the RFC's traced 17 stories (TEAM-S01…TEAM-S17, Detail 1.C):
- TF-3458 / TEAM-S18 — "Team Owner auto-syncs to the new owner's teams when contact
ownership changes" (labeled
backend, Ready to Test). - TF-3447 / TEAM-S19-NEG — "TEAM ONLY guard rails (no leakage / correct hierarchy +
Unassigned)" (Story, no
backendlabel, To Do). - TF-3563 / TEAM-S20 — "Migrate existing contacts to
team_owner_idsvia one-off backfill job (CHG-004)" (Story, To Do) — the backfill's own Story, split from by Task TF-3564. See 2026-07-22 update above.
Per product direction, these are a deliberate post-RFC scope amendment: TEAM-S18
adds live re-derivation of team_owner_ids on ownership change, which the RFC's
Decision 1 explicitly did not choose (team_owner_ids is a create-time snapshot —
OQ-2 was resolved in favor of snapshot semantics, not re-sync). Documenting this here so
the discrepancy between the RFC's written decision and the shipped behavior is traceable
rather than silent:
- The RFC (and its R2-reviewed, PROCEED-verdict revision) does not describe or gate
auto-sync-on-ownership-change behind
cdp_team_permission_enabled, analytics, or a rollback path the way the other chunks are. - If TEAM-S18 ships, OQ-2 in the RFC's §5 should be updated to reflect the amended decision (snapshot-at-create → snapshot-with-resync-on-owner-change), and Decision 1's "Consequences" section should note the new re-sync trigger, so a future reader of the RFC isn't misled by the stale "not auto-re-derived on later saves" text.
- TEAM-S19-NEG (guard rails / no-leakage) reads as a test/verification story rather than new functionality — likely fine as an added QA-facing story, not a functional amendment, but flagging since it's also outside Detail 1.C's numbering.
Full ticket ↔ story cross-check (backend-labeled only)
| Ticket | Story ID | In RFC's 17 stories? | RFC chunk |
|---|---|---|---|
| TF-3448 | TEAM-S01 (field) | Yes | 1 |
| TF-3449 | TEAM-S01 (LP integration) | Yes (dependency, §1/§2.4) | n/a (Decision 4) |
| TF-3450 | TEAM-S02 | Yes | 2, 3 (shared), 4 |
| TF-3451 | TEAM-S04 | Yes | 4 |
| TF-3452 | TEAM-S08 | Yes | 4 |
| TF-3453 | TEAM-S06 | Yes | 4 |
| TF-3454 | TEAM-S10 | Yes | 5 |
| TF-3455 | TEAM-S12 | Yes | 7 |
| TF-3456 | TEAM-S14 | Yes | 7 |
| TF-3457 | TEAM-S16 | Yes | 7 |
| TF-3488 | TEAM-S01 (flag) | Yes (CHG-005) | 6 (partial — see gap note) |
| TF-3458 | TEAM-S18 | No — post-RFC amendment | n/a |
| TF-3564 | TEAM-S20 | No — post-RFC amendment | 11 |
Note: TEAM-S03/S05/S07/S09/S11/S13/S15/S17 are the mobile counterparts of the
stories above — they exist in Jira as Story type (TF-3438…TF-3445) with no backend
label, correctly out of scope for this backend breakdown.
Summary
| Count | |
|---|---|
| RFC backend chunks (1–7, 11) + named LP dependency | 9 |
| Covered by an existing ticket | 9 (chunk 11 covered by TF-3564, split from Story TEAM-S20; supersedes the closed TF-3548) |
| Gap (no ticket) | 0 |
| Partially covered, needs confirmation | 2 — chunk 3 (shared EvaluatePermissions, likely bundled), chunk 6 (fallback/events not evidenced in ticket text) |
| Tickets beyond RFC scope | 3 — TF-3458 (TEAM-S18), TF-3447 (TEAM-S19-NEG), TF-3563/TF-3564 (TEAM-S20) |
Bottom line: the manual breakdown is now complete for all in-RFC backend chunks — every enforcement point (view/manage/delete/searchassoc/notes), the core data model change, and the migration backfill job (TF-3564, split from Story TEAM-S20 / TF-3563; supersedes the closed TF-3548) are ticketed. Remaining follow-ups are non-blocking confirmations: the fallback/events piece of chunk 6 is worth a quick look in the actual PR/code to confirm it's not silently missing, and TF-3458's RFC amendment should be reflected back into the RFC's OQ-2/Decision 1 text.