taks-d.2.1

This commit is contained in:
phaichayon
2026-06-17 14:46:52 +07:00
parent 0a484e0b45
commit 5be6c54272
39 changed files with 6254 additions and 316 deletions

View File

@@ -0,0 +1,39 @@
# Task D.2 Billing Customer + Project Parties
## Scope delivered
- Kept the main CRM field named `Billing Customer` on enquiry and quotation forms/detail pages.
- Added a separate `Project Parties` model for enquiries and aligned quotation related customers to the same business concept.
- Added `remark` support on quotation project parties and introduced enquiry project-party persistence.
- Switched role sourcing to the shared master-option category `crm_project_party_role`.
- Added compatibility mapping so legacy quotation roles such as `owner` and `billing` still render correctly.
## Data model
- Added `crm_enquiry_customers` for enquiry project parties.
- Added `remark` column to `crm_quotation_customers`.
- Generated migration `drizzle/0010_calm_wendell_rand.sql`.
## Server behavior
- Enquiry create/update now sync `Project Parties` transactionally.
- Quotation create/update now sync `Project Parties` transactionally.
- Billing customer is auto-added as a `billing_customer` project party during sync.
- Duplicate `customerId + roleCode` combinations are de-duplicated during main form sync and rejected in quotation detail CRUD.
- Quotation project-party reads normalize legacy role codes to current shared role options.
## UI changes
- Enquiry form now has `Billing Customer` plus a `Project Parties` editor.
- Enquiry detail now shows `Billing Customer` and `Project Parties` separately.
- Quotation form now has `Billing Customer` plus a `Project Parties` editor.
- Quotation detail overview separates `Billing Customer` and `Project Parties`.
- Quotation customer tab is relabeled to `Project Parties`, shows role clearly, and supports remark editing.
## Verification
- `npx tsc --noEmit` passes.
- `npm run lint` still fails because of pre-existing repo issues outside this task, including:
- `src/components/ui/input-group.tsx`
- `src/features/chat/components/message-composer.tsx`
- `src/components/forms/demo-form.tsx`

View File

@@ -0,0 +1,76 @@
# Task D.2.1: Revenue Attribution and Party Governance
## Scope delivered
- Froze the revenue attribution rules for CRM reporting.
- Froze project-party reporting authority so dashboard and reports use the same source of truth.
- Added reusable server-side helpers for grouped revenue analytics by party role.
- Added ADR governance so future reporting changes require an explicit decision trail.
## Frozen rules
### Revenue owner
- `Revenue Owner = End Customer`
- Authority: project-party role code `end_customer`
- Fallback: if no `end_customer` exists, use `billing_customer`
### Relationship revenue dimensions
- `Billing Revenue` = quotation revenue grouped by project-party role `billing_customer`
- `Contractor Revenue` = quotation revenue grouped by project-party role `contractor`
- `Consultant Revenue` = quotation revenue grouped by project-party role `consultant`
### Multiple party behavior
- When multiple parties share the same role on a quotation, each party receives full quotation attribution.
- This is intentional CRM relationship analytics behavior.
- No proration is applied.
### Reporting authority
- Reporting must use project-party relationships, not raw customer references by themselves.
- Service logic prefers `crm_quotation_customers`.
- If quotation parties are missing, service logic falls back to `crm_enquiry_customers` on the linked enquiry.
- Cancelled quotations are excluded by default from revenue attribution.
## Service layer added
Added server-only reporting helpers under `src/features/crm/reporting/server/`:
- `getRevenueByEndCustomer()`
- `getRevenueByBillingCustomer()`
- `getRevenueByContractor()`
- `getRevenueByConsultant()`
Shared behavior:
- organization scoped
- supports active quotation/report filters
- groups revenue by customer
- counts quotation coverage per attributed customer
- respects end-customer fallback to billing-customer for revenue-owner attribution
## Governance added
- Added ADR [0010-revenue-attribution-governance.md](/c:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/docs/adr/0010-revenue-attribution-governance.md)
- This ADR freezes:
- revenue owner rule
- project-party reporting rule
- fallback rule
- multiple-party rule
## Remaining risks
- quotations and enquiries still use separate party tables, so analytics currently depends on a precedence rule rather than a unified reporting projection
- legacy records without synchronized project-party rows may not attribute revenue until backfill or edit/save normalization happens
- historical analytics still reflects current project-party state, not an effective-dated party snapshot
## Task J readiness
- revenue attribution rules frozen
- revenue owner frozen
- project-party reporting rules frozen
- KPI dimensions frozen for reporting by end customer, billing customer, contractor, and consultant
- reusable service layer added for dashboard/report reuse
- ADR added

View File

