import { jsonb, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core"; import { relations } from "drizzle-orm"; export type SignatureFieldType = | 'client-signature' | 'initials' | 'text' | 'checkbox' | 'date' | 'agent-signature'; export interface SignatureFieldData { id: string; page: number; // 1-indexed x: number; // PDF user space, bottom-left origin, points y: number; // PDF user space, bottom-left origin, points width: number; // PDF points (default: 144 — 2 inches) height: number; // PDF points (default: 36 — 0.5 inches) type?: SignatureFieldType; // Optional — v1.0 documents have no type; fallback = 'client-signature' } /** * Safe field type reader — always returns a SignatureFieldType, never undefined. * v1.0 documents have no `type` on their JSONB fields; this coalesces to 'client-signature'. * ALWAYS use this instead of reading field.type directly. */ export function getFieldType(field: SignatureFieldData): SignatureFieldType { return field.type ?? 'client-signature'; } /** * Returns true for field types that should be visible in the client signing session. * agent-signature fields are embedded during document preparation and must never * surface to the client as required unsigned fields. */ export function isClientVisibleField(field: SignatureFieldData): boolean { return getFieldType(field) !== 'agent-signature'; } export const users = pgTable("users", { id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), email: text("email").notNull().unique(), passwordHash: text("password_hash").notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), }); export const documentStatusEnum = pgEnum("document_status", [ "Draft", "Sent", "Viewed", "Signed", ]); export const clients = pgTable("clients", { id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), name: text("name").notNull(), email: text("email").notNull(), propertyAddress: text("property_address"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); export const formTemplates = pgTable("form_templates", { id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), name: text("name").notNull(), filename: text("filename").notNull().unique(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); export const documents = pgTable("documents", { id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()), name: text("name").notNull(), clientId: text("client_id") .notNull() .references(() => clients.id, { onDelete: "cascade" }), status: documentStatusEnum("status").notNull().default("Draft"), sentAt: timestamp("sent_at"), createdAt: timestamp("created_at").defaultNow().notNull(), formTemplateId: text("form_template_id").references(() => formTemplates.id), filePath: text("file_path"), signatureFields: jsonb("signature_fields").$type(), textFillData: jsonb("text_fill_data").$type>(), assignedClientId: text("assigned_client_id"), preparedFilePath: text("prepared_file_path"), /** Email addresses collected at prepare time: assigned client email + any CC addresses. */ emailAddresses: jsonb("email_addresses").$type(), signedFilePath: text("signed_file_path"), pdfHash: text("pdf_hash"), signedAt: timestamp("signed_at"), }); export const documentsRelations = relations(documents, ({ one }) => ({ client: one(clients, { fields: [documents.clientId], references: [clients.id] }), })); export const clientsRelations = relations(clients, ({ many }) => ({ documents: many(documents), })); export const auditEventTypeEnum = pgEnum('audit_event_type', [ 'document_prepared', 'email_sent', 'link_opened', 'document_viewed', 'signature_submitted', 'pdf_hash_computed', ]); export const signingTokens = pgTable('signing_tokens', { jti: text('jti').primaryKey(), documentId: text('document_id').notNull() .references(() => documents.id, { onDelete: 'cascade' }), createdAt: timestamp('created_at').defaultNow().notNull(), expiresAt: timestamp('expires_at').notNull(), usedAt: timestamp('used_at'), }); export const auditEvents = pgTable('audit_events', { id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()), documentId: text('document_id').notNull() .references(() => documents.id, { onDelete: 'cascade' }), eventType: auditEventTypeEnum('event_type').notNull(), ipAddress: text('ip_address'), userAgent: text('user_agent'), metadata: jsonb('metadata').$type>(), createdAt: timestamp('created_at').defaultNow().notNull(), });