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:
2026-05-14 03:39:21 +02:00
parent b10bf9bf8e
commit 6b28459c45
110 changed files with 5402 additions and 796 deletions

View 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);

View 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;