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:
2026-05-18 15:09:22 +02:00
parent fb4a09e2ec
commit 9f5786890e
8 changed files with 425 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ import {
text,
boolean,
integer,
numeric,
timestamp,
jsonb,
index,
@@ -112,6 +113,24 @@ export const documents = pgTable(
invitationMessage: text('invitation_message'),
remindersDisabled: boolean('reminders_disabled').notNull().default(false),
reminderCadenceOverride: integer('reminder_cadence_override'),
// Phase 3 — per-document field overrides. When NULL, the canonical
// client/yacht record value flows through; when set, this document
// uses the override without touching the underlying record. Mirrors
// the AcroForm field set per docs/eoi-documenso-field-mapping.md.
// These are NOT promoted to client_contacts/addresses/yachts unless
// the dialog's "Save as new" toggle is ticked at generate time.
overrideClientEmail: text('override_client_email'),
overrideClientPhone: text('override_client_phone'),
overrideClientAddressLine1: text('override_client_address_line_1'),
overrideClientAddressLine2: text('override_client_address_line_2'),
overrideClientCity: text('override_client_city'),
overrideClientState: text('override_client_state'),
overrideClientPostalCode: text('override_client_postal_code'),
overrideClientCountry: text('override_client_country'),
overrideYachtName: text('override_yacht_name'),
overrideYachtLengthFt: numeric('override_yacht_length_ft'),
overrideYachtWidthFt: numeric('override_yacht_width_ft'),
overrideYachtDraftFt: numeric('override_yacht_draft_ft'),
createdBy: text('created_by').notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),