# App project-role assignments

## Scope

PR #167 includes the existing App F2F API, Assigned Lead search, and this adapter for petaV3's existing per-project `engagement_assignments`. No additional migration is required for the role adapter. The original F2F migration remains required.

- A Lead is assigned to an App user when they are its legacy Account Manager (`leads.assigned_admin_id`, a `users.id`) **or** hold any role on a non-deleted engagement for that Lead.
- Roles are not hard-coded to Caller / Closer. Current petaV3 uses `appointment` for the appointment/caller stage and supports administrator-defined roles. Inactive role labels remain displayable, consistent with the web pipeline. Revocation removes the assignment; deleting the engagement also removes its grant.
- This does not grant App-wide Lead search. `/agent-leads/all` and general search retain the separate existing permission. Staff, merged/deleted customer and profile eligibility checks remain in place.
- WhatsApp still requires its existing permission and `LeadVisibility` scope. Assignment filters intersect that scope; selecting a BD never exposes that BD's otherwise hidden customers.

## Responses and older Apps

Lead list/search/detail and WhatsApp list/detail now include an additive `assignments` array. Existing `assigned_salesperson` and WhatsApp `assigned_to` keep their types and single Account Manager meaning, including null when absent.

Each assignment card has:

```json
{
  "uuid": "<staff user UUID>",
  "name": "Salesperson display name",
  "role": "closer",
  "role_label": "Closer",
  "is_me": true,
  "project_uuid": "<project UUID>",
  "project_name": "Project display name",
  "engagement_uuid": "<engagement UUID>"
}
```

Legacy Account Manager cards use `role=account_manager` and null project/engagement fields. Several cards can refer to one employee across different roles or projects; lists and cursor pages still return each Lead once. No commission shares, internal numeric IDs or assignment-write API are exposed.

App clients may render `is_me` cards to show the current salesperson's roles. Publishing the backend does not itself add new role badges to an already-installed App UI. The corrected Assigned/WhatsApp filtering works with the existing requests.

The September 15 Flutter companion change parses this array and shows names,
server-provided role labels and projects in Lead details and WhatsApp lists,
thread context and customer details. Project-only assignments no longer display
as Unassigned. Older servers retain the legacy single-manager fallback. See
[completion release checklist](../COMPLETION-2026-09-15.md).

## Task authorization and revocation

Call creation and F2F session creation/matching reuse the same assignment rule, without relaxing owned-active-recorder checks. Transactional authorization locks the engagement rows before reading their holders, matching the web assignment writer's lock ordering and avoiding stale loaded relations / snapshot reads.

Removing a project role prevents subsequent Lead access, new tasks and subsequent automatic F2F matching. Existing recording ownership, idempotent recovery and manual CRM decisions keep their existing semantics; the change does not erase recordings or retroactively reassign already-matched audio. WhatsApp send workers retain the existing permission/visibility recheck for queued messages.

## Regression coverage

- Project-only assignment: Assigned list, server-side Assigned search, details and WhatsApp BD filters.
- Multiple roles/projects, duplicate-free cursor pagination and configurable role labels.
- Legacy response fields retained; display cards exclude commission/internal fields.
- Outsiders, hidden customers belonging to an otherwise selectable BD, revoked assignments and deleted engagements.
- Call creation after role assignment and rejection after revocation, including revocation at the existing preparation test seam.
- F2F creation/matching for project holders and no later match after revocation.
- Previously queued WhatsApp text is not sent after the role is revoked.

Validation uses the existing isolated local testing database and mocked outbound providers. No production configuration, deployment, real customer messages or App binary publication is part of this change.

### Verified 2026-09-14

- Baseline: master `2fded2fe4` plus PR #167's existing F2F commits and Assigned search `1db4ab631` (cherry-picked onto this PR).
- PHP 8.4 / local MySQL: Agent API + F2F ingestion + Manage/F2F + engagement visibility, **393 tests / 2,670 assertions passed**, including seven new project-assignment cases. One pre-existing PHPUnit XML deprecation, no test failures.
- All 21 PHP files in the PR diff: Pint passed after correcting five formatting issues reported by CI. Changed/new PHP files: syntax checks passed. Test-double signature checker: 56 overridden methods match. `git diff --check` passed.
- The isolated runner needs a test-only `APP_KEY` and adequate PHP memory (`-d memory_limit=1024M`); runs without those encountered environment failures, not production configuration changes.
- Full unrelated petaV3 test-suite health and production physical-lanyard acceptance are not claimed by this focused run.
