Files
alla-allaos-fullstack/plans/task-d.5.6.md
phaichayon 65dd0e2e56 task-d.5.6
2026-06-25 13:09:21 +07:00

9.9 KiB

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:

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:

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:

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:

new
qualification
requirement_gathering
site_survey
proposal_preparation
quotation_created
negotiation
closed

Opportunity Outcome

The final business result.

Example:

open
won
lost
cancelled
no_quotation

Stage answers:

Where is Sales working now?

Outcome answers:

What was the final result?

Scope D5.6.1 Lifecycle Model

Define official lifecycle model.

Recommended fields if already available:

status
processStatus
businessStatus
outcomeStatus
lostReason
closedAt
closedBy

If schema does not yet support outcome fields, add a migration.

Recommended DB additions to crm_opportunities:

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:

crm_opportunity_stage
crm_opportunity_outcome_status
crm_opportunity_lost_reason
crm_opportunity_cancel_reason
crm_opportunity_no_quotation_reason

Recommended stages:

new
qualification
requirement_gathering
site_survey
proposal_preparation
quotation_created
negotiation
closed

Recommended outcome statuses:

open
won
lost
cancelled
no_quotation

Recommended lost reasons:

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:

markOpportunityWon()
markOpportunityLost()
markOpportunityCancelled()
markOpportunityNoQuotation()
reopenOpportunity()

Location:

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:

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:

// mark lost
{
  lostReason: string;
  lostDetail?: string;
}

// mark cancelled
{
  cancelReason: string;
}

// mark no quotation
{
  noQuotationReason: string;
}

// reopen
{
  reason: string;
}

Response:

{
  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:

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:

Stage
Outcome
Lost Reason
Closed Date

Filters:

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:

mark_opportunity_won
mark_opportunity_lost
mark_opportunity_cancelled
mark_opportunity_no_quotation
reopen_opportunity

Audit should capture:

previousStage
previousOutcomeStatus
newStage
newOutcomeStatus
reason
actedBy
actedAt

Scope D5.6.9 Permissions

Add or verify permissions:

crm.opportunity.lifecycle.update
crm.opportunity.lifecycle.reopen

Optional granular permissions:

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:

docs/implementation/task-d56-opportunity-lifecycle-won-lost-alignment.md

Document:

stage model
outcome model
state transition rules
permission rules
quotation alignment
known limitations

State Transition Rules

Recommended:

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:

won
lost
cancelled
no_quotation

Closed opportunities:

cannot edit normal fields
cannot create quotation
cannot add sales follow-up
can view history
can reopen if permitted

Deliverables

New or updated:

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:

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:

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:

npm exec tsc --noEmit
npm run build

Manual verification:

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:

Opportunity Lifecycle = Established
Won/Lost Alignment = Established
Pipeline Outcome Foundation = Ready
Ready for Pipeline KPI / Dashboard Foundation