Files
alla-allaos-fullstack/plans/task-d.5.7.md
phaichayon 69279fc341 task-d.5.7
2026-06-25 15:41:59 +07:00

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.md
  • plans/task-d.5.7.md
  • docs/standards/project-foundations.md
  • docs/standards/task-catalog.md
  • docs/adr/0018-lead-enquiry-domain-separation.md
  • docs/implementation/task-d551-force-rename-enquiry-domain-opportunity.md
  • docs/implementation/task-d56-opportunity-lifecycle-won-lost-alignment.md
  • docs/security/crm-authorization-boundaries.md
  • src/db/schema.ts
  • src/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_created or moves to negotiation if 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

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