diff --git a/docs/adr/0018-lead-enquiry-domain-separation.md b/docs/adr/0018-lead-enquiry-domain-separation.md new file mode 100644 index 0000000..0d86968 --- /dev/null +++ b/docs/adr/0018-lead-enquiry-domain-separation.md @@ -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. diff --git a/docs/implementation/task-d5-lead-enquiry-domain-separation.md b/docs/implementation/task-d5-lead-enquiry-domain-separation.md new file mode 100644 index 0000000..cdc7d55 --- /dev/null +++ b/docs/implementation/task-d5-lead-enquiry-domain-separation.md @@ -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 diff --git a/drizzle/0020_lead_foundation_schema.sql b/drizzle/0020_lead_foundation_schema.sql new file mode 100644 index 0000000..4f26edf --- /dev/null +++ b/drizzle/0020_lead_foundation_schema.sql @@ -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; diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 040e14a..1740109 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -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 } ] -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 63b03fe..8d5756b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 71f96fb..fa05946 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/plans/task-d.5.1.md b/plans/task-d.5.1.md new file mode 100644 index 0000000..b0e19fb --- /dev/null +++ b/plans/task-d.5.1.md @@ -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 diff --git a/src/db/schema.ts b/src/db/schema.ts index dc81c51..a1847aa 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -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'), diff --git a/src/db/seeds/foundation.seed.ts b/src/db/seeds/foundation.seed.ts index d645953..4c2aef7 100644 --- a/src/db/seeds/foundation.seed.ts +++ b/src/db/seeds/foundation.seed.ts @@ -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' } ]; diff --git a/src/features/foundation/document-sequence/service.ts b/src/features/foundation/document-sequence/service.ts index f2ceabe..05388f5 100644 --- a/src/features/foundation/document-sequence/service.ts +++ b/src/features/foundation/document-sequence/service.ts @@ -16,6 +16,8 @@ const DEFAULT_DOCUMENT_PREFIXES: Record = { customer: 'CUS', contact: 'CON', enquiry: 'ENQ', + crm_lead: 'LD', + crm_enquiry: 'EN', quotation: 'QT', approval: 'APV' };