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,
|
||||
"tag": "0019_sleepy_ultron",
|
||||
"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",
|
||||
"date-fns": "^4.1.0",
|
||||
"drizzle-orm": "^0.44.2",
|
||||
"headroom-ai": "^0.22.4",
|
||||
"input-otp": "^1.4.2",
|
||||
"kbar": "^0.1.0-beta.48",
|
||||
"match-sorter": "^8.2.0",
|
||||
@@ -8000,6 +8001,35 @@
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"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": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"drizzle-orm": "^0.44.2",
|
||||
"headroom-ai": "^0.22.4",
|
||||
"input-otp": "^1.4.2",
|
||||
"kbar": "^0.1.0-beta.48",
|
||||
"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()
|
||||
});
|
||||
|
||||
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(
|
||||
'crm_contact_shares',
|
||||
{
|
||||
@@ -296,6 +323,7 @@ export const crmEnquiries = pgTable(
|
||||
id: text('id').primaryKey(),
|
||||
organizationId: text('organization_id').notNull(),
|
||||
branchId: text('branch_id'),
|
||||
leadId: text('lead_id'),
|
||||
code: text('code').notNull(),
|
||||
customerId: text('customer_id').notNull(),
|
||||
contactId: text('contact_id'),
|
||||
|
||||
@@ -342,6 +342,105 @@ const FOUNDATION_OPTIONS = {
|
||||
{ code: 'line', label: 'LINE', value: 'line', sortOrder: 5 },
|
||||
{ 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: [
|
||||
{ code: 'pipeline', label: 'Pipeline', value: 'pipeline', sortOrder: 1 },
|
||||
{ code: 'sales', label: 'Sales', value: 'sales', sortOrder: 2 },
|
||||
@@ -421,6 +520,8 @@ const DOCUMENT_SEQUENCES = [
|
||||
{ documentType: 'customer', prefix: 'CUS' },
|
||||
{ documentType: 'contact', prefix: 'CON' },
|
||||
{ documentType: 'enquiry', prefix: 'ENQ' },
|
||||
{ documentType: 'crm_lead', prefix: 'LD' },
|
||||
{ documentType: 'crm_enquiry', prefix: 'EN' },
|
||||
{ documentType: 'quotation', prefix: 'QT' },
|
||||
{ documentType: 'approval', prefix: 'APV' }
|
||||
];
|
||||
|
||||
@@ -16,6 +16,8 @@ const DEFAULT_DOCUMENT_PREFIXES: Record<string, string> = {
|
||||
customer: 'CUS',
|
||||
contact: 'CON',
|
||||
enquiry: 'ENQ',
|
||||
crm_lead: 'LD',
|
||||
crm_enquiry: 'EN',
|
||||
quotation: 'QT',
|
||||
approval: 'APV'
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user