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:
@@ -143,6 +143,13 @@ export const documentSends = pgTable(
|
||||
failedAt: timestamp('failed_at', { withTimezone: true }),
|
||||
/** Human-readable failure reason; only meaningful when failedAt is non-null. */
|
||||
errorReason: text('error_reason'),
|
||||
// Phase 6 — async bounce tracking. Populated by the IMAP NDR
|
||||
// poller (`src/jobs/processors/imap-bounce-poller.ts`) when a
|
||||
// delivery failure message arrives in the configured mailbox and
|
||||
// matches this send via recipient_email + sent_at window.
|
||||
bounceStatus: text('bounce_status'), // 'hard' | 'soft' | 'ooo'
|
||||
bounceReason: text('bounce_reason'),
|
||||
bounceDetectedAt: timestamp('bounce_detected_at', { withTimezone: true }),
|
||||
},
|
||||
(t) => [
|
||||
index('idx_ds_client').on(t.clientId, t.sentAt),
|
||||
|
||||
@@ -89,6 +89,13 @@ export const clientContacts = pgTable(
|
||||
label: text('label'), // primary, secondary, work, personal, broker, assistant
|
||||
isPrimary: boolean('is_primary').notNull().default(false),
|
||||
notes: text('notes'),
|
||||
// Phase 3 — origin tracking.
|
||||
// source: 'manual' | 'imported' | 'eoi-custom-input'
|
||||
// source_document_id: when source='eoi-custom-input', points at the
|
||||
// EOI document this row was spawned from. Surfaces an [EOI] badge
|
||||
// on the client detail panel + a link back to the generating doc.
|
||||
source: text('source').notNull().default('manual'),
|
||||
sourceDocumentId: text('source_document_id'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
@@ -249,6 +256,9 @@ export const clientAddresses = pgTable(
|
||||
/** ISO-3166-1 alpha-2 country code. */
|
||||
countryIso: text('country_iso'),
|
||||
isPrimary: boolean('is_primary').notNull().default(true),
|
||||
// Phase 3 — origin tracking, same pattern as client_contacts.
|
||||
source: text('source').notNull().default('manual'),
|
||||
sourceDocumentId: text('source_document_id'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -46,6 +46,12 @@ export const yachts = pgTable(
|
||||
status: text('status').notNull().default('active'), // 'active' | 'retired' | 'sold_away'
|
||||
notes: text('notes'),
|
||||
archivedAt: timestamp('archived_at', { withTimezone: true }),
|
||||
// Phase 3 — origin tracking. eoi-generated marks yachts that were
|
||||
// spawned via the EOI dialog's "+ New yacht" inline form, so the
|
||||
// detail page can surface an [EOI] badge + link to the originating
|
||||
// document.
|
||||
source: text('source').notNull().default('manual'),
|
||||
sourceDocumentId: text('source_document_id'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user