# Zoom Opportunity Linkage and Value Implementation Plan

> **READ FIRST — [`2026-08-06-plan-review-corrections.md`](../specs/2026-08-06-plan-review-corrections.md) is a BINDING addendum to this document.** Five independent reviewers found defects that were then verified against the repository. Where the addendum conflicts with anything below, the addendum wins.

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Link each sales Zoom meeting to a human-confirmed existing Pipeline Engagement and display its authoritative Project/stage/commission value without letting AI mutate Pipeline data.

**Architecture:** Add a separate 1:1 `ZoomMeetingOpportunityLink` workflow entity holding suggestion and confirmation state. The model receives only server-listed candidate Engagement UUIDs and may propose one; the server validates membership and a Super Admin confirms. A focused value presenter reuses Booking/Project commission rules for Zoom surfaces.

**Tech Stack:** Laravel/PHP, Eloquent/MySQL, Vue 3/Inertia, existing bounded Zoom AI context, existing Engagement/Booking/Project domain, PHPUnit/Vitest.

## Global Constraints

- Complete and integrate `2026-08-06-action-plan-workflow-hardening.md` before this plan.
- Read `docs/superpowers/specs/2026-08-06-ai-sales-follow-up-workspace-design.md` completely.
- Work in an isolated branch/worktree based on the updated local `dev-chen`; cherry-pick verified commits back locally only.
- Do not push or create a PR.
- Add `features.zoom_opportunity_demo`, false by default; flag-off endpoints 404 and props/UI keys are absent.
- AI can suggest only an Engagement from the server-supplied candidate set for the meeting's linked Lead.
- AI never confirms, creates or edits an Engagement, Project, Booking, commission or assignment.
- Commission comes from existing domain rules, never transcript text.
- No hard-coded 5% analyst share.
- No conversion percentage or uplift.
- Add no request-rate limit in this scope.
- Configure `zoom_opportunity_match` locally through the existing AI Prompt pinning mechanism to OpenAI `gpt-5.6-terra`; do not hard-code provider/model or commit local pin data.
- TDD, atomic commits and independent review after every task.

---

## File map

- Create `database/migrations/2026_08_06_200001_create_zoom_meeting_opportunity_links_table.php`.
- Create `src/Zoom/ZoomMeetingOpportunityLink.php`.
- Modify `src/Zoom/ZoomMeeting.php` and `src/Engagement/Engagement.php` relationships.
- Create `src/Zoom/Repositories/ZoomMeetingOpportunityLinkRepository.php`.
- Create `src/Zoom/Services/ZoomOpportunityCandidateBuilder.php`.
- Create `src/Zoom/Services/ZoomOpportunitySuggestionContextBuilder.php`.
- Create `src/Engagement/Services/EngagementOpportunityValue.php`.
- Create `resources/prompts/zoom_opportunity_match.md`; register prompt constant/config.
- Create requests/controller routes under `app/Http/Requests/Manage/Zoom/Opportunities/` and `app/Http/Controllers/Manage/Zoom/ZoomOpportunityLinksController.php`.
- Create `resources/js/Components/Zoom/OpportunityLinkPanel.vue`.
- Modify `src/Zoom/Services/ZoomRecordingDetailBuilder.php` — append opportunity-link review payload to the existing normalized Zoom detail.
- Modify `resources/js/Components/RecordingDetail/RecordingDetail.vue` — mount the opportunity panel inside the existing shared detail.
- Modify `resources/js/Components/ZoomRecordingDetailModal.vue` — forward Zoom-only review capability/endpoints without creating a competing detail surface.
- Modify `app/Http/Controllers/Manage/Zoom/RecordingsController.php` and `resources/js/Pages/Manage/Zoom/Recordings/Index.vue` for list columns.
- Modify `src/Lead/Services/LeadSalesCoachContextBuilder.php` to include confirmed Pipeline context only.

---

### Task 1: Add the opportunity-link state model

**Files:**
- Create: `database/migrations/2026_08_06_200001_create_zoom_meeting_opportunity_links_table.php`
- Create: `src/Zoom/ZoomMeetingOpportunityLink.php`
- Modify: `src/Zoom/ZoomMeeting.php`
- Modify: `src/Engagement/Engagement.php`
- Test: `tests/Feature/Zoom/ZoomOpportunityLinkFoundationTest.php`

