# Agent App: Face-to-face Sessions

## What it does

Records a salesperson's declared meeting with a Lead and an assigned independent
YHY Wi-Fi/4G lanyard (`Device::TYPE_BADGE`). The mobile app does not control that
hardware. Phone-call YHY02 BLE devices (`TYPE_BADGE_BLE`) are excluded.

This is a business-session API, not a claim that recording has started. The
existing YHY ingestion, private media storage, transcription and analysis remain
the recording pipeline. No mobile audio upload is introduced here.

## How it works

- Both `/agent-api/agent-f2f` (preferred) and `/api/agent-f2f` are supported.
  Every endpoint requires staff JWT authentication. Ownership is derived from the
  token, never a submitted salesperson ID.
- `GET /devices`: active independent lanyards assigned to the current Admin;
  `device_uuid`, `device_sn`, `label`, `hardware_control: physical_button` only.
- `POST /sessions`: `session_uuid` (client-generated retry identity), `lead_uuid`,
  `device_uuid`. Returns 201 on creation, 200 on identical replay. Server time is
  the start boundary; starting requires connectivity. The Lead must be visible
  under the existing Agent App permission rules. One active F2F session per Admin.
- `GET /sessions`: the owner's active session first, then recent sessions, at
  most 50. `GET /sessions/{uuid}` retrieves one owned session, including older ones.
- `PATCH /sessions/{uuid}`: `status: completed` and an explicit ISO-8601
  `ended_at` with timezone, or `status: canceled`. Completion must be after the
  start, at most 12 hours later, and no more than 30 seconds ahead of server time.
  The client preserves the original end across retries. A closed session cannot
  be reopened or retimed. Cancellation remains possible after reassignment.
- Responses include `session_uuid`, `lead_uuid`, `device_sn`, `status`,
  `started_at`, `ended_at`, `matched_recording_count`, `matching_status`, and
  `hardware_control`. No database IDs, credentials or audio URLs are returned.

### Matching contract

Matching runs after live `createFromYhy` and on completion, so either arrival order
works. It requires exactly one completed window containing the entire recording
(start plus positive known duration), the same Admin and exact device SN, and
current active device ownership and Lead authorization. Device edits since the
session snapshot prevent an automatic match. Dates use the existing F2F app-zone
storage convention; API timestamps carry their offset.

No arbitrary 15-minute nearest-neighbour window is used. Unknown duration/time,
overlap, old imported history, ownership changes, recordings spanning meetings,
and canceled sessions are not guessed. Multiple chunks within one meeting may
all link to that Lead. Short recordings are not filtered out.

Links are fill-only and locked transactionally. A manual Lead edit, including
explicit unlink, sets `lead_assignment_locked`; the matcher must never override
it. Soft-deleted recordings remain deleted. Existing action-plan draft sync is
reused when linking a previously analyzed recording.

Matching errors are reported through Laravel's exception logger without rolling
back the ingested audio. Replaying completion with the same original end retries
matching. An unmatched response deliberately says
`awaiting_recording_or_manual_review`: it is not proof the hardware is uploading.

### UI integration contract

The UI belongs to a separate session. Place Face-to-face beside Call on a Lead;
select an assigned lanyard, create the session, then instruct the salesperson to
start its physical recording. At the end, stop the physical recorder first, then
complete the meeting. Do not label the active meeting as confirmed recording.

Expose retry/cancel recovery for pending requests and manual review when a
recording cannot safely match. Do not restart or cancel the physical recorder
when retrying network requests. A 404 from `/devices` on older deployments means
the feature is unavailable, not "no registered lanyards".

## Deployment and verification

### In-App recording reports (September 15)

- `GET /sessions/{uuid}/recordings`: up to 25 matched recordings, newest first.
  `data.recordings` contains `uuid`, offset-bearing `recorded_at`,
  `duration_seconds` and `processing_status`. `data.older_cursor` may be sent
  back as the positive integer `before`; null means the last page.
