Files
red/teressa-copeland-homes/src/lib/db/schema.ts
Chandler Copeland baa1c785a5 feat(09-01): add property_address column to clients — schema, migration, server actions
- Add propertyAddress: text("property_address") nullable column to clients pgTable
- Generate migration drizzle/0007_equal_nekra.sql: ALTER TABLE "clients" ADD COLUMN "property_address" text
- Apply migration successfully to local postgres database
- Extend clientSchema Zod schema with propertyAddress: z.string().optional()
- createClient: persist propertyAddress || null to coerce empty string to NULL
- updateClient: persist propertyAddress || null alongside name, email, updatedAt
2026-03-21 12:13:55 -06:00

129 lines
4.7 KiB
TypeScript

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<SignatureFieldData[]>(),
textFillData: jsonb("text_fill_data").$type<Record<string, string>>(),
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<string[]>(),
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<Record<string, unknown>>(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});