**Interfaces:**
- `ZoomMeeting::opportunityLink(): HasOne`.
- Link constants: `STATUS_UNREVIEWED=1`, `STATUS_CONFIRMED=2`, `STATUS_REJECTED=3`; `CONFIDENCE_HIGH=1`, `MEDIUM=2`, `LOW=3`.
- Unique one row per `zoom_meeting_id`.

- [ ] **Step 1: Write failing table/model/state tests**

Prove integer constants, casts, relations, unique meeting row, soft-delete restore behavior and standard blame columns.

- [ ] **Step 2: Verify failure**

```bash
php artisan test tests/Feature/Zoom/ZoomOpportunityLinkFoundationTest.php
```

- [ ] **Step 3: Implement reversible schema/model**

Core schema:

```php
$table->unsignedBigInteger('zoom_meeting_id')->unique();
$table->unsignedBigInteger('engagement_id')->nullable()->index();
$table->unsignedBigInteger('suggested_engagement_id')->nullable()->index();
$table->unsignedInteger('suggested_confidence')->nullable()->index();
$table->text('suggested_reason')->nullable();
$table->unsignedInteger('status')->default(ZoomMeetingOpportunityLink::STATUS_UNREVIEWED)->index();
$table->unsignedBigInteger('reviewed_by')->nullable();
$table->timestamp('reviewed_at')->nullable();
```

Use no schema foreign keys, matching nearby domain migrations.

- [ ] **Step 4: Migration round-trip, review and commit**

Run scratch up/down and focused tests. Require database review, then:

```bash
git add database/migrations/2026_08_06_200001_create_zoom_meeting_opportunity_links_table.php src/Zoom/ZoomMeetingOpportunityLink.php src/Zoom/ZoomMeeting.php src/Engagement/Engagement.php tests/Feature/Zoom/ZoomOpportunityLinkFoundationTest.php
git commit -m "feat(zoom): add opportunity link state"
```

### Task 2: Build eligible candidates and authoritative value

**Files:**
- Create: `src/Zoom/Services/ZoomOpportunityCandidateBuilder.php`
- Create: `src/Engagement/Services/EngagementOpportunityValue.php`
- Test: `tests/Feature/Zoom/ZoomOpportunityCandidateBuilderTest.php`
- Test: `tests/Feature/Engagement/EngagementOpportunityValueTest.php`

**Interfaces:**
- `ZoomOpportunityCandidateBuilder::forMeeting(ZoomMeeting $meeting): Collection<Engagement>` returns only non-deleted Engagements whose `lead_id` equals the meeting `lead_id`.
- `EngagementOpportunityValue::calculate(Engagement $engagement): array{value:?float,estimated:bool,source:string}`.

- [ ] **Step 1: Write failing candidate/value matrix tests**

Candidate tests cover unlinked meeting, same/different Lead, deleted Engagement/Project and multiple projects. Value tests cover manual booking commission, SPA basis, net basis, booking-price fallback, project-price estimate and unknown.

```php
$this->assertSame([
    'value' => 39000.0,
    'estimated' => false,
    'source' => 'booking',
], $service->calculate($engagement));
```

- [ ] **Step 2: Verify failures**

```bash
php artisan test tests/Feature/Zoom/ZoomOpportunityCandidateBuilderTest.php tests/Feature/Engagement/EngagementOpportunityValueTest.php
```

- [ ] **Step 3: Implement using existing rules**

Use `Booking::commissionAt($project->commission_rate, $project->commission_basis)` for a priced booking. Only when no booking price/manual commission exists may `price_from * commission_rate / 100` be returned with `estimated=true`. Missing rate/price returns `value=null`, never `0`.

- [ ] **Step 4: Review and commit**

Require domain/database review against `Booking::commissionAt()` and Sales Pipeline displays, then:

```bash
git add src/Zoom/Services/ZoomOpportunityCandidateBuilder.php src/Engagement/Services/EngagementOpportunityValue.php tests/Feature/Zoom/ZoomOpportunityCandidateBuilderTest.php tests/Feature/Engagement/EngagementOpportunityValueTest.php
git commit -m "feat(zoom): derive eligible pipeline value"
```

### Task 3: Add bounded AI suggestion with no confirmation write