@@ -0,0 +1,139 @@
# Task J.0: KPI Definition Freeze
## Scope delivered
- Froze the CRM sales pipeline definitions that Dashboard KPI must use.
- Froze customer party terminology so enquiry, quotation, and dashboard reporting use the same business language.
- Froze KPI and revenue formulas before Task J dashboard work begins.
- Declared that KPI definition changes after this point require a new ADR or equivalent decision record.
## Governance rule
- After Task J.0, KPI definitions, pipeline meanings, and dashboard counting rules must not change silently.
- Any future change to these definitions requires a new ADR so reporting behavior stays auditable.
## Frozen sales pipeline
### Lead
- Definition: enquiry is not assigned to sales yet.
- Rule: `assignedToUserId = null`
- Rule: `pipelineStage = lead`
- KPI impact: `Lead Count`, `Lead Aging`, `Lead Source`, `New Leads`, `Unassigned Leads`
### Opportunity
- Definition: enquiry has already been assigned to sales.
- Rule: `assignedToUserId != null`
- Rule: `pipelineStage = opportunity`
- Auto transition: assigning sales moves the record from `Lead` to `Opportunity`
- KPI impact: `Opportunity Count`, `Opportunity Value`, `Hot Opportunities`, `Opportunity Aging`, `Open Opportunities`
### Closed Won
- Definition: customer confirms the award, or the project is officially awarded.
- Rule: `pipelineStage = closed_won`
### Closed Lost
- Definition: competitor wins, budget is not approved, customer cancels, or project does not proceed.
- Rule: `pipelineStage = closed_lost`
## Frozen opportunity lifecycle
```txt
Lead
-> Assign Sales
Opportunity
-> Create Quotation
Opportunity
-> Approved Quotation
Opportunity
-> Award
Closed Won
```
## Frozen customer party definitions
- `Billing Customer`: the company used for quotation issue and billing.
- `End Customer`: the real project owner.
- `Contractor`: the construction contractor or main contractor.
- `Consultant`: the project consultant.
- `Customer`: a general commercial counterparty or related party when a more specific role is not applicable.
These terms are frozen as the shared language for CRM forms, detail views, and future dashboard/reporting logic.
## Frozen KPI definitions
### Lead metrics
- `Lead Count` = enquiries where `pipelineStage = lead`
- `New Leads` = lead-stage enquiries created within the selected reporting period
- `Unassigned Leads` = lead-stage enquiries where `assignedToUserId = null`
- `Lead Aging` = age of lead-stage enquiries based on creation date until current/report date
### Opportunity metrics
- `Opportunity Count` = enquiries where `pipelineStage = opportunity`
- `Open Opportunities` = opportunities still active and not moved to `closed_won` or `closed_lost`
- `Hot Opportunities` = opportunity-stage enquiries flagged by the future dashboard/business rule layer
- `Opportunity Aging` = age of opportunity-stage enquiries based on entry into active opportunity lifecycle until current/report date
### Conversion metrics
- `Lead -> Opportunity` = `Opportunity Created / Total Leads`
- `Opportunity -> Quotation` = `Opportunity With Quotation / Total Opportunities`
- `Quotation -> Won` = `Closed Won / Total Opportunities With Quotation`
## Frozen revenue rules
### Opportunity value
- Source: `crm_enquiries.estimatedValue`
- Condition: only count rows where `pipelineStage = opportunity`
### Quotation value
- Source: `crm_quotations.totalAmount`
- Condition: count quotations where `status != cancelled`
### Won value
- Source: `crm_quotations.totalAmount`
- Condition: count quotation-linked business only when parent enquiry `pipelineStage = closed_won`
### Lost value
- Source: `crm_quotations.totalAmount`
- Condition: count quotation-linked business only when parent enquiry `pipelineStage = closed_lost`
## Frozen ownership rules for reporting
- Dashboard ownership for `Lead` and `Opportunity` must use `assignedToUserId`.
- Dashboard ownership for `Quotation` must use `salesmanId`.
This prevents mixing enquiry ownership with quotation ownership in the same KPI bucket.
## Explicit non-scope items
Task J.0 intentionally does not add lifecycle timestamp fields or new master-data defaults.
Deferred fields:
- `convertedToOpportunityAt`
- `closedWonAt`
- `closedLostAt`
- `lostReason`
- `defaultPartyRole`
These remain technical debt and should be added only when downstream reporting or workflow requirements justify schema changes.
## Dashboard readiness
Task J can now build CRM dashboard KPIs on top of these frozen definitions:
- KPI definitions frozen
- sales pipeline frozen
- customer party definitions frozen
- revenue rules frozen
- dashboard semantics ready for implementation

View File

@@ -242,3 +242,58 @@ Future:
- decide when direct signed URLs are acceptable versus mandatory server streaming
- add response caching strategy for large artifact downloads
- evaluate optional download-audit sampling if volume grows
## After Task J.0
### Missing lifecycle timestamps for KPI precision
Task J.0 freezes KPI rules around `pipelineStage`, but the schema still lacks explicit lifecycle timestamps for stage conversion and closure events.
Future:
- add `convertedToOpportunityAt` for precise lead-to-opportunity timing
- add `closedWonAt` for won-date reporting
- add `closedLostAt` for lost-date reporting
- backfill reporting logic to prefer explicit lifecycle timestamps over inference when available
### Lost reason analytics
Closed-lost semantics are frozen, but there is still no structured `lostReason` field for dashboard grouping and sales analysis.
Future:
- add `lostReason` to the CRM enquiry lifecycle model
- define a controlled vocabulary or master-option category for loss analysis
- expose lost-reason reporting only after the field is reliably captured
### Default customer party role behavior
Customer party definitions are frozen, but there is still no `defaultPartyRole` behavior to help normalize customer selection and reduce operator ambiguity.
Future:
- decide whether customers should carry a suggested default project-party role
- add `defaultPartyRole` only if it improves data-entry quality without creating hidden automation
- keep dashboard logic independent from this field until the model is formally introduced
## After Task D.2.1
### Unified project-party reporting projection
Revenue attribution is now frozen, but the schema still stores enquiry and quotation parties in separate tables rather than a dedicated reporting projection.
Future:
- add a unified reporting view or materialized projection if dashboard/report queries become heavy
- keep quotation-party precedence and enquiry fallback centralized in one service until that projection exists
- avoid duplicating attribution logic in dashboard-specific code
### Historical party snapshot fidelity
Current revenue attribution uses live project-party relationships with quotation-first fallback behavior, but it does not yet provide effective-dated historical party snapshots for reporting.
Future:
- decide whether approved quotation snapshots or another immutable reporting snapshot should become the historical source of truth
- define how reporting should behave if project parties change after quotation approval or after a won/lost outcome
- add backfill or reconciliation tooling if historical party drift becomes a business issue