Skip to main content

Technician & TenantUser Lifecycle — Planning Doc

Status: Draft, started 2026-08-17. Blocked on confirming TRA-271's full scope before the target-state section is finalized — what's below is the confirmed current-state picture only.

Why this exists: TRA-403/404 (backend correctness — the domain-bypass pattern TRA-359 already fixed for Tenant) and TRA-271 (the user-facing edit/remove feature, filed back in June) appear to describe the same underlying gap from two different directions. Worth mapping properly before writing any code, rather than risk building the backend fix and the UI feature as two separate, uncoordinated efforts.


Part 1 — Confirmed current state (from TRA-402's audit and TRA-403/404's own scoping)

Technician

  • Every technician created in production skips all 5 of Create()'s guards — no confirmation any existing row actually satisfies them
  • No way to deactivate/reactivate a technician at all today
  • Skill management methods exist on the aggregate, never callable — Skills is still eagerly loaded on every read regardless

TenantUser

  • No way to change a team member's role today — not bypassed, genuinely absent
  • Activate/Deactivate bypass the aggregate the same way Technician's does
  • Domain event doc comments still reference "Keycloak" — stale, this project uses Entra External ID

Part 2 — TRA-271's full scope (confirmed, complete)

Already explicitly linked in Linear to TRA-403/404 — confirms this was always meant to be understood as one body of work, not three separate efforts. Also marked as a duplicate of TRA-190, not yet reviewed — worth a quick check before implementation starts, flagged here so it isn't forgotten.

Edit (TenantAdmin only):

  • User: FirstName, LastName, Role
  • Technician: Phone, Status

Remove (TenantAdmin only, real hard delete — not the same as Deactivate):

  • Deletes the TenantUser record and corresponding Technician record if applicable
  • Deletes the Entra External ID account via Graph API — explicitly required, per TRA-271's own notes, or re-inviting the same email later fails with a duplicate-user error (the exact class of problem the whole TRA-375/377/384/389 Entra saga was about)
  • Confirmation dialog required; cannot remove yourself or another TenantAdmin

Explicitly both needed, not one replacing the other: Deactivate (soft — blocks login, keeps the record) and Remove (hard — permanently deletes everything, including the real Entra identity) are two distinct, both-required capabilities.

A real, useful fact from TRA-271's own notes, worth keeping: Technician records are auto-created at invite time with Phone = null — confirms exactly why the edit capability is needed, not just a nice-to-have.


Part 3 — Target state and sequenced task list

Why this order, not just ticket-number order:

  1. Tier 1 first — these are already-confirmed bugs with an already-proven fix pattern (TRA-359). Lowest risk, most mechanical, and gets the aggregates into a correctly-guarded state before anything else builds on top of them.
  2. Tier 2 next — genuinely new capabilities, but still contained to this app's own database. Real work, but nothing irreversible.
  3. Tier 3 last, deliberately — the one piece with real, permanent, cross-system consequences (deleting a real Entra identity). Building this on top of an already-correct, already-tested Tier 1/2 foundation is safer than building it first and hoping the ground underneath holds.

Also worth resolving before Tier 3 specifically: decide whether "Remove"'s Graph API call reuses DeactivateUserAsync's existing pattern (extend IGraphUserService with a real, explicit DeleteUserAsync) or needs its own, separately-reviewed method — given how deliberately DeactivateUserAsync was scoped during TRA-375, this shouldn't just be bolted on without the same care.

Not decided here, worth a real conversation before Tier 3 starts: does "Remove" happen synchronously in the request, or does it need the same kind of resilient, retry-safe background handling ProvisionTenantJob uses for the creation side of Entra identity management? A failure partway through (DB rows deleted, Graph API call fails) would leave a real, orphaned mess — worth designing deliberately, not assuming a simple synchronous call is safe.