Opportunities — Developer Guide
Mirror of the Leads developer guide (pipeline board) and Activities developer guide (soft related FKs, notes, assignee scope). Prefer copying those patterns over inventing new ones.
Backend layout
| Piece | Path |
|---|---|
| Models | app/Models/Opportunity.php, OpportunityStage, OpportunityTag, OpportunityNote, OpportunityActivity |
| Enums | OpportunityActivityTypeEnum |
| Service | app/Services/Tenant/OpportunityService.php (+ ScopesToAssignee), OpportunityTagService.php |
| Controller | app/Http/Controllers/Tenant/Api/V1/OpportunityController.php, OpportunityTagController.php |
| Requests | app/Http/Requests/Tenant/Api/V1/Opportunity/* |
| Resources | app/Http/Resources/Tenant/Api/V1/Opportunity/* |
| Policy | app/Policies/OpportunityPolicy.php, OpportunityTagPolicy.php |
| Events | app/Events/Opportunity*.php (includes OpportunityTagCreated, OpportunityTagsSynced) |
| Realtime board channel | app/Broadcasting/OpportunityBoardChannel.php (registered in routes/channels.php) |
| Realtime board broadcasts | app/Events/Opportunity/Opportunity{Created,Updated,StageChanged,Assigned,Deleted}Broadcast.php + OpportunityBoardBroadcastPayload.php |
| Subscriber | app/Listeners/OpportunityEventSubscriber.php (audit + assignment notification + realtime board broadcasts) |
| Notifications | app/Notifications/Tenant/Opportunity/OpportunityAssignedNotification.php |
| Link rules | LinkableContact, LinkableLead, LinkableCompanyForOpportunity, EligibleOpportunityAssignee |
| Stage seeder | database/seeders/Tenant/OpportunityStageSeeder.php |
| Tests | tests/Feature/Tenant/Opportunity/OpportunityTest.php, OpportunityTagTest.php, OpportunityRealtimeBoardTest.php |
Domain notes
- Sales Pipeline is not a separate module —
opportunity_stages+ board endpoints live inside Opportunities. - Assignee scoping via
ScopesToAssigneewithopportunities.assign. opportunities.force.deleteis not granted to any default role — owner/superadmin only.- Related FKs (
contact_id/company_id/lead_id) are optional; each is validated for module entitlement + assignee scope on the related record when set. - Default stages are ensured idempotently (
OpportunityStageSeeder/ensureDefaultStages()): Prospecting → Qualification → Proposal → Negotiation → Won / Lost. - Soft delete; stage changes via
POST .../stage(can:opportunities.update). - No hard
module_dependenciesrows for Contacts/Companies/Leads. Quotations and Contracts declare Opportunities as a required hard dependency.
Permissions
opportunities.view | create | update | delete | restore | force.delete | assignRoutes use module:opportunities then can:opportunities.* / policies.
Catalog: slug opportunities, category sales, is_default_included = false, is_billable = false, sort_order = 40, version 1.3.0. Registered via DefaultModuleRegistrar migration (migrate-only).
Real-time board sync
Mirrors the Leads 1.8.0 / Live Chat inbox invalidate pattern — not the Team Chat message-patching pattern. The board refetches on change instead of patching individual cards.
- Channel: private
tenant.{tenantId}.opportunities.board, registered inroutes/channels.php. Auth (OpportunityBoardChannel::join): same tenant andopportunities.view. - Broadcast events (
App\Events\Opportunity\*Broadcast, allShouldBroadcastNow, kept separate from the existing domain events):OpportunityCreatedBroadcast→broadcastAs('OpportunityCreated')OpportunityUpdatedBroadcast→broadcastAs('OpportunityUpdated')OpportunityStageChangedBroadcast→broadcastAs('OpportunityStageChanged')(includesprevious_stage_id)OpportunityAssignedBroadcast→broadcastAs('OpportunityAssigned')(includesassigned_to)OpportunityDeletedBroadcast→broadcastAs('OpportunityDeleted')
- Payload shape (
OpportunityBoardBroadcastPayload):action,opportunity_id,uuid,actor_id,stage_id, plusprevious_stage_id(stage-changed only) /assigned_to(assigned only). - Dispatch:
OpportunityEventSubscriberfires the matching broadcast from create/update/stage/assign/delete handlers viaSafeRealtimeBroadcast::dispatchso a Reverb outage never breaks the write path. - Frontend:
src/hooks/use-opportunities-board-realtime.tsjoins the channel via sharedwindow.Echowhileopportunities-page.tsxhasviewMode === 'board'. Listens for.OpportunityCreated/.OpportunityUpdated/.OpportunityStageChanged/.OpportunityAssigned/.OpportunityDeletedand debounces (~200ms) an invalidate ofQUERY_KEYS.opportunityBoard+QUERY_KEYS.opportunityStats. - Catalog opportunities 1.2.0 → 1.3.0 (migrate-only +
CatalogSeedercompanion). - Production readiness: Opportunities real-time board sync 1.3.0.
API (tenant)
Base: /api/tenant/v1 — full reference tenant-v1-opportunities.md.
Colored tags are create-only for MVP (GET/POST /opportunity-tags, assign via tag_ids / PUT …/tags, filter tag_id). No rename/delete/reorder tag routes.
Frontend
SPA should mirror Leads (board default + table, create/edit page, record page) under the existing AppLayout — do not invent a parallel shell.
| Piece | Path (expected) |
|---|---|
| Page | src/pages/opportunities/ |
| Shared board | src/components/crm/kanban-board.tsx (per-column vertical scroll + contained horizontal scroll; titles stay fixed) |
| Form / detail | create/edit page + record page (Overview, Notes, Activity); board DnD auto-saves stage on the list page |
| Service | opportunityService in src/api/services.ts |
| Realtime board hook | src/hooks/use-opportunities-board-realtime.ts — joins tenant.{id}.opportunities.board, debounce-invalidates board + stats |
| Nav | permission: opportunities.view, module: 'opportunities' (Sales) |
| Playwright | e2e/pages/opportunities.page.ts, e2e/tests/opportunities/, npm run test:e2e:opportunities |
| Catalog | 1.3.0 (real-time board sync) |
Tests
php artisan test --compact tests/Feature/Tenant/Opportunity/OpportunityTest.php
php artisan test --compact tests/Feature/Tenant/Opportunity/OpportunityTagTest.php
php artisan test --compact tests/Feature/Tenant/Opportunity/OpportunityRealtimeBoardTest.php
npm run typecheck && npm run lint && npm run build
npm run test:e2e:opportunitiesLogging
- Spatie
LogsActivityonOpportunity(log nameopportunities) - Domain
opportunity_activitiestimeline PlatformAuditServiceviaOpportunityEventSubscriber(includesopportunity_tag_created,opportunity_tags_synced)
Ask EloSync
Ask EloSync Opportunity tools (get_opportunity, get_opportunity_stages, confirmed stage/assign/note writes, plus existing search_opportunities / get_pipeline_summary) are registered in AIToolRegistry and confirmed via PendingAiActionService. Search rows include stage_id and assignee id. HTTP POST …/stage validates stage_id in the current tenant. See AI tools and AI Opportunity triage production readiness.