Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
309 lines
13 KiB
SQL
309 lines
13 KiB
SQL
CREATE TABLE IF NOT EXISTS "audit_log" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"user_id" uuid,
|
|
"firm_id" uuid,
|
|
"action" text NOT NULL,
|
|
"meta" text,
|
|
"ip" "inet",
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "email_verifications" (
|
|
"token_hash" text PRIMARY KEY NOT NULL,
|
|
"user_id" uuid NOT NULL,
|
|
"expires_at" timestamp with time zone NOT NULL,
|
|
"consumed_at" timestamp with time zone,
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "login_attempts" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"email" text NOT NULL,
|
|
"ip" "inet",
|
|
"success" boolean DEFAULT false NOT NULL,
|
|
"attempted_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "password_resets" (
|
|
"token_hash" text PRIMARY KEY NOT NULL,
|
|
"user_id" uuid NOT NULL,
|
|
"expires_at" timestamp with time zone NOT NULL,
|
|
"consumed_at" timestamp with time zone,
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "sessions" (
|
|
"id" text PRIMARY KEY NOT NULL,
|
|
"user_id" uuid NOT NULL,
|
|
"expires_at" timestamp with time zone NOT NULL,
|
|
"ip" "inet",
|
|
"user_agent" text,
|
|
"last_seen_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "users" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"firm_id" uuid,
|
|
"email" text NOT NULL,
|
|
"password_hash" text NOT NULL,
|
|
"full_name" text,
|
|
"role" text DEFAULT 'owner' NOT NULL,
|
|
"email_verified_at" timestamp with time zone,
|
|
"totp_secret_enc" text,
|
|
"totp_enabled" boolean DEFAULT false NOT NULL,
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
CONSTRAINT "users_email_unique" UNIQUE("email")
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "firms" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"name" text NOT NULL,
|
|
"plan" text DEFAULT 'starter' NOT NULL,
|
|
"watermark_enabled" boolean DEFAULT true NOT NULL,
|
|
"storage_bytes_used" integer DEFAULT 0 NOT NULL,
|
|
"stripe_customer_id" text,
|
|
"stripe_subscription_id" text,
|
|
"trial_ends_at" timestamp with time zone,
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "clients" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"firm_id" uuid NOT NULL,
|
|
"name" text NOT NULL,
|
|
"email" text,
|
|
"phone" text,
|
|
"address" text,
|
|
"notes" text,
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "cases" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"firm_id" uuid NOT NULL,
|
|
"client_id" uuid NOT NULL,
|
|
"title" text NOT NULL,
|
|
"case_number" text,
|
|
"status" text DEFAULT 'open' NOT NULL,
|
|
"practice_area" text,
|
|
"description" text,
|
|
"hourly_rate" numeric(10, 2),
|
|
"opened_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
"closed_at" timestamp with time zone,
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "time_entries" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"firm_id" uuid NOT NULL,
|
|
"case_id" uuid NOT NULL,
|
|
"user_id" uuid NOT NULL,
|
|
"description" text NOT NULL,
|
|
"started_at" timestamp with time zone NOT NULL,
|
|
"ended_at" timestamp with time zone,
|
|
"minutes" integer DEFAULT 0 NOT NULL,
|
|
"rate" numeric(10, 2) NOT NULL,
|
|
"billable" boolean DEFAULT true NOT NULL,
|
|
"invoice_item_id" uuid,
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "documents" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"firm_id" uuid NOT NULL,
|
|
"case_id" uuid,
|
|
"uploaded_by" uuid,
|
|
"name" text NOT NULL,
|
|
"storage_key" text NOT NULL,
|
|
"mime_type" text NOT NULL,
|
|
"size_bytes" integer NOT NULL,
|
|
"version" integer DEFAULT 1 NOT NULL,
|
|
"parent_id" uuid,
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "invoice_items" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"invoice_id" uuid NOT NULL,
|
|
"description" text NOT NULL,
|
|
"quantity" numeric(10, 2) DEFAULT '1' NOT NULL,
|
|
"rate" numeric(10, 2) NOT NULL,
|
|
"amount" numeric(12, 2) NOT NULL,
|
|
"sort_order" integer DEFAULT 0 NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "invoices" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"firm_id" uuid NOT NULL,
|
|
"client_id" uuid NOT NULL,
|
|
"case_id" uuid,
|
|
"number" text NOT NULL,
|
|
"status" text DEFAULT 'draft' NOT NULL,
|
|
"subtotal" numeric(12, 2) DEFAULT '0' NOT NULL,
|
|
"tax_rate" numeric(5, 2) DEFAULT '0' NOT NULL,
|
|
"total" numeric(12, 2) DEFAULT '0' NOT NULL,
|
|
"notes" text,
|
|
"issued_at" timestamp with time zone,
|
|
"due_at" timestamp with time zone,
|
|
"paid_at" timestamp with time zone,
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "contact_messages" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"full_name" text NOT NULL,
|
|
"email" text NOT NULL,
|
|
"message" text NOT NULL,
|
|
"ip" "inet",
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
CREATE TABLE IF NOT EXISTS "tool_usage" (
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"tool" text NOT NULL,
|
|
"session_id" text,
|
|
"ip" "inet",
|
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
);
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_firm_id_firms_id_fk" FOREIGN KEY ("firm_id") REFERENCES "public"."firms"("id") ON DELETE set null ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "email_verifications" ADD CONSTRAINT "email_verifications_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "password_resets" ADD CONSTRAINT "password_resets_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "users" ADD CONSTRAINT "users_firm_id_firms_id_fk" FOREIGN KEY ("firm_id") REFERENCES "public"."firms"("id") ON DELETE set null ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "clients" ADD CONSTRAINT "clients_firm_id_firms_id_fk" FOREIGN KEY ("firm_id") REFERENCES "public"."firms"("id") ON DELETE cascade ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "cases" ADD CONSTRAINT "cases_firm_id_firms_id_fk" FOREIGN KEY ("firm_id") REFERENCES "public"."firms"("id") ON DELETE cascade ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "cases" ADD CONSTRAINT "cases_client_id_clients_id_fk" FOREIGN KEY ("client_id") REFERENCES "public"."clients"("id") ON DELETE restrict ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "time_entries" ADD CONSTRAINT "time_entries_firm_id_firms_id_fk" FOREIGN KEY ("firm_id") REFERENCES "public"."firms"("id") ON DELETE cascade ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "time_entries" ADD CONSTRAINT "time_entries_case_id_cases_id_fk" FOREIGN KEY ("case_id") REFERENCES "public"."cases"("id") ON DELETE cascade ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "time_entries" ADD CONSTRAINT "time_entries_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE restrict ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "documents" ADD CONSTRAINT "documents_firm_id_firms_id_fk" FOREIGN KEY ("firm_id") REFERENCES "public"."firms"("id") ON DELETE cascade ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "documents" ADD CONSTRAINT "documents_case_id_cases_id_fk" FOREIGN KEY ("case_id") REFERENCES "public"."cases"("id") ON DELETE cascade ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "documents" ADD CONSTRAINT "documents_uploaded_by_users_id_fk" FOREIGN KEY ("uploaded_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "invoice_items" ADD CONSTRAINT "invoice_items_invoice_id_invoices_id_fk" FOREIGN KEY ("invoice_id") REFERENCES "public"."invoices"("id") ON DELETE cascade ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "invoices" ADD CONSTRAINT "invoices_firm_id_firms_id_fk" FOREIGN KEY ("firm_id") REFERENCES "public"."firms"("id") ON DELETE cascade ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "invoices" ADD CONSTRAINT "invoices_client_id_clients_id_fk" FOREIGN KEY ("client_id") REFERENCES "public"."clients"("id") ON DELETE restrict ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
DO $$ BEGIN
|
|
ALTER TABLE "invoices" ADD CONSTRAINT "invoices_case_id_cases_id_fk" FOREIGN KEY ("case_id") REFERENCES "public"."cases"("id") ON DELETE set null ON UPDATE no action;
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "audit_firm_idx" ON "audit_log" USING btree ("firm_id","created_at");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "login_attempts_email_idx" ON "login_attempts" USING btree ("email","attempted_at");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "login_attempts_ip_idx" ON "login_attempts" USING btree ("ip","attempted_at");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "sessions_user_idx" ON "sessions" USING btree ("user_id");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "sessions_expires_idx" ON "sessions" USING btree ("expires_at");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "users_firm_idx" ON "users" USING btree ("firm_id");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "clients_firm_idx" ON "clients" USING btree ("firm_id");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "cases_firm_idx" ON "cases" USING btree ("firm_id");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "cases_client_idx" ON "cases" USING btree ("client_id");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "cases_status_idx" ON "cases" USING btree ("firm_id","status");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "time_firm_idx" ON "time_entries" USING btree ("firm_id");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "time_case_idx" ON "time_entries" USING btree ("case_id");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "time_user_idx" ON "time_entries" USING btree ("user_id","started_at");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "time_unbilled_idx" ON "time_entries" USING btree ("firm_id","invoice_item_id");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "docs_firm_idx" ON "documents" USING btree ("firm_id");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "docs_case_idx" ON "documents" USING btree ("case_id");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "invoice_items_invoice_idx" ON "invoice_items" USING btree ("invoice_id");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "invoices_firm_idx" ON "invoices" USING btree ("firm_id");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "invoices_status_idx" ON "invoices" USING btree ("firm_id","status");--> statement-breakpoint
|
|
CREATE INDEX IF NOT EXISTS "tool_usage_tool_idx" ON "tool_usage" USING btree ("tool","created_at"); |