- `GET /sessions/{uuid}/recordings/{recordingUuid}/report`: the same report
  contract as phone sessions: `uuid`, `processing_status`, `transcript`,
  `ai_analysis`, `meeting_report`, `recommendations`. Uses the existing shared
  ConversationAnalysis normalizer; does not start transcription or analysis.
- Both are read-only/no-store. Require session ownership, current Lead access,
  and a non-deleted recording still linked to that session, same Admin and same
  Lead. Another agent, reassigned/deleted Lead, manual unlink and deleted recording
  fail closed. No provider metadata, private storage paths or pipeline errors.
- The App displays the recording list and opens its existing four report tabs,
  with Face-to-face metadata and the F2F endpoint (not the phone report endpoint).
  Empty matches remain waiting; processing and failures are shown explicitly.
  Showroom remains available for authorized manual review.
- No additional migration beyond the original F2F session migration below.

- Apply `2026_09_12_000010_create_agent_f2f_sessions_table.php` before the new API
  or matcher is used. Existing vendor ingestion still requires its own YHY
  configuration and queue workers; this feature does not enable it automatically.
- Backend feature tests cover authorization, device type/ownership, idempotency,
  arrival order, explicit UTC conversion, chunks, unsafe windows, manual unlink,
  deletion, reassignment and matcher-failure recovery.
- App 1.0.59 (59) includes the F2F UI. Production checks on September 12 returned
  route-not-found 404s for both `/agent-api/agent-f2f/devices` and `/sessions`:
  the backend release below is required. Real lanyard validation remains pending;
  automated tests and an App upload do not establish end-to-end acceptance.

### Backend release checklist — 2026-09-12

This release is based on master `839b72064` and contains only the F2F task,
not the unrelated assigned-Lead search change or WhatsApp voice configuration.

1. Merge the F2F release PR and use the normal deployment maintenance procedure.
   Run the new migration before serving the updated API, Showroom editing or YHY
   ingestion workers. No historical recordings are reassigned by this migration.
2. Refresh the normal application/route caches and restart long-running workers
   using the existing deployment procedure. Do not reset or seed production data.
3. With a salesperson token, verify both `GET /agent-api/agent-f2f/devices` and
   `GET /agent-api/agent-f2f/sessions` return 200. An empty device list means no
   active independent lanyard is assigned; 404 must not be treated as that case.
4. Use an approved test Lead and assigned Wi-Fi/4G lanyard: start the App meeting,
   start/stop the physical recorder, end the App meeting, then verify the existing
   vendor upload is retained and linked to the intended Lead in Showroom.
   Check both upload-before-completion and upload-after-completion ordering.
5. Check that unrelated recordings, manual assignments/unlinks and canceled
   meetings remain unchanged. Keep ambiguous recordings for manual review.

Validation on this master baseline: **381 tests / 2,540 assertions** passed across
`tests/Feature/AgentApi`, `tests/Feature/F2f` and `tests/Feature/Manage/F2f`, using
PHP 8.4 and the isolated local `petav3_f2f_release_20260912_testing` database.
The existing PHPUnit XML configuration deprecation remains. Prebuilt web assets
were reused for page tests; this is not a new frontend build. Pint passed for all
seven newly added PHP files. No production migration, settings change, hardware
recording or customer message was performed during this validation.

## Related files

- [AgentF2fSession](/src/F2f/AgentF2fSession.php)
- [AgentF2fSessionService](/src/F2f/Services/AgentF2fSessionService.php)
- [AgentF2fSessionsController](/app/Http/Controllers/AgentApi/AgentF2fSessionsController.php)
- [F2fRecordingRepository](/src/F2f/Repositories/F2fRecordingRepository.php)
- [AgentF2fSessionTest](/tests/Feature/AgentApi/AgentF2fSessionTest.php)
- [Existing Showroom ingestion](/docs/modules_handbook/manage/f2f/readMe.md)
