10 KiB
Task D.5.7: Opportunity ↔ Quotation Synchronization Foundation
Status
Planned
Objective
Establish synchronization rules between Opportunity and Quotation after the Opportunity lifecycle foundation is in place.
This task ensures that Opportunity stage and outcome are aligned with quotation creation, quotation approval, quotation acceptance, quotation rejection, revision, and future PO flow.
Business flow:
Lead → Opportunity → Quotation → Approval → Customer → PO / Lost
The objective is to make Opportunity the sales pipeline source of truth while keeping Quotation as the commercial document source of truth.
Mandatory Review
Review before implementation:
AGENTS.mdplans/task-d.5.7.mddocs/standards/project-foundations.mddocs/standards/task-catalog.mddocs/adr/0018-lead-enquiry-domain-separation.mddocs/implementation/task-d551-force-rename-enquiry-domain-opportunity.mddocs/implementation/task-d56-opportunity-lifecycle-won-lost-alignment.mddocs/security/crm-authorization-boundaries.mdsrc/db/schema.tssrc/features/crm/opportunities/**src/features/crm/quotations/**src/features/crm/approval/**src/features/foundation/audit-log/**
Current Foundation
Already available:
crm_leads
crm_opportunities
crm_opportunity_followups
crm_opportunity_customers
crm_quotations
crm_quotation_items
crm_quotation_followups
crm_approval_requests
crm_document_artifacts
Lead → Opportunity assignment
Opportunity lifecycle
Quotation creation
Quotation approval
Approved PDF artifact
Audit foundation
Business Ownership Rule
Opportunity owns:
sales pipeline
sales stage
business outcome
sales owner
expected value
chance percent
expected close date
Quotation owns:
commercial document
quotation amount
revision
approval state
approved PDF
customer-facing proposal
Scope D5.7.1 Relationship Naming Alignment
Ensure active code uses:
opportunityId
instead of:
enquiryId
for quotation relationship fields and DTOs.
If DB still contains legacy column name:
crm_quotations.enquiry_id
then rename to:
crm_quotations.opportunity_id
Because the project is still in dev/reset mode, prefer clean naming.
Rules:
- no active application code should expose
enquiryId - quotation APIs should accept
opportunityId - quotation detail should show linked Opportunity
- Opportunity detail should show linked Quotations
Scope D5.7.2 Quotation Creation Sync
When creating a quotation from an Opportunity:
-
require
opportunityId -
verify Opportunity exists and is open
-
prevent quotation creation from closed opportunities
-
copy baseline context from Opportunity:
- customer
- contact
- project name
- project location
- product type
- salesman / owner
- chance percent
- hot project flag if available
After quotation creation:
opportunity.status = quotation_created
opportunity.outcomeStatus = open
Rules:
- do not overwrite closed outcome
- do not auto mark won
- audit both quotation creation and opportunity sync
Scope D5.7.3 Quotation Revision Sync
When creating quotation revision:
- revision must remain linked to the same Opportunity
- parent and child quotation must share
opportunityId - Opportunity stays open unless already closed
- Opportunity stage remains
quotation_createdor moves tonegotiationif supported
Rules:
- do not duplicate opportunity
- do not unlink old revisions
- detail UI should group revisions under the same opportunity
Scope D5.7.4 Approval Sync
When quotation is submitted for approval:
Opportunity stage may remain quotation_created
When quotation is approved:
Opportunity stage may become negotiation
Recommended mapping:
quotation draft → opportunity quotation_created
quotation pending_approval → opportunity quotation_created
quotation approved → opportunity negotiation
quotation sent_to_customer → opportunity negotiation
Rules:
- approval sync must be non-destructive
- do not change opportunity outcome
- do not mark won on approval
- audit stage sync if changed
Scope D5.7.5 Quotation Outcome Sync
Add explicit service operations or hooks for:
acceptQuotation()
rejectQuotation()
markQuotationLost()
If such operations already exist, wire them to Opportunity lifecycle.
When quotation is accepted or PO is confirmed:
opportunity.outcomeStatus = won
opportunity.closedAt = now
opportunity.closedWonAt = now
When quotation is rejected/lost and no other active quotation remains viable:
opportunity.outcomeStatus = lost
opportunity.closedAt = now
opportunity.closedLostAt = now
Rules:
- one won quotation marks opportunity won
- one lost quotation must not automatically mark opportunity lost if another active quotation/revision is still open
- latest active revision should drive outcome when applicable
- lost requires reason
- won should eventually be driven by PO, but manual won remains allowed until PO module exists
Scope D5.7.6 Opportunity Detail Integration
Update Opportunity detail to show linked quotations.
Display:
Quotation Code
Revision
Status
Approval Status
Total Amount
Currency
Valid Until
Accepted / Rejected state
Actions:
Open Quotation
Create Quotation
Create Revision
Mark Won
Mark Lost
Rules:
- Create Quotation action should preselect current opportunity
- closed opportunities disable create quotation
- winning quotation should be visibly highlighted
Scope D5.7.7 Quotation Detail Integration
Update Quotation detail to show linked Opportunity.
Display:
Opportunity Code
Opportunity Stage
Opportunity Outcome
Sales Owner
Expected Close Date
Actions:
Open Opportunity
Rules:
- no broader permission leak
- show only if user can access quotation and linked opportunity context is safe to display
Scope D5.7.8 Data Consistency Guards
Add guards:
- quotation cannot link to opportunity from another organization
- quotation product type must match opportunity product type unless explicitly permitted
- quotation customer must match opportunity customer or opportunity project party unless explicitly permitted
- closed opportunity cannot create quotation
- quotation revision must keep the same opportunity
Scope D5.7.9 Audit Logging
Required audit actions:
sync_opportunity_from_quotation_created
sync_opportunity_from_quotation_approved
sync_opportunity_from_quotation_sent
sync_opportunity_from_quotation_won
sync_opportunity_from_quotation_lost
sync_quotation_opportunity_link
Audit must capture:
opportunityId
quotationId
previousOpportunityStage
newOpportunityStage
previousOutcomeStatus
newOutcomeStatus
reason
actedBy
Scope D5.7.10 Permissions
Use existing permissions:
crm.opportunity.read
crm.opportunity.update
crm.opportunity.lifecycle.update
crm.quotation.create
crm.quotation.update
crm.quotation.read
Rules:
- quotation creation requires quotation create permission
- opportunity sync should be service-side and not require separate user UI permission if triggered by valid quotation action
- manual outcome changes still require opportunity lifecycle permission
- do not check role strings directly
Scope D5.7.11 Documentation
Create:
docs/implementation/task-d57-opportunity-quotation-synchronization-foundation.md
Document:
relationship model
sync trigger map
stage mapping
outcome mapping
revision rules
guard rules
known limitations
Recommended Sync Map
Quotation Created
→ Opportunity stage = quotation_created
→ Outcome = open
Quotation Submitted
→ Opportunity stage = quotation_created
→ Outcome = open
Quotation Approved
→ Opportunity stage = negotiation
→ Outcome = open
Quotation Sent
→ Opportunity stage = negotiation
→ Outcome = open
Quotation Accepted / PO Confirmed
→ Opportunity stage = closed
→ Outcome = won
Quotation Rejected / Lost
→ Opportunity stage = closed only if no viable quotation remains
→ Outcome = lost
Deliverables
Updated:
src/db/schema.ts
src/features/crm/opportunities/**
src/features/crm/quotations/**
src/features/crm/approval/**
New or updated:
src/features/crm/opportunities/server/quotation-sync.service.ts
src/features/crm/quotations/server/opportunity-link.service.ts
Updated APIs:
/api/crm/quotations
/api/crm/quotations/[id]
/api/crm/quotations/[id]/revisions
Updated UI:
Opportunity Detail linked quotations
Quotation Detail linked opportunity
Quotation Create form preselect opportunity
Documentation:
docs/implementation/task-d57-opportunity-quotation-synchronization-foundation.md
Explicit Non-Scope
Do not:
- create PO module
- create dashboard datasets
- create KPI cards
- create reports
- rewrite approval matrix
- create Sales Enquiry document
- rewrite PDF template
- implement revenue recognition
- migrate production data
These belong to later phases.
Verification
Run:
npm exec tsc --noEmit
npm run build
Manual verification:
Create quotation from opportunity
Opportunity stage becomes quotation_created
Submit quotation for approval
Approve quotation
Opportunity stage becomes negotiation if configured
Create quotation revision
Revision keeps same opportunityId
Mark quotation accepted / won
Opportunity outcome becomes won
Mark quotation lost
Opportunity becomes lost only when no viable quotation remains
Closed opportunity blocks quotation creation
Opportunity detail shows quotations
Quotation detail shows opportunity
Audit logs capture sync actions
Lead → Opportunity still works
Approval flow still works
PDF flow still works
Definition of Done
Task is complete when:
- Quotation uses
opportunityId - Opportunity stage syncs from quotation lifecycle
- Opportunity outcome can sync from quotation won/lost events
- Closed opportunity blocks new quotations
- Opportunity detail shows linked quotations
- Quotation detail shows linked opportunity
- Revisions preserve opportunity linkage
- Audit logs capture synchronization
- Existing Lead, Opportunity, Quotation, Approval, and PDF flows remain operational
Result:
Opportunity ↔ Quotation Sync = Established
Pipeline Outcome Integrity = Strengthened
Ready for Pipeline KPI / Dashboard Foundation