task-d.5.6
This commit is contained in:
602
plans/task-d.5.6.md
Normal file
602
plans/task-d.5.6.md
Normal file
@@ -0,0 +1,602 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user