feat(post-audit): Phase 3/6/7 schema foundations + bounce parser
Phase 3 — EOI override foundation (migration 0073): - client_contacts/addresses/yachts get source + source_document_id with FK SET NULL on doc deletion. CHECK constraints enforce the allow-list of source values (manual/imported/eoi-custom-input or manual/imported/eoi-generated for yachts). - documents.override_client_* + override_yacht_* columns mirror the AcroForm field set per docs/eoi-documenso-field-mapping.md. When NULL the canonical record value flows; when set, this document uses the override without touching the underlying record. - Drizzle schema mirrors all new columns; numeric import added to documents schema for the yacht-dimensions override columns. Phase 6 — IMAP bounce foundation (migration 0074): - document_sends.bounce_status / bounce_reason / bounce_detected_at with bounce_status CHECK constraint (hard/soft/ooo). - Partial index for the "show bounced sends" UI filter. - New src/lib/email/bounce-parser.ts library — handles RFC 3464 DSN + Outlook NDR shapes + OOO auto-replies. Returns null recipient + 'unknown' class when shape isn't recognizable. Cron worker deferred to Phase 6b. Phase 7 — PDF editor field-map types: - New src/lib/templates/field-map.ts defines FieldMap shape with percent-coord positioning so placements survive page-size changes. - Zod schemas for API boundary validation. - validateFieldMapAgainstPageCount helper for the "new PDF upload" warning. - No schema migration needed — existing document_templates. overlay_positions JSONB column accepts the new shape; the editor migrates legacy absolute-coord entries on first save. Tests: 1374/1374 passing. tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
114
src/lib/db/migrations/0073_phase3_eoi_overrides.sql
Normal file
114
src/lib/db/migrations/0073_phase3_eoi_overrides.sql
Normal file
@@ -0,0 +1,114 @@
|
||||
-- Phase 3 — EOI field override foundation (POST-AUDIT-SPEC §1 + MASTER-PLAN §C).
|
||||
--
|
||||
-- This migration is foundation-only. The EOI generate-and-sign endpoint
|
||||
-- + the dialog UI extensions land in subsequent sub-sessions (3a-3d).
|
||||
--
|
||||
-- Adds:
|
||||
-- 1. client_contacts.{source, source_document_id}
|
||||
-- 2. client_addresses.{source, source_document_id}
|
||||
-- 3. yachts.{source, source_document_id}
|
||||
-- 4. documents.override_* columns mirroring the AcroForm field map
|
||||
--
|
||||
-- audit_actions is a free-text column (text, not enum) — new action verbs
|
||||
-- 'eoi_field_override', 'promote_to_primary', 'eoi_spawn_yacht' don't
|
||||
-- need DDL; they just appear in inserted rows.
|
||||
|
||||
-- ─── client_contacts ──────────────────────────────────────────────────
|
||||
ALTER TABLE client_contacts
|
||||
ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT 'manual',
|
||||
ADD COLUMN IF NOT EXISTS source_document_id text;
|
||||
|
||||
-- Add FK only if it doesn't exist yet. ON DELETE SET NULL — keep the
|
||||
-- contact row around even if the originating document is deleted.
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'client_contacts_source_document_id_fkey') THEN
|
||||
ALTER TABLE client_contacts
|
||||
ADD CONSTRAINT client_contacts_source_document_id_fkey
|
||||
FOREIGN KEY (source_document_id) REFERENCES documents(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_client_contacts_source') THEN
|
||||
ALTER TABLE client_contacts
|
||||
ADD CONSTRAINT chk_client_contacts_source
|
||||
CHECK (source IN ('manual', 'imported', 'eoi-custom-input'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ─── client_addresses ──────────────────────────────────────────────────
|
||||
ALTER TABLE client_addresses
|
||||
ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT 'manual',
|
||||
ADD COLUMN IF NOT EXISTS source_document_id text;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'client_addresses_source_document_id_fkey') THEN
|
||||
ALTER TABLE client_addresses
|
||||
ADD CONSTRAINT client_addresses_source_document_id_fkey
|
||||
FOREIGN KEY (source_document_id) REFERENCES documents(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_client_addresses_source') THEN
|
||||
ALTER TABLE client_addresses
|
||||
ADD CONSTRAINT chk_client_addresses_source
|
||||
CHECK (source IN ('manual', 'imported', 'eoi-custom-input'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ─── yachts ────────────────────────────────────────────────────────────
|
||||
ALTER TABLE yachts
|
||||
ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT 'manual',
|
||||
ADD COLUMN IF NOT EXISTS source_document_id text;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'yachts_source_document_id_fkey') THEN
|
||||
ALTER TABLE yachts
|
||||
ADD CONSTRAINT yachts_source_document_id_fkey
|
||||
FOREIGN KEY (source_document_id) REFERENCES documents(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_yachts_source') THEN
|
||||
ALTER TABLE yachts
|
||||
ADD CONSTRAINT chk_yachts_source
|
||||
CHECK (source IN ('manual', 'imported', 'eoi-generated'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ─── documents — per-document overrides ────────────────────────────────
|
||||
-- These columns mirror the AcroForm field set per
|
||||
-- docs/eoi-documenso-field-mapping.md. When NULL, the canonical
|
||||
-- client/yacht values flow through unchanged. When set, the EOI uses
|
||||
-- the override value without touching the underlying record.
|
||||
ALTER TABLE documents
|
||||
ADD COLUMN IF NOT EXISTS override_client_email text,
|
||||
ADD COLUMN IF NOT EXISTS override_client_phone text,
|
||||
ADD COLUMN IF NOT EXISTS override_client_address_line_1 text,
|
||||
ADD COLUMN IF NOT EXISTS override_client_address_line_2 text,
|
||||
ADD COLUMN IF NOT EXISTS override_client_city text,
|
||||
ADD COLUMN IF NOT EXISTS override_client_state text,
|
||||
ADD COLUMN IF NOT EXISTS override_client_postal_code text,
|
||||
ADD COLUMN IF NOT EXISTS override_client_country text,
|
||||
ADD COLUMN IF NOT EXISTS override_yacht_name text,
|
||||
ADD COLUMN IF NOT EXISTS override_yacht_length_ft numeric(10, 2),
|
||||
ADD COLUMN IF NOT EXISTS override_yacht_width_ft numeric(10, 2),
|
||||
ADD COLUMN IF NOT EXISTS override_yacht_draft_ft numeric(10, 2);
|
||||
|
||||
-- ─── Comments ──────────────────────────────────────────────────────────
|
||||
COMMENT ON COLUMN client_contacts.source IS
|
||||
'Phase 3: origin of this contact row. manual = rep typed it; imported = bulk import; eoi-custom-input = inline-typed during EOI generation.';
|
||||
COMMENT ON COLUMN client_contacts.source_document_id IS
|
||||
'Phase 3: when source=eoi-custom-input, points at the EOI that created it.';
|
||||
COMMENT ON COLUMN yachts.source IS
|
||||
'Phase 3: origin. eoi-generated = spawned from the EOI dialog''s "+ New yacht" inline form.';
|
||||
COMMENT ON COLUMN documents.override_client_email IS
|
||||
'Phase 3: per-document override of the client''s email field on this EOI. NULL = use canonical client_contacts row.';
|
||||
33
src/lib/db/migrations/0074_phase6_bounce_tracking.sql
Normal file
33
src/lib/db/migrations/0074_phase6_bounce_tracking.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- Phase 6 — IMAP bounce-to-interest linking foundation
|
||||
-- (POST-AUDIT-SPEC §14.9 / MASTER-PLAN §F).
|
||||
--
|
||||
-- Schema-only. Parser library + cron worker land in 6b/6c.
|
||||
--
|
||||
-- Adds bounce_status / bounce_reason / bounce_detected_at columns to
|
||||
-- document_sends + a partial index so the UI's "show bounced rows" filter
|
||||
-- is cheap.
|
||||
|
||||
ALTER TABLE document_sends
|
||||
ADD COLUMN IF NOT EXISTS bounce_status text,
|
||||
ADD COLUMN IF NOT EXISTS bounce_reason text,
|
||||
ADD COLUMN IF NOT EXISTS bounce_detected_at timestamptz;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_document_sends_bounce_status') THEN
|
||||
ALTER TABLE document_sends
|
||||
ADD CONSTRAINT chk_document_sends_bounce_status
|
||||
CHECK (bounce_status IS NULL OR bounce_status IN ('hard', 'soft', 'ooo'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_document_sends_bounce
|
||||
ON document_sends (port_id, bounce_detected_at)
|
||||
WHERE bounce_status IS NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN document_sends.bounce_status IS
|
||||
'Phase 6: hard|soft|ooo classification when an NDR for this send was matched by the IMAP poller. NULL = no bounce detected (default).';
|
||||
COMMENT ON COLUMN document_sends.bounce_reason IS
|
||||
'Phase 6: human-readable reason from the parsed NDR (e.g. "user not found"). NULL when not bounced.';
|
||||
COMMENT ON COLUMN document_sends.bounce_detected_at IS
|
||||
'Phase 6: timestamp the cron worker detected this bounce. Drives the in-CRM notification firing once.';
|
||||
Reference in New Issue
Block a user