# Task D.5.6: Opportunity Lifecycle / Won-Lost Alignment ## Status Planned ## Objective Establish the official Opportunity lifecycle and business outcome rules after the Enquiry domain has been force-renamed to Opportunity. This task aligns: ```txt Lead → Opportunity → Quotation → PO / Lost / Cancelled / No Quotation ``` without changing dashboard/report datasets yet. The goal is to make Opportunity outcome states consistent before KPI, pipeline reporting, and dashboard work begins. --- ## Mandatory Review Review before implementation: * `AGENTS.md` * `plans/task-d.5.6.md` * `docs/standards/project-foundations.md` * `docs/standards/task-catalog.md` * `docs/adr/0018-lead-enquiry-domain-separation.md` * `docs/implementation/task-d55-opportunity-sales-workspace-refinement.md` * `docs/implementation/task-d551-force-rename-enquiry-domain-opportunity.md` * `docs/security/crm-authorization-boundaries.md` * `src/db/schema.ts` * `src/features/crm/opportunities/**` * `src/features/crm/quotations/**` * `src/features/foundation/master-options/**` * `src/features/foundation/audit-log/**` --- ## Current Foundation Already available: ```txt crm_leads crm_opportunities crm_opportunity_followups crm_opportunity_customers /api/crm/leads /api/crm/opportunities /api/crm/quotations Lead → Opportunity assignment Opportunity UI Quotation flow Approval flow Audit foundation Master option foundation ``` Do not recreate: ```txt lead domain opportunity domain quotation domain approval domain dashboard reports ``` --- ## Business Definitions ### Opportunity A sales-owned execution record created from Lead assignment or direct sales capture. ### Opportunity Stage The current sales working phase. Example: ```txt new qualification requirement_gathering site_survey proposal_preparation quotation_created negotiation closed ``` ### Opportunity Outcome The final business result. Example: ```txt open won lost cancelled no_quotation ``` Stage answers: ```txt Where is Sales working now? ``` Outcome answers: ```txt What was the final result? ``` --- ## Scope D5.6.1 Lifecycle Model Define official lifecycle model. Recommended fields if already available: ```txt status processStatus businessStatus outcomeStatus lostReason closedAt closedBy ``` If schema does not yet support outcome fields, add a migration. Recommended DB additions to `crm_opportunities`: ```txt stage text not null default 'new' outcome_status text not null default 'open' lost_reason text null lost_detail text null closed_at timestamptz null closed_by text null cancel_reason text null no_quotation_reason text null ``` Rules: * `stage` = working lifecycle * `outcomeStatus` = final result * only one final outcome allowed * final opportunities should not allow normal editing unless reopened by permitted user --- ## Scope D5.6.2 Master Options Add or verify master option categories: ```txt crm_opportunity_stage crm_opportunity_outcome_status crm_opportunity_lost_reason crm_opportunity_cancel_reason crm_opportunity_no_quotation_reason ``` Recommended stages: ```txt new qualification requirement_gathering site_survey proposal_preparation quotation_created negotiation closed ``` Recommended outcome statuses: ```txt open won lost cancelled no_quotation ``` Recommended lost reasons: ```txt price_too_high competitor_won budget_cancelled timeline_not_match spec_not_match customer_no_response other ``` Rules: * no hardcoded labels in UI * use master-option foundation * seed all required options --- ## Scope D5.6.3 Opportunity Outcome Service Add service operations: ```ts markOpportunityWon() markOpportunityLost() markOpportunityCancelled() markOpportunityNoQuotation() reopenOpportunity() ``` Location: ```txt src/features/crm/opportunities/server/lifecycle.service.ts ``` Rules: * all operations must enforce CRM access * all operations must audit * closed opportunities cannot be closed again * reopening requires explicit permission * outcome updates must be transactional --- ## Scope D5.6.4 Outcome API Routes Add routes: ```txt POST /api/crm/opportunities/[id]/mark-won POST /api/crm/opportunities/[id]/mark-lost POST /api/crm/opportunities/[id]/mark-cancelled POST /api/crm/opportunities/[id]/mark-no-quotation POST /api/crm/opportunities/[id]/reopen ``` Request examples: ```ts // mark lost { lostReason: string; lostDetail?: string; } // mark cancelled { cancelReason: string; } // mark no quotation { noQuotationReason: string; } // reopen { reason: string; } ``` Response: ```ts { opportunityId: string; outcomeStatus: string; stage: string; closedAt?: string | null; } ``` --- ## Scope D5.6.5 Quotation Alignment Align opportunity outcome with quotation state. Rules: * When quotation is created from opportunity: * opportunity stage may become `quotation_created` * outcome remains `open` * When quotation is accepted / PO received: * opportunity can be marked `won` * When quotation is rejected / lost: * opportunity can be marked `lost` * Multiple quotations may exist under one opportunity * A single won quotation is enough to mark opportunity as `won` * If opportunity is closed, prevent creating new quotation unless reopened Do not rewrite quotation service heavily in this task. Add minimal guard/hooks only. --- ## Scope D5.6.6 UI Outcome Actions Update Opportunity Detail. Add outcome action area: ```txt Mark Won Mark Lost Mark Cancelled Mark No Quotation Reopen ``` Rules: * show actions based on permission and current state * require reason for lost/cancelled/no quotation * require reason for reopen * closed opportunities show locked/closed state * outcome actions should not appear on lead page --- ## Scope D5.6.7 Opportunity List Outcome Display Update Opportunity List. Add columns or badges: ```txt Stage Outcome Lost Reason Closed Date ``` Filters: ```txt stage outcomeStatus lostReason closedAt range ``` Rules: * open opportunities remain default view * closed outcomes can be filtered * do not change dashboard/report datasets yet --- ## Scope D5.6.8 Audit Logging Required audit actions: ```txt mark_opportunity_won mark_opportunity_lost mark_opportunity_cancelled mark_opportunity_no_quotation reopen_opportunity ``` Audit should capture: ```txt previousStage previousOutcomeStatus newStage newOutcomeStatus reason actedBy actedAt ``` --- ## Scope D5.6.9 Permissions Add or verify permissions: ```txt crm.opportunity.lifecycle.update crm.opportunity.lifecycle.reopen ``` Optional granular permissions: ```txt crm.opportunity.mark_won crm.opportunity.mark_lost crm.opportunity.mark_cancelled crm.opportunity.mark_no_quotation crm.opportunity.reopen ``` Recommendation: Use granular permissions if current permission model supports it. Rules: * do not check role strings directly * use resolved CRM access * backend remains source of truth --- ## Scope D5.6.10 Documentation Create: ```txt docs/implementation/task-d56-opportunity-lifecycle-won-lost-alignment.md ``` Document: ```txt stage model outcome model state transition rules permission rules quotation alignment known limitations ``` --- ## State Transition Rules Recommended: ```txt open → won open → lost open → cancelled open → no_quotation won → open only via reopen lost → open only via reopen cancelled → open only via reopen no_quotation → open only via reopen ``` Closed states: ```txt won lost cancelled no_quotation ``` Closed opportunities: ```txt cannot edit normal fields cannot create quotation cannot add sales follow-up can view history can reopen if permitted ``` --- ## Deliverables New or updated: ```txt src/features/crm/opportunities/server/lifecycle.service.ts src/features/crm/opportunities/schemas/lifecycle.schema.ts src/app/api/crm/opportunities/[id]/mark-won/route.ts src/app/api/crm/opportunities/[id]/mark-lost/route.ts src/app/api/crm/opportunities/[id]/mark-cancelled/route.ts src/app/api/crm/opportunities/[id]/mark-no-quotation/route.ts src/app/api/crm/opportunities/[id]/reopen/route.ts ``` Updated: ```txt src/db/schema.ts src/db/seeds/foundation.seed.ts src/features/crm/opportunities/types.ts src/features/crm/opportunities/server/service.ts src/features/crm/opportunities/components/opportunity-detail.tsx src/features/crm/opportunities/components/opportunities-table.tsx src/features/crm/opportunities/components/opportunity-status-badge.tsx src/features/crm/opportunities/api/service.ts src/features/crm/opportunities/api/mutations.ts src/features/crm/opportunities/api/queries.ts ``` Documentation: ```txt docs/implementation/task-d56-opportunity-lifecycle-won-lost-alignment.md ``` --- ## Explicit Non-Scope Do not: * rename opportunity domain again * change lead domain * rewrite quotation approval * create dashboard datasets * create reports * implement sales KPI * implement PO module * migrate historical production data * create Sales Enquiry document These belong to later phases. --- ## Verification Run: ```txt npm exec tsc --noEmit npm run build ``` Manual verification: ```txt Create opportunity Create quotation from opportunity Mark opportunity won Mark opportunity lost with reason Mark opportunity cancelled with reason Mark opportunity no quotation with reason Reopen opportunity Blocked quotation creation on closed opportunity Opportunity list filters by outcome Audit logs exist for lifecycle actions Lead → Opportunity still works Quotation flow still works Approval flow still works ``` --- ## Definition of Done Task is complete when: * Opportunity has official stage and outcome model * Won/Lost/Cancelled/No Quotation actions work * Reopen flow works with permission * Opportunity list/detail show lifecycle state * Closed opportunities are guarded * Quotation alignment is minimally enforced * Audit logs capture lifecycle transitions * Existing Lead, Quotation, and Approval flows remain operational Result: ```txt Opportunity Lifecycle = Established Won/Lost Alignment = Established Pipeline Outcome Foundation = Ready Ready for Pipeline KPI / Dashboard Foundation ```