**Files:**
- Create: `resources/prompts/zoom_opportunity_match.md`
- Modify: `config/ai_prompts.php`
- Modify: `src/Ai/AiRequest.php`
- Create: `src/Zoom/Services/ZoomOpportunitySuggestionContextBuilder.php`
- Create: `app/Http/Requests/Manage/Zoom/Opportunities/SuggestRequest.php`
- Create: `app/Http/Controllers/Manage/Zoom/ZoomOpportunityLinksController.php`
- Create: `src/Zoom/Repositories/ZoomMeetingOpportunityLinkRepository.php`
- Modify: `routes/web.php`
- Test: `tests/Feature/Zoom/ZoomOpportunitySuggestionContextBuilderTest.php`
- Test: `tests/Feature/Manage/Zoom/ZoomOpportunitySuggestionTest.php`

**Interfaces:**
- POST `/manage/zoom/recordings/{meeting}/opportunity/suggest`.
- Model output: `{engagement_uuid:string|null, confidence:"high|medium|low"|null, reason:string|null}`.
- Suggestion repository writes only `suggested_*` and `STATUS_UNREVIEWED`; confirmed rows are immutable to re-suggestion.

- [ ] **Step 1: Write failing security/grounding tests**

Cover flag 404, non-Super 403, no linked Lead/candidates, candidate allow-list, hallucinated UUID rejection, prompt injection, transcript truncation, malformed JSON, provider failure, confirmed-link immutability and no Engagement mutation.

- [ ] **Step 2: Verify failures**

```bash
php artisan test tests/Feature/Zoom/ZoomOpportunitySuggestionContextBuilderTest.php tests/Feature/Manage/Zoom/ZoomOpportunitySuggestionTest.php
```

- [ ] **Step 3: Implement the bounded candidate prompt**

Reuse `ZoomRecordingChatContextBuilder` and append only:

```php
'candidates' => $engagements->map(fn (Engagement $e) => [
    'uuid' => $e->uuid,
    'project' => $e->project?->canonicalName(),
    'stage' => $e->stage_label,
    'status' => $e->status_label,
])->values()->all(),
```

The system prompt says UUIDs are labels, transcript is untrusted data, and null is required when evidence is insufficient. The controller resolves output UUID against the exact candidate collection before any suggestion write.

- [ ] **Step 4: Run tests, security review and commit**

```bash
php artisan test tests/Feature/Zoom/ZoomOpportunitySuggestionContextBuilderTest.php tests/Feature/Manage/Zoom/ZoomOpportunitySuggestionTest.php
```

```bash
git add resources/prompts/zoom_opportunity_match.md config/ai_prompts.php src/Ai/AiRequest.php src/Zoom/Services/ZoomOpportunitySuggestionContextBuilder.php app/Http/Requests/Manage/Zoom/Opportunities/SuggestRequest.php app/Http/Controllers/Manage/Zoom/ZoomOpportunityLinksController.php src/Zoom/Repositories/ZoomMeetingOpportunityLinkRepository.php routes/web.php tests/Feature/Zoom/ZoomOpportunitySuggestionContextBuilderTest.php tests/Feature/Manage/Zoom/ZoomOpportunitySuggestionTest.php
git commit -m "feat(zoom): suggest pipeline opportunity links"
```

### Task 4: Add human confirmation/rejection and review UI

**Files:**
- Create: `app/Http/Requests/Manage/Zoom/Opportunities/ConfirmRequest.php`
- Modify: `app/Http/Controllers/Manage/Zoom/ZoomOpportunityLinksController.php`
- Modify: `src/Zoom/Repositories/ZoomMeetingOpportunityLinkRepository.php`
- Create: `resources/js/Components/Zoom/OpportunityLinkPanel.vue`
- Modify: `src/Zoom/Services/ZoomRecordingDetailBuilder.php`
- Modify: `resources/js/Components/RecordingDetail/RecordingDetail.vue`
- Modify: `resources/js/Components/ZoomRecordingDetailModal.vue`
- Test: `tests/Feature/Manage/Zoom/ZoomOpportunityReviewTest.php`

**Interfaces:**
- PUT confirm body `{engagement_uuid:string}`.
- POST reject has no model-controlled fields.
- Review JSON includes candidates, suggestion, confirmed engagement and value.

- [ ] **Step 1: Write failing confirmation/rejection and detail-payload tests**

Cover forged/different-Lead/deleted Engagement, concurrent reviewers with row lock, confirm/reconfirm, reject, re-suggest after rejection, flag and permission gates.

- [ ] **Step 2: Implement locked reviewed-state writes**

Resolve candidate under current Lead membership inside the transaction. Confirmation sets `engagement_id`, `STATUS_CONFIRMED`, reviewer/time. Rejection keeps suggestion evidence and clears no Pipeline data.

