feat(pipeline): 9→7 stage refactor + v1.1 hardening wave
Replaces the legacy 9-stage pipeline with 7 canonical stages
(enquiry → qualified → eoi → reservation → deposit_paid → contract →
nurturing) plus three doc sub-status columns (eoi_doc_status,
reservation_doc_status, contract_doc_status) that track sent/signed
within a single stage instead of branching it.
Schema (migration 0062):
- interests gains assigned_to, deposit_expected_amount/currency,
three doc-status columns, two documenso-id columns, and
date_reservation_signed.
- New tables: qualification_criteria (per-port admin-configurable),
interest_qualifications (per-interest state), payments (deposit /
balance / refund records keyed to interest + client).
- Default qualification criteria seeded for every existing port.
- Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into
the new stage + doc-status + outcome shape.
Migration 0063 adds interest_contact_log.voice_transcript and
template_used columns for v1.1-A/B (quick-template buttons + voice
transcription via Web Speech API).
v1.1 phase work bundled here:
- A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on
the contact-log compose dialog (useVoiceTranscription hook).
- C: berth-rules-engine wraps state writes in pg_advisory_xact_lock
with an idempotent re-read; emits rule_evaluated audit traces.
- D: Documenso webhook: reservation/contract sub-status stamping
moved out of the PDF-download try-block so a download failure
no longer swallows the stamp. New integration test coverage.
- E: /admin/qualification-criteria CRUD page + admin component.
- F: default_new_interest_owner exposed in System Settings.
- G: recentActivityCount + active_engagement deal-pulse signal
surfaced as a chip on interests + hot-deals card.
- H: interest_assigned notification on assignedTo change (skips
self-assign, uses a dedupe key).
Plus the supporting components: AssignedToChip, DealPulseChip,
PaymentsSection, QualificationChecklist, MultiEoiChip,
SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner,
SupplementalInfoRequestButton, UserPicker.
Tests: 1370/1370 vitest pass (added deal-health unit suite +
expanded constants/validators/pipeline-transitions coverage). tsc
clean, eslint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
139
src/lib/db/migrations/0062_pipeline_refactor.sql
Normal file
139
src/lib/db/migrations/0062_pipeline_refactor.sql
Normal file
@@ -0,0 +1,139 @@
|
||||
-- 0062_pipeline_refactor.sql
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Pipeline refactor: 7 canonical stages, doc sub-status columns, qualification
|
||||
-- criteria, payment records, assigned_to ownership, expected deposit amount.
|
||||
-- Dummy-data only at this point; legacy-to-new migration for prod cutover is
|
||||
-- a separate one-shot tool to be written when production data is ready.
|
||||
|
||||
-- ─── interests: new columns ────────────────────────────────────────────────
|
||||
|
||||
ALTER TABLE interests
|
||||
ADD COLUMN IF NOT EXISTS assigned_to text REFERENCES user_profiles(user_id),
|
||||
ADD COLUMN IF NOT EXISTS deposit_expected_amount numeric,
|
||||
ADD COLUMN IF NOT EXISTS deposit_expected_currency text DEFAULT 'EUR',
|
||||
ADD COLUMN IF NOT EXISTS eoi_doc_status text,
|
||||
ADD COLUMN IF NOT EXISTS reservation_doc_status text,
|
||||
ADD COLUMN IF NOT EXISTS contract_doc_status text,
|
||||
ADD COLUMN IF NOT EXISTS reservation_documenso_id text,
|
||||
ADD COLUMN IF NOT EXISTS contract_documenso_id text,
|
||||
ADD COLUMN IF NOT EXISTS date_reservation_signed timestamptz;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_interests_assigned_to ON interests (assigned_to);
|
||||
|
||||
-- ─── stage value migration (collapse Sent/Signed pairs) ────────────────────
|
||||
-- Dummy-data only — destructive UPDATE is safe.
|
||||
|
||||
UPDATE interests SET pipeline_stage = 'enquiry'
|
||||
WHERE pipeline_stage IN ('open', 'details_sent', 'in_communication');
|
||||
|
||||
UPDATE interests
|
||||
SET pipeline_stage = 'eoi', eoi_doc_status = 'sent'
|
||||
WHERE pipeline_stage = 'eoi_sent';
|
||||
|
||||
UPDATE interests
|
||||
SET pipeline_stage = 'eoi', eoi_doc_status = 'signed'
|
||||
WHERE pipeline_stage = 'eoi_signed';
|
||||
|
||||
UPDATE interests SET pipeline_stage = 'deposit_paid'
|
||||
WHERE pipeline_stage = 'deposit_10pct';
|
||||
|
||||
UPDATE interests
|
||||
SET pipeline_stage = 'contract', contract_doc_status = 'sent'
|
||||
WHERE pipeline_stage = 'contract_sent';
|
||||
|
||||
UPDATE interests
|
||||
SET pipeline_stage = 'contract', contract_doc_status = 'signed'
|
||||
WHERE pipeline_stage = 'contract_signed';
|
||||
|
||||
-- `completed` collapses into contract+signed+won (the old terminal stage
|
||||
-- always implied outcome=won; outcome field carries that forward).
|
||||
UPDATE interests
|
||||
SET pipeline_stage = 'contract',
|
||||
contract_doc_status = 'signed',
|
||||
outcome = COALESCE(outcome, 'won'),
|
||||
outcome_at = COALESCE(outcome_at, updated_at)
|
||||
WHERE pipeline_stage = 'completed';
|
||||
|
||||
-- ─── Qualification criteria (per-port, admin-configurable) ──────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qualification_criteria (
|
||||
id text PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
||||
port_id text NOT NULL REFERENCES ports(id) ON DELETE CASCADE,
|
||||
key text NOT NULL,
|
||||
label text NOT NULL,
|
||||
description text,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
display_order int NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (port_id, key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_qualification_criteria_port
|
||||
ON qualification_criteria (port_id);
|
||||
|
||||
-- Seed the default criteria for every existing port. Port admins can later
|
||||
-- enable/disable or add port-specific entries.
|
||||
INSERT INTO qualification_criteria (port_id, key, label, description, enabled, display_order)
|
||||
SELECT p.id, 'dimensions', 'Dimensions confirmed',
|
||||
'We know the vessel''s length, width, and draft.', true, 1
|
||||
FROM ports p
|
||||
ON CONFLICT (port_id, key) DO NOTHING;
|
||||
|
||||
INSERT INTO qualification_criteria (port_id, key, label, description, enabled, display_order)
|
||||
SELECT p.id, 'intent', 'Intent confirmed',
|
||||
'Client has explicitly confirmed they want a berth at this marina.', true, 2
|
||||
FROM ports p
|
||||
ON CONFLICT (port_id, key) DO NOTHING;
|
||||
|
||||
-- These are built but disabled — admins enable per port when relevant.
|
||||
INSERT INTO qualification_criteria (port_id, key, label, description, enabled, display_order)
|
||||
SELECT p.id, 'signatory', 'Buyer signatory confirmed',
|
||||
'We know who is authorized to sign the EOI on the buyer side (owner / lawyer / company rep).',
|
||||
false, 3
|
||||
FROM ports p
|
||||
ON CONFLICT (port_id, key) DO NOTHING;
|
||||
|
||||
INSERT INTO qualification_criteria (port_id, key, label, description, enabled, display_order)
|
||||
SELECT p.id, 'timeline', 'Move-in timeline confirmed',
|
||||
'Client has indicated when they want to start using the berth.',
|
||||
false, 4
|
||||
FROM ports p
|
||||
ON CONFLICT (port_id, key) DO NOTHING;
|
||||
|
||||
-- ─── Per-interest qualification state ──────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS interest_qualifications (
|
||||
interest_id text NOT NULL REFERENCES interests(id) ON DELETE CASCADE,
|
||||
criterion_key text NOT NULL,
|
||||
confirmed boolean NOT NULL DEFAULT false,
|
||||
confirmed_at timestamptz,
|
||||
confirmed_by text,
|
||||
notes text,
|
||||
PRIMARY KEY (interest_id, criterion_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_interest_qualifications_interest
|
||||
ON interest_qualifications (interest_id);
|
||||
|
||||
-- ─── Payment records (no invoice generation) ───────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payments (
|
||||
id text PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
||||
port_id text NOT NULL REFERENCES ports(id) ON DELETE CASCADE,
|
||||
interest_id text NOT NULL REFERENCES interests(id) ON DELETE CASCADE,
|
||||
client_id text NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||
payment_type text NOT NULL, -- 'deposit' | 'balance' | 'refund' | 'other'
|
||||
amount numeric NOT NULL,
|
||||
currency text NOT NULL DEFAULT 'EUR',
|
||||
received_at timestamptz NOT NULL,
|
||||
receipt_file_id text REFERENCES files(id),
|
||||
notes text,
|
||||
recorded_by text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_interest ON payments (interest_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_client ON payments (client_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_port ON payments (port_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_type ON payments (port_id, payment_type);
|
||||
14
src/lib/db/migrations/0063_contact_log_voice_template.sql
Normal file
14
src/lib/db/migrations/0063_contact_log_voice_template.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- 0063_contact_log_voice_template.sql
|
||||
-- ----------------------------------------------------------------------------
|
||||
-- Pipeline-refactor follow-up. The contact-log captures sales-rep
|
||||
-- interactions; the v1 UX adds two new columns:
|
||||
-- - voice_transcript: raw Web Speech API output, kept separate from the
|
||||
-- rep-polished `summary` so we can re-render the
|
||||
-- transcript verbatim if the rep wants to revisit it.
|
||||
-- - template_used: which of the 3 quick-template buttons was tapped, if
|
||||
-- any. Surfaces in reports so admins can see how reps
|
||||
-- actually log activity (call/visit/email).
|
||||
|
||||
ALTER TABLE interest_contact_log
|
||||
ADD COLUMN IF NOT EXISTS voice_transcript text,
|
||||
ADD COLUMN IF NOT EXISTS template_used text;
|
||||
Reference in New Issue
Block a user