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:
53
src/lib/templates/field-map.ts
Normal file
53
src/lib/templates/field-map.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Phase 7 — PDF template editor field-map type definitions.
|
||||
*
|
||||
* The editor (deferred to 7.1/7.2) operates on the existing
|
||||
* `document_templates.overlay_positions` JSONB column with the extended
|
||||
* shape below. No schema migration is required — JSONB already accepts
|
||||
* any shape; the only contract is the Zod validator on the API boundary.
|
||||
*
|
||||
* Coordinates are stored as percent of page width/height (0..1) so
|
||||
* field placements survive page-size changes (A4 → US Letter etc.).
|
||||
* The legacy overlay schema used absolute coords; the editor migrates
|
||||
* those on first save by reading the source PDF's page dimensions.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
export const fieldMapEntrySchema = z.object({
|
||||
/** Must match an entry in VALID_MERGE_TOKENS (validated at the API). */
|
||||
token: z.string().min(1).max(100),
|
||||
/** 1-indexed page number. */
|
||||
page: z.number().int().min(1),
|
||||
/** Percent of page width — top-left corner of the marker. */
|
||||
x: z.number().min(0).max(1),
|
||||
/** Percent of page height — top-left corner of the marker. */
|
||||
y: z.number().min(0).max(1),
|
||||
/** Percent width — defaults to 0.15 if omitted by an older editor. */
|
||||
w: z.number().min(0).max(1).optional(),
|
||||
/** Percent height — defaults to 0.04. */
|
||||
h: z.number().min(0).max(1).optional(),
|
||||
/** Optional explicit font size; otherwise auto-fit to the box. */
|
||||
fontSize: z.number().int().min(6).max(72).optional(),
|
||||
});
|
||||
|
||||
export const fieldMapSchema = z.array(fieldMapEntrySchema).max(500);
|
||||
|
||||
export type FieldMapEntry = z.infer<typeof fieldMapEntrySchema>;
|
||||
export type FieldMap = z.infer<typeof fieldMapSchema>;
|
||||
|
||||
/**
|
||||
* Cross-validate a parsed field-map against the source PDF page count.
|
||||
* The editor surfaces a warning when an upload truncates the page set
|
||||
* (e.g. uploading a 3-page PDF over a 5-page template). Returns null
|
||||
* when valid, or an error message describing the first out-of-range
|
||||
* marker.
|
||||
*/
|
||||
export function validateFieldMapAgainstPageCount(map: FieldMap, pageCount: number): string | null {
|
||||
for (const m of map) {
|
||||
if (m.page > pageCount) {
|
||||
return `Field "${m.token}" sits on page ${m.page} but the PDF only has ${pageCount} pages.`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user