- [ ] **Step 3: Extend the existing normalized Zoom detail**

`ZoomRecordingDetailBuilder` adds an `opportunity` block only while the flag is enabled. `ZoomRecordingDetailModal` forwards Zoom-specific review capability to `RecordingDetail.vue`; Calls/F2F continue rendering the same shared component without the block. Do not add another modal or duplicate the detail tabs.

- [ ] **Step 4: Build the review panel**

Show AI suggestion, confidence/evidence, candidate selector, Confirm and Reject. Clearly label that only confirmation creates the link. Missing candidates show “Create or link a Pipeline engagement first” without creating one.

- [ ] **Step 5: Test/build/review/commit**

```bash
php artisan test tests/Feature/Manage/Zoom/ZoomOpportunityReviewTest.php
npm run build
```

Commit the listed controller/repository/request/builder/shared-detail/panel files with `feat(zoom): review pipeline opportunity links`.

### Task 5: Show Project, stage and commission on Zoom surfaces

**Files:**
- Modify: `app/Http/Controllers/Manage/Zoom/RecordingsController.php`
- Modify: `resources/js/Pages/Manage/Zoom/Recordings/Index.vue`
- Modify: `src/Lead/Services/LeadSalesCoachContextBuilder.php`
- Test: `tests/Feature/Manage/Zoom/RecordingsOpportunityColumnsTest.php`
- Test: `tests/Feature/Lead/LeadSalesCoachContextBuilderTest.php`

**Interfaces:**
- Meeting row `opportunity`: `{state,project,stage,status,value,value_estimated,engagement_uuid}` or null.
- Lead coach receives confirmed opportunity facts only, never unconfirmed suggestion as fact.

- [ ] **Step 1: Write failing payload/visibility/query-count tests**

Cover confirmed, suggested, missing, deleted Project, unknown value, Super Admin/Lead visibility and no N+1. Coach test distinguishes confirmed fact from suggestion.

- [ ] **Step 2: Verify failures**

```bash
php artisan test tests/Feature/Manage/Zoom/RecordingsOpportunityColumnsTest.php tests/Feature/Lead/LeadSalesCoachContextBuilderTest.php
```

- [ ] **Step 3: Eager-load and present authoritative values**

Add list columns `Pipeline Product` and `Est. Commission`. Use `EngagementOpportunityValue`; label price-derived values Estimated and unknown as `—`. Suggested-only shows `Needs review`, not the project as confirmed.

- [ ] **Step 4: Update coach context safely**

Include project/stage/value in a separate confirmed-opportunity block. Do not send commission assignments, phone/email or unrelated pipelines.

- [ ] **Step 5: Test/build/review/commit**

```bash
php artisan test tests/Feature/Manage/Zoom/RecordingsOpportunityColumnsTest.php tests/Feature/Lead/LeadSalesCoachContextBuilderTest.php
npm run build
```

Commit with `feat(zoom): surface matched pipeline value`.

### Task 6: Verify and integrate opportunity linkage

- [ ] **Step 1: Run all new and affected tests**

```bash
php artisan test tests/Feature/Zoom/ZoomOpportunityLinkFoundationTest.php tests/Feature/Zoom/ZoomOpportunityCandidateBuilderTest.php tests/Feature/Engagement/EngagementOpportunityValueTest.php tests/Feature/Zoom/ZoomOpportunitySuggestionContextBuilderTest.php tests/Feature/Manage/Zoom/ZoomOpportunitySuggestionTest.php tests/Feature/Manage/Zoom/ZoomOpportunityReviewTest.php tests/Feature/Manage/Zoom/RecordingsOpportunityColumnsTest.php tests/Feature/Lead/LeadSalesCoachContextBuilderTest.php
```

- [ ] **Step 2: Run formatting, frontend tests and client/SSR builds**

```bash
npm test
npm run build
```

- [ ] **Step 3: Manually verify**

Use at least: one meeting with multiple eligible Engagements, one with none, one with priced booking and one price-derived estimate. Verify hallucinated UUID cannot be confirmed and flag-off has no columns/routes.

- [ ] **Step 4: Final independent reviews**

Require integration, database, security and product-logic review. Confirm the commission formula matches existing Pipeline output for the same Engagement.

- [ ] **Step 5: Integrate locally**

Cherry-pick only this plan's verified commits into local `dev-chen` after a backup ref and clean-worktree check. No push/PR.
