603 lines
9.9 KiB
Markdown
603 lines
9.9 KiB
Markdown
# 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
|
|
```
|