task-d.5
task-d.5.1
This commit is contained in:
117
docs/adr/0018-lead-enquiry-domain-separation.md
Normal file
117
docs/adr/0018-lead-enquiry-domain-separation.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# ADR 0018: Lead / Enquiry Domain Separation
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
ADR-0011 froze the current CRM lifecycle around a single persisted entity:
|
||||||
|
|
||||||
|
- `crm_enquiries` stores both marketing-owned leads and sales-owned enquiries
|
||||||
|
- `pipeline_stage` acts as the discriminator
|
||||||
|
- navigation separation exists, but domain separation does not
|
||||||
|
|
||||||
|
That model was an intentional stabilization step for Tasks D.3 and D.3.1. It reduced UX ambiguity without forcing a schema split during the first production rollout.
|
||||||
|
|
||||||
|
Task D.5 introduces a stronger business requirement:
|
||||||
|
|
||||||
|
- Marketing owns `Lead` as a dedicated entity
|
||||||
|
- Sales owns `Enquiry` as a dedicated entity
|
||||||
|
- one lead may create many enquiries
|
||||||
|
- sales may also create enquiries directly without a lead
|
||||||
|
- marketing outcome must be derived from sales execution, not manually controlled
|
||||||
|
|
||||||
|
The current single-record model now creates structural ambiguity in at least these areas:
|
||||||
|
|
||||||
|
1. Assignment currently mutates one record from `lead -> enquiry`, which destroys the original lead as a stable marketing record.
|
||||||
|
2. A true `1 Lead -> N Enquiries` relationship cannot be represented cleanly while the lead itself is also the enquiry record.
|
||||||
|
3. Dashboard and report logic still infer lead analytics from `crm_enquiries.pipeline_stage`, which couples marketing and sales datasets.
|
||||||
|
4. Outcome synchronization for marketing is harder to reason about because the source marketing entity does not exist independently.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
We will replace the single-record lead/enquiry persistence model with a split domain model:
|
||||||
|
|
||||||
|
- `crm_leads` becomes the dedicated marketing-owned lead table
|
||||||
|
- `crm_enquiries` remains the dedicated sales-owned enquiry table
|
||||||
|
- `crm_enquiries.lead_id` becomes nullable to support:
|
||||||
|
- lead-origin enquiries
|
||||||
|
- direct sales enquiries with `lead_id = null`
|
||||||
|
|
||||||
|
### Lifecycle
|
||||||
|
|
||||||
|
The target business flow is:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Lead
|
||||||
|
-> Enquiry
|
||||||
|
-> Quotation
|
||||||
|
-> Won / Lost
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relationship rules
|
||||||
|
|
||||||
|
- `crm_leads 1 -> N crm_enquiries`
|
||||||
|
- `crm_enquiries 1 -> N crm_quotations`
|
||||||
|
- `crm_quotations` remains the owning record for quotation revisions and child resources
|
||||||
|
|
||||||
|
### Ownership rules
|
||||||
|
|
||||||
|
- marketing owns lead creation and lead follow-up
|
||||||
|
- sales owns enquiry creation, enquiry follow-up, quotation generation, and won/lost execution
|
||||||
|
- managers and admins retain cross-workspace monitoring based on resolved CRM access
|
||||||
|
|
||||||
|
### Outcome rules
|
||||||
|
|
||||||
|
- lead outcome is derived, never manually updated
|
||||||
|
- when any linked enquiry becomes won, the lead outcome becomes `won`
|
||||||
|
- when all linked enquiries are lost, the lead outcome becomes `lost`
|
||||||
|
- otherwise the lead outcome remains `open`
|
||||||
|
|
||||||
|
### Sequence rules
|
||||||
|
|
||||||
|
- lead codes use a dedicated lead document sequence
|
||||||
|
- enquiry codes keep a separate enquiry document sequence
|
||||||
|
|
||||||
|
### Reporting and dashboard rules
|
||||||
|
|
||||||
|
- lead KPI and lead reports read from `crm_leads`
|
||||||
|
- enquiry KPI and enquiry reports read from `crm_enquiries`
|
||||||
|
- no report should continue to treat `pipeline_stage` as the marketing/sales entity boundary after migration
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- aligns persistence with the real marketing-to-sales handoff
|
||||||
|
- supports true `1 Lead -> N Enquiries`
|
||||||
|
- preserves lead identity after assignment
|
||||||
|
- simplifies marketing analytics and awareness-source reporting
|
||||||
|
- makes direct sales enquiries first-class without overloading lead semantics
|
||||||
|
|
||||||
|
### Tradeoffs
|
||||||
|
|
||||||
|
- supersedes a previously accepted ADR and requires broad migration across services, reports, dashboard, and permissions
|
||||||
|
- requires data migration from existing `crm_enquiries.pipeline_stage`
|
||||||
|
- increases temporary compatibility complexity during rollout
|
||||||
|
|
||||||
|
## Migration Strategy
|
||||||
|
|
||||||
|
The migration should be delivered in explicit phases:
|
||||||
|
|
||||||
|
1. Introduce ADR and governance freeze for split-domain direction.
|
||||||
|
2. Add schema and seed support for `crm_leads`, `lead_id`, and lead master options.
|
||||||
|
3. Introduce lead services and lead APIs without deleting enquiry flows.
|
||||||
|
4. Migrate dashboard and reports to separated datasets.
|
||||||
|
5. Remove old single-record assumptions from shared enquiry services and UI.
|
||||||
|
|
||||||
|
## Supersedes
|
||||||
|
|
||||||
|
This ADR is intended to supersede the following sections of ADR-0011 once implemented:
|
||||||
|
|
||||||
|
- `Single record model`
|
||||||
|
- `Pipeline stage model` as the lead/enquiry persistence boundary
|
||||||
|
- `Navigation Separation` as the primary separation mechanism
|
||||||
|
|
||||||
|
Until implementation is complete, ADR-0011 remains the description of current production behavior, while this ADR defines the approved target direction for Task D.5.
|
||||||
134
docs/implementation/task-d5-lead-enquiry-domain-separation.md
Normal file
134
docs/implementation/task-d5-lead-enquiry-domain-separation.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# Task D.5: Lead / Enquiry Domain Separation
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
In progress
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Replace the current single-record lead/enquiry persistence model with a split domain model that preserves marketing leads independently from sales enquiries.
|
||||||
|
|
||||||
|
## Review Summary
|
||||||
|
|
||||||
|
Reviewed before implementation:
|
||||||
|
|
||||||
|
- `AGENTS.md`
|
||||||
|
- `docs/standards/task-contract-template.md`
|
||||||
|
- `docs/standards/task-review-checklist.md`
|
||||||
|
- `docs/standards/task-catalog.md`
|
||||||
|
- `docs/standards/project-foundations.md`
|
||||||
|
- `docs/adr/0011-lead-enquiry-ownership-model.md`
|
||||||
|
- `docs/adr/0016-won-lost-lifecycle-governance.md`
|
||||||
|
- `docs/implementation/task-d3-lead-enquiry-ownership-refinement.md`
|
||||||
|
- `docs/implementation/task-d31-leads-enquiries-navigation-separation.md`
|
||||||
|
- `docs/implementation/task-d4-won-lost-lifecycle-governance.md`
|
||||||
|
- `docs/implementation/task-k2-pipeline-reports.md`
|
||||||
|
- `docs/security/crm-authorization-boundaries.md`
|
||||||
|
- `src/db/schema.ts`
|
||||||
|
- `src/features/crm/enquiries/server/service.ts`
|
||||||
|
|
||||||
|
## Historical Findings
|
||||||
|
|
||||||
|
### Current production truth
|
||||||
|
|
||||||
|
The current production CRM still follows ADR-0011:
|
||||||
|
|
||||||
|
- `crm_enquiries` is the single source of truth
|
||||||
|
- `pipeline_stage` distinguishes `lead`, `enquiry`, `closed_won`, `closed_lost`
|
||||||
|
- assigning a lead mutates the same record into an enquiry
|
||||||
|
- lead workspace and enquiry workspace are only UX separation
|
||||||
|
|
||||||
|
### Why D.5 is different
|
||||||
|
|
||||||
|
Task D.5 explicitly requires:
|
||||||
|
|
||||||
|
- dedicated `crm_leads`
|
||||||
|
- `crm_enquiries.lead_id`
|
||||||
|
- `1 Lead -> N Enquiries`
|
||||||
|
- direct sales enquiries with nullable `lead_id`
|
||||||
|
- dashboard/report split by entity rather than `pipeline_stage`
|
||||||
|
|
||||||
|
This is not an incremental extension of ADR-0011. It is a domain replacement.
|
||||||
|
|
||||||
|
## Governance Decision
|
||||||
|
|
||||||
|
Because Task D.5 conflicts with an accepted ADR, the first implementation step is to add a replacement ADR:
|
||||||
|
|
||||||
|
- `docs/adr/0018-lead-enquiry-domain-separation.md`
|
||||||
|
|
||||||
|
This task should not continue with schema or API changes while ADR-0011 remains the only source of truth for this area.
|
||||||
|
|
||||||
|
## Reuse Constraints
|
||||||
|
|
||||||
|
The split-domain refactor must still reuse existing foundations rather than rebuilding CRM from scratch:
|
||||||
|
|
||||||
|
- Audit Foundation for migration and lifecycle actions
|
||||||
|
- Master Data Foundation for lead awareness/status/follow-up/lost-reason options
|
||||||
|
- Document Sequence Foundation for lead and enquiry code generation
|
||||||
|
- Authorization Foundation for visibility and scope rules
|
||||||
|
- Dashboard Foundation for KPI composition
|
||||||
|
- Reporting Foundation for dataset and export contracts
|
||||||
|
- Won / Lost Governance Foundation for enquiry outcome transitions
|
||||||
|
|
||||||
|
## Phase Plan
|
||||||
|
|
||||||
|
### Phase 0
|
||||||
|
|
||||||
|
- freeze ADR-0018
|
||||||
|
- document the conflict with ADR-0011
|
||||||
|
- define migration boundaries
|
||||||
|
|
||||||
|
### Phase 1
|
||||||
|
|
||||||
|
- add `crm_leads`
|
||||||
|
- add `crm_enquiries.lead_id`
|
||||||
|
- add lead master option categories
|
||||||
|
- add lead document sequence type
|
||||||
|
|
||||||
|
### Phase 2
|
||||||
|
|
||||||
|
- create dedicated lead services and `/api/crm/leads/**`
|
||||||
|
- keep enquiry services focused on sales execution
|
||||||
|
- migrate assignment flow from `pipeline_stage` mutation to `lead -> create enquiry`
|
||||||
|
|
||||||
|
### Phase 3
|
||||||
|
|
||||||
|
- update dashboard datasets to separate lead/enquiry sources
|
||||||
|
- update report datasets and exports to separate lead/enquiry sources
|
||||||
|
- preserve quotation, approval, PDF, and pricing foundations
|
||||||
|
|
||||||
|
### Phase 4
|
||||||
|
|
||||||
|
- migrate legacy rows from `crm_enquiries.pipeline_stage`
|
||||||
|
- preserve follow-ups, project-party relations, attachments, quotations, and audit history
|
||||||
|
- remove single-record assumptions from surviving UI and server code
|
||||||
|
|
||||||
|
## Initial Risks
|
||||||
|
|
||||||
|
- this task affects schema, services, APIs, dashboard, reports, permissions, and migration logic at once
|
||||||
|
- existing lead reports and dashboard KPIs currently depend on `pipeline_stage`
|
||||||
|
- current enquiry services bundle lead creation defaults, assignment conversion, and won/lost logic together
|
||||||
|
- authorization rules for marketing monitoring are currently described against the shared enquiry table
|
||||||
|
|
||||||
|
## Deliverables Started
|
||||||
|
|
||||||
|
- added `docs/adr/0018-lead-enquiry-domain-separation.md`
|
||||||
|
- added `docs/implementation/task-d5-lead-enquiry-domain-separation.md`
|
||||||
|
|
||||||
|
## Next Implementation Slice
|
||||||
|
|
||||||
|
The next safe code slice is:
|
||||||
|
|
||||||
|
1. add schema support for `crm_leads` and `crm_enquiries.lead_id`
|
||||||
|
2. seed new lead option categories and document sequence configuration
|
||||||
|
3. keep existing enquiry routes live while introducing lead-specific route contracts
|
||||||
|
|
||||||
|
## Verification Target
|
||||||
|
|
||||||
|
When code phases begin, verification must include:
|
||||||
|
|
||||||
|
- `npm exec tsc --noEmit`
|
||||||
|
- migration validation for existing `crm_enquiries`
|
||||||
|
- direct sales enquiry creation with `leadId = null`
|
||||||
|
- lead assignment creating enquiries without deleting the lead
|
||||||
|
- outcome synchronization from enquiry results back to lead outcome
|
||||||
24
drizzle/0020_lead_foundation_schema.sql
Normal file
24
drizzle/0020_lead_foundation_schema.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "crm_leads" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"branch_id" text,
|
||||||
|
"code" text NOT NULL,
|
||||||
|
"customer_id" text,
|
||||||
|
"contact_id" text,
|
||||||
|
"project_name" text,
|
||||||
|
"project_location" text,
|
||||||
|
"awareness_id" text,
|
||||||
|
"status" text DEFAULT 'new_job' NOT NULL,
|
||||||
|
"followup_status" text,
|
||||||
|
"lost_reason" text,
|
||||||
|
"outcome" text DEFAULT 'open' NOT NULL,
|
||||||
|
"owner_marketing_user_id" text,
|
||||||
|
"created_by" text NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"deleted_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "crm_leads_org_code_idx" ON "crm_leads" USING btree ("organization_id","code");
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "crm_enquiries" ADD COLUMN IF NOT EXISTS "lead_id" text;
|
||||||
@@ -141,6 +141,13 @@
|
|||||||
"when": 1782137095907,
|
"when": 1782137095907,
|
||||||
"tag": "0019_sleepy_ultron",
|
"tag": "0019_sleepy_ultron",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 20,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1782262800000,
|
||||||
|
"tag": "0020_lead_foundation_schema",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
30
package-lock.json
generated
30
package-lock.json
generated
@@ -58,6 +58,7 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-orm": "^0.44.2",
|
"drizzle-orm": "^0.44.2",
|
||||||
|
"headroom-ai": "^0.22.4",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"kbar": "^0.1.0-beta.48",
|
"kbar": "^0.1.0-beta.48",
|
||||||
"match-sorter": "^8.2.0",
|
"match-sorter": "^8.2.0",
|
||||||
@@ -8000,6 +8001,35 @@
|
|||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/headroom-ai": {
|
||||||
|
"version": "0.22.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/headroom-ai/-/headroom-ai-0.22.4.tgz",
|
||||||
|
"integrity": "sha512-9a0rgB/jsWe8gs/ggyUwe6E8DYwKAuBvlUml2ApwlUjb5EfJ611X6X+WG0SiXw3nO6sdyV1/+Ah5uw9P7ecnjw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ai-sdk/provider": ">=1.0.0",
|
||||||
|
"@anthropic-ai/sdk": ">=0.30.0",
|
||||||
|
"ai": ">=6.0.0",
|
||||||
|
"openai": ">=4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@ai-sdk/provider": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@anthropic-ai/sdk": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"ai": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"openai": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html-entities": {
|
"node_modules/html-entities": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"drizzle-orm": "^0.44.2",
|
"drizzle-orm": "^0.44.2",
|
||||||
|
"headroom-ai": "^0.22.4",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"kbar": "^0.1.0-beta.48",
|
"kbar": "^0.1.0-beta.48",
|
||||||
"match-sorter": "^8.2.0",
|
"match-sorter": "^8.2.0",
|
||||||
|
|||||||
349
plans/task-d.5.1.md
Normal file
349
plans/task-d.5.1.md
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
# Task D.5.1: Lead Foundation Schema & Compatibility Layer
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Planned
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Create the foundational schema and compatibility layer required for Lead / Enquiry Domain Separation without breaking the existing CRM workflow.
|
||||||
|
|
||||||
|
This task introduces the Lead domain but does not yet migrate business flows.
|
||||||
|
|
||||||
|
The objective is to prepare the platform for D.5.2 and later phases while keeping all existing CRM features operational.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mandatory Review
|
||||||
|
|
||||||
|
Review:
|
||||||
|
|
||||||
|
* AGENTS.md
|
||||||
|
* Project Foundations Registry
|
||||||
|
* Task Catalog
|
||||||
|
* ADR-0011 Lead / Enquiry Ownership Model
|
||||||
|
* ADR-0016 Won / Lost Lifecycle Governance
|
||||||
|
* ADR-0018 Lead / Enquiry Domain Separation
|
||||||
|
* Task D.3
|
||||||
|
* Task D.3.1
|
||||||
|
* Task D.4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Required Skills
|
||||||
|
|
||||||
|
## Governance
|
||||||
|
|
||||||
|
* AGENTS.md
|
||||||
|
* Architecture Rules
|
||||||
|
* Task Contract Template
|
||||||
|
* Task Review Checklist
|
||||||
|
|
||||||
|
## CRM
|
||||||
|
|
||||||
|
* Lead / Enquiry Governance
|
||||||
|
* Won / Lost Governance
|
||||||
|
* Customer Ownership Governance
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
* Drizzle ORM Pattern
|
||||||
|
* Service Layer Pattern
|
||||||
|
* Route Handler Pattern
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
* CRM Authorization Foundation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope D5.1.1 Create Lead Table
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_leads
|
||||||
|
```
|
||||||
|
|
||||||
|
Initial fields:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
id
|
||||||
|
organizationId
|
||||||
|
branchId
|
||||||
|
|
||||||
|
code
|
||||||
|
|
||||||
|
customerId
|
||||||
|
contactId
|
||||||
|
|
||||||
|
projectName
|
||||||
|
projectLocation
|
||||||
|
|
||||||
|
awarenessId
|
||||||
|
|
||||||
|
status
|
||||||
|
followupStatus
|
||||||
|
lostReason
|
||||||
|
|
||||||
|
outcome
|
||||||
|
|
||||||
|
ownerMarketingUserId
|
||||||
|
|
||||||
|
createdBy
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
deletedAt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope D5.1.2 Add Lead Reference to Enquiry
|
||||||
|
|
||||||
|
Modify:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_enquiries
|
||||||
|
```
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
leadId nullable
|
||||||
|
```
|
||||||
|
|
||||||
|
Relationship:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_leads 1
|
||||||
|
→ N crm_enquiries
|
||||||
|
```
|
||||||
|
|
||||||
|
No business flow changes yet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope D5.1.3 Master Data Foundation
|
||||||
|
|
||||||
|
Create categories:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_lead_awareness
|
||||||
|
crm_lead_status
|
||||||
|
crm_lead_followup_status
|
||||||
|
crm_lead_lost_reason
|
||||||
|
```
|
||||||
|
|
||||||
|
Seed:
|
||||||
|
|
||||||
|
## crm_lead_awareness
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Google Search / Website
|
||||||
|
Social Media
|
||||||
|
Email Marketing
|
||||||
|
Visit
|
||||||
|
Exhibition / Event
|
||||||
|
Brochure / Catalog
|
||||||
|
Word of Mouth
|
||||||
|
Corporate Car
|
||||||
|
Product Label
|
||||||
|
Re-purchasing
|
||||||
|
Introducing
|
||||||
|
BCI
|
||||||
|
```
|
||||||
|
|
||||||
|
## crm_lead_status
|
||||||
|
|
||||||
|
```txt
|
||||||
|
new_job
|
||||||
|
follow_up
|
||||||
|
closed_lost
|
||||||
|
cancel
|
||||||
|
```
|
||||||
|
|
||||||
|
## crm_lead_followup_status
|
||||||
|
|
||||||
|
```txt
|
||||||
|
budget_pending
|
||||||
|
project_under_review
|
||||||
|
spec_clarification
|
||||||
|
site_survey_waiting
|
||||||
|
drawing_followup
|
||||||
|
quotation_preparation
|
||||||
|
waiting_po
|
||||||
|
```
|
||||||
|
|
||||||
|
## crm_lead_lost_reason
|
||||||
|
|
||||||
|
```txt
|
||||||
|
price
|
||||||
|
delivery_time
|
||||||
|
sales_person
|
||||||
|
service
|
||||||
|
customer
|
||||||
|
product_quality
|
||||||
|
other
|
||||||
|
transfer
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope D5.1.4 Document Sequence Foundation
|
||||||
|
|
||||||
|
Register:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_lead
|
||||||
|
crm_enquiry
|
||||||
|
```
|
||||||
|
|
||||||
|
Document codes:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Lead:
|
||||||
|
LD2606-001
|
||||||
|
|
||||||
|
Enquiry:
|
||||||
|
EN2606-001
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not switch existing generation logic yet.
|
||||||
|
|
||||||
|
Only prepare configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope D5.1.5 Compatibility Layer
|
||||||
|
|
||||||
|
Keep existing:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_enquiries.pipeline_stage
|
||||||
|
```
|
||||||
|
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
No route behavior changes.
|
||||||
|
|
||||||
|
No dashboard changes.
|
||||||
|
|
||||||
|
No report changes.
|
||||||
|
|
||||||
|
No migration of existing records.
|
||||||
|
|
||||||
|
This task prepares schema only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Scope D5.1.6 Authorization Preparation
|
||||||
|
|
||||||
|
Add placeholders for future Lead permissions:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm.lead.read
|
||||||
|
crm.lead.create
|
||||||
|
crm.lead.update
|
||||||
|
crm.lead.assign
|
||||||
|
crm.lead.delete
|
||||||
|
```
|
||||||
|
|
||||||
|
No enforcement changes yet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Deliverables
|
||||||
|
|
||||||
|
## New
|
||||||
|
|
||||||
|
* crm_leads schema
|
||||||
|
* lead master option categories
|
||||||
|
* lead document sequence configuration
|
||||||
|
* ADR-0018 references
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modified
|
||||||
|
|
||||||
|
* crm_enquiries
|
||||||
|
* foundation.seed.ts
|
||||||
|
* document sequence seed
|
||||||
|
* CRM permission registry
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Explicit Non-Scope
|
||||||
|
|
||||||
|
Do NOT:
|
||||||
|
|
||||||
|
* create Lead APIs
|
||||||
|
* create Lead UI
|
||||||
|
* create Lead assignment workflow
|
||||||
|
* migrate existing records
|
||||||
|
* change dashboard
|
||||||
|
* change reports
|
||||||
|
* change security behavior
|
||||||
|
* remove pipeline_stage
|
||||||
|
|
||||||
|
Those belong to later D.5 phases.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Verification
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
npm exec tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_leads table exists
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
crm_enquiries.leadId exists
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
lead master options seeded
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
document sequence configuration exists
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
existing CRM workflow remains unchanged
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Definition of Done
|
||||||
|
|
||||||
|
Task is complete when:
|
||||||
|
|
||||||
|
* Lead schema exists
|
||||||
|
* Enquiry can reference Lead
|
||||||
|
* Lead master data exists
|
||||||
|
* Lead document sequence exists
|
||||||
|
* Existing CRM remains fully operational
|
||||||
|
* No business workflow regression occurs
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
Lead Foundation = Established
|
||||||
|
|
||||||
|
Compatibility Layer = Established
|
||||||
|
|
||||||
|
Ready for D.5.2 Lead APIs & UI
|
||||||
@@ -266,6 +266,33 @@ export const crmCustomerContacts = pgTable('crm_customer_contacts', {
|
|||||||
updatedBy: text('updated_by').notNull()
|
updatedBy: text('updated_by').notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const crmLeads = pgTable(
|
||||||
|
'crm_leads',
|
||||||
|
{
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
organizationId: text('organization_id').notNull(),
|
||||||
|
branchId: text('branch_id'),
|
||||||
|
code: text('code').notNull(),
|
||||||
|
customerId: text('customer_id'),
|
||||||
|
contactId: text('contact_id'),
|
||||||
|
projectName: text('project_name'),
|
||||||
|
projectLocation: text('project_location'),
|
||||||
|
awarenessId: text('awareness_id'),
|
||||||
|
status: text('status').default('new_job').notNull(),
|
||||||
|
followupStatus: text('followup_status'),
|
||||||
|
lostReason: text('lost_reason'),
|
||||||
|
outcome: text('outcome').default('open').notNull(),
|
||||||
|
ownerMarketingUserId: text('owner_marketing_user_id'),
|
||||||
|
createdBy: text('created_by').notNull(),
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
deletedAt: timestamp('deleted_at', { withTimezone: true })
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
organizationCodeIdx: uniqueIndex('crm_leads_org_code_idx').on(table.organizationId, table.code)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const crmContactShares = pgTable(
|
export const crmContactShares = pgTable(
|
||||||
'crm_contact_shares',
|
'crm_contact_shares',
|
||||||
{
|
{
|
||||||
@@ -296,6 +323,7 @@ export const crmEnquiries = pgTable(
|
|||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
organizationId: text('organization_id').notNull(),
|
organizationId: text('organization_id').notNull(),
|
||||||
branchId: text('branch_id'),
|
branchId: text('branch_id'),
|
||||||
|
leadId: text('lead_id'),
|
||||||
code: text('code').notNull(),
|
code: text('code').notNull(),
|
||||||
customerId: text('customer_id').notNull(),
|
customerId: text('customer_id').notNull(),
|
||||||
contactId: text('contact_id'),
|
contactId: text('contact_id'),
|
||||||
|
|||||||
@@ -342,6 +342,105 @@ const FOUNDATION_OPTIONS = {
|
|||||||
{ code: 'line', label: 'LINE', value: 'line', sortOrder: 5 },
|
{ code: 'line', label: 'LINE', value: 'line', sortOrder: 5 },
|
||||||
{ code: 'other', label: 'Other', value: 'other', sortOrder: 6 }
|
{ code: 'other', label: 'Other', value: 'other', sortOrder: 6 }
|
||||||
],
|
],
|
||||||
|
crm_lead_awareness: [
|
||||||
|
{
|
||||||
|
code: 'google_search_website',
|
||||||
|
label: 'Google Search / Website',
|
||||||
|
value: 'google_search_website',
|
||||||
|
sortOrder: 1
|
||||||
|
},
|
||||||
|
{ code: 'social_media', label: 'Social Media', value: 'social_media', sortOrder: 2 },
|
||||||
|
{
|
||||||
|
code: 'email_marketing',
|
||||||
|
label: 'Email Marketing',
|
||||||
|
value: 'email_marketing',
|
||||||
|
sortOrder: 3
|
||||||
|
},
|
||||||
|
{ code: 'visit', label: 'Visit', value: 'visit', sortOrder: 4 },
|
||||||
|
{
|
||||||
|
code: 'exhibition_event',
|
||||||
|
label: 'Exhibition / Event',
|
||||||
|
value: 'exhibition_event',
|
||||||
|
sortOrder: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'brochure_catalog',
|
||||||
|
label: 'Brochure / Catalog',
|
||||||
|
value: 'brochure_catalog',
|
||||||
|
sortOrder: 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'word_of_mouth',
|
||||||
|
label: 'Word of Mouth',
|
||||||
|
value: 'word_of_mouth',
|
||||||
|
sortOrder: 7
|
||||||
|
},
|
||||||
|
{ code: 'corporate_car', label: 'Corporate Car', value: 'corporate_car', sortOrder: 8 },
|
||||||
|
{ code: 'product_label', label: 'Product Label', value: 'product_label', sortOrder: 9 },
|
||||||
|
{ code: 'repurchasing', label: 'Re-purchasing', value: 'repurchasing', sortOrder: 10 },
|
||||||
|
{ code: 'introducing', label: 'Introducing', value: 'introducing', sortOrder: 11 },
|
||||||
|
{ code: 'bci', label: 'BCI', value: 'bci', sortOrder: 12 }
|
||||||
|
],
|
||||||
|
crm_lead_status: [
|
||||||
|
{ code: 'new_job', label: 'New Job', value: 'new_job', sortOrder: 1 },
|
||||||
|
{ code: 'follow_up', label: 'Follow Up', value: 'follow_up', sortOrder: 2 },
|
||||||
|
{ code: 'closed_lost', label: 'Closed Lost', value: 'closed_lost', sortOrder: 3 },
|
||||||
|
{ code: 'cancel', label: 'Cancel', value: 'cancel', sortOrder: 4 }
|
||||||
|
],
|
||||||
|
crm_lead_followup_status: [
|
||||||
|
{
|
||||||
|
code: 'budget_pending',
|
||||||
|
label: 'Budget Pending',
|
||||||
|
value: 'budget_pending',
|
||||||
|
sortOrder: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'project_under_review',
|
||||||
|
label: 'Project Under Review',
|
||||||
|
value: 'project_under_review',
|
||||||
|
sortOrder: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'spec_clarification',
|
||||||
|
label: 'Spec Clarification',
|
||||||
|
value: 'spec_clarification',
|
||||||
|
sortOrder: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'site_survey_waiting',
|
||||||
|
label: 'Site Survey Waiting',
|
||||||
|
value: 'site_survey_waiting',
|
||||||
|
sortOrder: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'drawing_followup',
|
||||||
|
label: 'Drawing Follow-up',
|
||||||
|
value: 'drawing_followup',
|
||||||
|
sortOrder: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'quotation_preparation',
|
||||||
|
label: 'Quotation Preparation',
|
||||||
|
value: 'quotation_preparation',
|
||||||
|
sortOrder: 6
|
||||||
|
},
|
||||||
|
{ code: 'waiting_po', label: 'Waiting PO', value: 'waiting_po', sortOrder: 7 }
|
||||||
|
],
|
||||||
|
crm_lead_lost_reason: [
|
||||||
|
{ code: 'price', label: 'Price', value: 'price', sortOrder: 1 },
|
||||||
|
{ code: 'delivery_time', label: 'Delivery Time', value: 'delivery_time', sortOrder: 2 },
|
||||||
|
{ code: 'sales_person', label: 'Sales Person', value: 'sales_person', sortOrder: 3 },
|
||||||
|
{ code: 'service', label: 'Service', value: 'service', sortOrder: 4 },
|
||||||
|
{ code: 'customer', label: 'Customer', value: 'customer', sortOrder: 5 },
|
||||||
|
{
|
||||||
|
code: 'product_quality',
|
||||||
|
label: 'Product Quality',
|
||||||
|
value: 'product_quality',
|
||||||
|
sortOrder: 6
|
||||||
|
},
|
||||||
|
{ code: 'other', label: 'Other', value: 'other', sortOrder: 7 },
|
||||||
|
{ code: 'transfer', label: 'Transfer', value: 'transfer', sortOrder: 8 }
|
||||||
|
],
|
||||||
crm_report_category: [
|
crm_report_category: [
|
||||||
{ code: 'pipeline', label: 'Pipeline', value: 'pipeline', sortOrder: 1 },
|
{ code: 'pipeline', label: 'Pipeline', value: 'pipeline', sortOrder: 1 },
|
||||||
{ code: 'sales', label: 'Sales', value: 'sales', sortOrder: 2 },
|
{ code: 'sales', label: 'Sales', value: 'sales', sortOrder: 2 },
|
||||||
@@ -421,6 +520,8 @@ const DOCUMENT_SEQUENCES = [
|
|||||||
{ documentType: 'customer', prefix: 'CUS' },
|
{ documentType: 'customer', prefix: 'CUS' },
|
||||||
{ documentType: 'contact', prefix: 'CON' },
|
{ documentType: 'contact', prefix: 'CON' },
|
||||||
{ documentType: 'enquiry', prefix: 'ENQ' },
|
{ documentType: 'enquiry', prefix: 'ENQ' },
|
||||||
|
{ documentType: 'crm_lead', prefix: 'LD' },
|
||||||
|
{ documentType: 'crm_enquiry', prefix: 'EN' },
|
||||||
{ documentType: 'quotation', prefix: 'QT' },
|
{ documentType: 'quotation', prefix: 'QT' },
|
||||||
{ documentType: 'approval', prefix: 'APV' }
|
{ documentType: 'approval', prefix: 'APV' }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ const DEFAULT_DOCUMENT_PREFIXES: Record<string, string> = {
|
|||||||
customer: 'CUS',
|
customer: 'CUS',
|
||||||
contact: 'CON',
|
contact: 'CON',
|
||||||
enquiry: 'ENQ',
|
enquiry: 'ENQ',
|
||||||
|
crm_lead: 'LD',
|
||||||
|
crm_enquiry: 'EN',
|
||||||
quotation: 'QT',
|
quotation: 'QT',
|
||||||
approval: 'APV'
|
approval: 'APV'
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user