Files
network-app-api/src/db/schema.ts
Hammer 229b9407c2
All checks were successful
CI/CD / check (push) Successful in 1m1s
CI/CD / deploy (push) Successful in 1s
feat: add client documents, goals, and referral tracking
- New client_documents table with file upload/download/delete API
- New client_goals table with full CRUD and dashboard overview
- New referrals table with stats (top referrers, conversion rate)
- All routes follow existing auth middleware pattern
2026-01-30 04:41:23 +00:00

446 lines
17 KiB
TypeScript

import { pgTable, text, timestamp, uuid, boolean, jsonb, integer, numeric } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
// Users table (managed by BetterAuth - uses text IDs)
export const users = pgTable('users', {
id: text('id').primaryKey(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
emailVerified: boolean('email_verified').default(false),
image: text('image'),
role: text('role').default('user'), // 'admin' | 'user'
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Invites table
export const invites = pgTable('invites', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull(),
name: text('name').notNull(),
role: text('role').default('user').notNull(),
token: text('token').notNull().unique(),
invitedBy: text('invited_by').references(() => users.id, { onDelete: 'set null' }),
status: text('status').default('pending').notNull(), // 'pending' | 'accepted' | 'expired'
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// Password reset tokens
export const passwordResetTokens = pgTable('password_reset_tokens', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
token: text('token').notNull().unique(),
expiresAt: timestamp('expires_at').notNull(),
usedAt: timestamp('used_at'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// User profile (additional settings beyond BetterAuth)
export const userProfiles = pgTable('user_profiles', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull().unique(),
title: text('title'), // e.g., "Senior Wealth Advisor"
company: text('company'), // e.g., "ABC Financial Group"
phone: text('phone'),
emailSignature: text('email_signature'), // Custom signature block
communicationStyle: jsonb('communication_style').$type<{
tone?: 'formal' | 'friendly' | 'casual';
greeting?: string;
signoff?: string;
writingSamples?: string[];
avoidWords?: string[];
}>(),
onboardingComplete: boolean('onboarding_complete').default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// BetterAuth session table
export const sessions = pgTable('sessions', {
id: text('id').primaryKey(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
token: text('token').notNull().unique(),
expiresAt: timestamp('expires_at').notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// BetterAuth account table (for OAuth providers)
export const accounts = pgTable('accounts', {
id: text('id').primaryKey(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at'),
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
scope: text('scope'),
idToken: text('id_token'),
password: text('password'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// BetterAuth verification table
export const verifications = pgTable('verifications', {
id: text('id').primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Clients table
export const clients = pgTable('clients', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
// Basic info
firstName: text('first_name').notNull(),
lastName: text('last_name').notNull(),
email: text('email'),
phone: text('phone'),
// Address
street: text('street'),
city: text('city'),
state: text('state'),
zip: text('zip'),
// Professional
company: text('company'),
role: text('role'),
industry: text('industry'),
// Personal
birthday: timestamp('birthday'),
anniversary: timestamp('anniversary'),
interests: jsonb('interests').$type<string[]>().default([]),
family: jsonb('family').$type<{ spouse?: string; children?: string[] }>(),
notes: text('notes'),
// Organization
tags: jsonb('tags').$type<string[]>().default([]),
stage: text('stage').default('lead'), // 'lead' | 'prospect' | 'onboarding' | 'active' | 'inactive'
// Tracking
lastContactedAt: timestamp('last_contacted_at'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Events table (birthdays, anniversaries, follow-ups)
export const events = pgTable('events', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
type: text('type').notNull(), // 'birthday' | 'anniversary' | 'followup' | 'custom'
title: text('title').notNull(),
date: timestamp('date').notNull(),
recurring: boolean('recurring').default(false),
reminderDays: integer('reminder_days').default(7),
lastTriggered: timestamp('last_triggered'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// Communications table (emails, messages)
export const communications = pgTable('communications', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
type: text('type').notNull(), // 'email' | 'birthday' | 'followup'
subject: text('subject'),
content: text('content').notNull(),
aiGenerated: boolean('ai_generated').default(false),
aiModel: text('ai_model'), // Which model was used
status: text('status').default('draft'), // 'draft' | 'approved' | 'sent'
sentAt: timestamp('sent_at'),
batchId: text('batch_id'), // for grouping bulk sends
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// Notifications table
export const notifications = pgTable('notifications', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
type: text('type').notNull(), // 'event_reminder' | 'interaction' | 'system'
title: text('title').notNull(),
message: text('message').notNull(),
read: boolean('read').default(false),
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'set null' }),
eventId: uuid('event_id').references(() => events.id, { onDelete: 'set null' }),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// Interactions table (touchpoint logging)
export const interactions = pgTable('interactions', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
type: text('type').notNull(), // 'call' | 'meeting' | 'email' | 'note' | 'other'
title: text('title').notNull(),
description: text('description'),
duration: integer('duration'), // in minutes
contactedAt: timestamp('contacted_at').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// Client notes table
export const clientNotes = pgTable('client_notes', {
id: uuid('id').primaryKey().defaultRandom(),
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
content: text('content').notNull(),
pinned: boolean('pinned').default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Email templates table
export const emailTemplates = pgTable('email_templates', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
name: text('name').notNull(),
category: text('category').notNull(), // 'follow-up' | 'birthday' | 'introduction' | 'check-in' | 'thank-you' | 'custom'
subject: text('subject').notNull(),
content: text('content').notNull(), // supports {{firstName}}, {{lastName}}, {{company}} placeholders
isDefault: boolean('is_default').default(false), // mark as default for category
usageCount: integer('usage_count').default(0),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Saved client filters / segments
export const clientSegments = pgTable('client_segments', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
name: text('name').notNull(),
description: text('description'),
filters: jsonb('filters').$type<{
stages?: string[];
tags?: string[];
industries?: string[];
cities?: string[];
states?: string[];
lastContactedBefore?: string; // ISO date
lastContactedAfter?: string;
createdBefore?: string;
createdAfter?: string;
hasEmail?: boolean;
hasPhone?: boolean;
search?: string;
}>().notNull(),
color: text('color').default('#3b82f6'), // badge color
pinned: boolean('pinned').default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Audit logs table (compliance)
export const auditLogs = pgTable('audit_logs', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id').references(() => users.id, { onDelete: 'set null' }),
action: text('action').notNull(), // 'create' | 'update' | 'delete' | 'view' | 'send' | 'login' | 'logout' | 'password_change'
entityType: text('entity_type').notNull(), // 'client' | 'email' | 'event' | 'template' | 'segment' | 'user' | 'auth' | etc
entityId: text('entity_id'), // ID of the affected entity
details: jsonb('details').$type<Record<string, unknown>>(), // what changed
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// Client Documents table (file attachments)
export const clientDocuments = pgTable('client_documents', {
id: uuid('id').primaryKey().defaultRandom(),
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
name: text('name').notNull(), // user-facing display name
filename: text('filename').notNull(), // actual filename on disk
mimeType: text('mime_type').notNull(),
size: integer('size').notNull(), // bytes
category: text('category').default('other').notNull(), // 'contract' | 'agreement' | 'id' | 'statement' | 'correspondence' | 'other'
path: text('path').notNull(), // filesystem path
notes: text('notes'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// Client Goals / Financial Objectives table
export const clientGoals = pgTable('client_goals', {
id: uuid('id').primaryKey().defaultRandom(),
clientId: uuid('client_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
title: text('title').notNull(),
description: text('description'),
category: text('category').default('other').notNull(), // 'retirement' | 'investment' | 'savings' | 'insurance' | 'estate' | 'education' | 'debt' | 'other'
targetAmount: numeric('target_amount', { precision: 15, scale: 2 }),
currentAmount: numeric('current_amount', { precision: 15, scale: 2 }).default('0'),
targetDate: timestamp('target_date'),
status: text('status').default('on-track').notNull(), // 'on-track' | 'at-risk' | 'behind' | 'completed'
priority: text('priority').default('medium').notNull(), // 'high' | 'medium' | 'low'
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Referrals table
export const referrals = pgTable('referrals', {
id: uuid('id').primaryKey().defaultRandom(),
referrerId: uuid('referrer_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
referredId: uuid('referred_id').references(() => clients.id, { onDelete: 'cascade' }).notNull(),
userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
type: text('type').default('client').notNull(), // 'client' | 'partner' | 'event'
notes: text('notes'),
status: text('status').default('pending').notNull(), // 'pending' | 'contacted' | 'converted' | 'lost'
value: numeric('value', { precision: 15, scale: 2 }), // estimated deal value
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Relations
export const usersRelations = relations(users, ({ many }) => ({
clients: many(clients),
events: many(events),
communications: many(communications),
notifications: many(notifications),
interactions: many(interactions),
sessions: many(sessions),
accounts: many(accounts),
emailTemplates: many(emailTemplates),
clientSegments: many(clientSegments),
}));
export const emailTemplatesRelations = relations(emailTemplates, ({ one }) => ({
user: one(users, {
fields: [emailTemplates.userId],
references: [users.id],
}),
}));
export const clientSegmentsRelations = relations(clientSegments, ({ one }) => ({
user: one(users, {
fields: [clientSegments.userId],
references: [users.id],
}),
}));
export const clientsRelations = relations(clients, ({ one, many }) => ({
user: one(users, {
fields: [clients.userId],
references: [users.id],
}),
events: many(events),
communications: many(communications),
notes: many(clientNotes),
interactions: many(interactions),
documents: many(clientDocuments),
goals: many(clientGoals),
referralsMade: many(referrals, { relationName: 'referrer' }),
referralsReceived: many(referrals, { relationName: 'referred' }),
}));
export const clientDocumentsRelations = relations(clientDocuments, ({ one }) => ({
client: one(clients, {
fields: [clientDocuments.clientId],
references: [clients.id],
}),
user: one(users, {
fields: [clientDocuments.userId],
references: [users.id],
}),
}));
export const clientGoalsRelations = relations(clientGoals, ({ one }) => ({
client: one(clients, {
fields: [clientGoals.clientId],
references: [clients.id],
}),
user: one(users, {
fields: [clientGoals.userId],
references: [users.id],
}),
}));
export const referralsRelations = relations(referrals, ({ one }) => ({
referrer: one(clients, {
fields: [referrals.referrerId],
references: [clients.id],
relationName: 'referrer',
}),
referred: one(clients, {
fields: [referrals.referredId],
references: [clients.id],
relationName: 'referred',
}),
user: one(users, {
fields: [referrals.userId],
references: [users.id],
}),
}));
export const notificationsRelations = relations(notifications, ({ one }) => ({
user: one(users, {
fields: [notifications.userId],
references: [users.id],
}),
client: one(clients, {
fields: [notifications.clientId],
references: [clients.id],
}),
event: one(events, {
fields: [notifications.eventId],
references: [events.id],
}),
}));
export const interactionsRelations = relations(interactions, ({ one }) => ({
user: one(users, {
fields: [interactions.userId],
references: [users.id],
}),
client: one(clients, {
fields: [interactions.clientId],
references: [clients.id],
}),
}));
export const clientNotesRelations = relations(clientNotes, ({ one }) => ({
client: one(clients, {
fields: [clientNotes.clientId],
references: [clients.id],
}),
user: one(users, {
fields: [clientNotes.userId],
references: [users.id],
}),
}));
export const eventsRelations = relations(events, ({ one }) => ({
user: one(users, {
fields: [events.userId],
references: [users.id],
}),
client: one(clients, {
fields: [events.clientId],
references: [clients.id],
}),
}));
export const communicationsRelations = relations(communications, ({ one }) => ({
user: one(users, {
fields: [communications.userId],
references: [users.id],
}),
client: one(clients, {
fields: [communications.clientId],
references: [clients.id],
}),
}));