397 lines
17 KiB
TypeScript
Executable File
397 lines
17 KiB
TypeScript
Executable File
// db/schema.ts
|
|
import {
|
|
bigint,
|
|
boolean,
|
|
doublePrecision,
|
|
integer,
|
|
json,
|
|
pgTable,
|
|
text,
|
|
varchar,
|
|
} from "drizzle-orm/pg-core";
|
|
|
|
export const users = pgTable("users", {
|
|
id: text("id").primaryKey(), // OAuth2 user ID
|
|
name: text("name"),
|
|
email: text("email"),
|
|
phone: text("phone"),
|
|
address_1: text("address_1"),
|
|
address_2: text("address_2"),
|
|
city: text("city"),
|
|
state: text("state"),
|
|
zip: text("zip"),
|
|
country: text("country"),
|
|
user_type: text("user_type"), // 'landlord', 'tenant', 'admin'
|
|
onboarded_landlord: boolean("onboarded_landlord").default(false),
|
|
onboarded_tenant: boolean("onboarded_tenant").default(false),
|
|
email_verified: boolean("email_verified").default(false),
|
|
banking_setup_complete: boolean("banking_setup_complete").default(false),
|
|
notification_preferences_json: json("notification_preferences_json"),
|
|
payment_preferences_json: json("payment_preferences_json"),
|
|
onboarding_step: integer("onboarding_step").default(0), // Track onboarding progress
|
|
encrypted_data: text("encrypted_data"), // Encrypted personal information
|
|
started_on: bigint("started_on", { mode: "number" }),
|
|
updated_on: bigint("updated_on", { mode: "number" }),
|
|
});
|
|
|
|
export const roles = pgTable("roles", {
|
|
id: text("id").primaryKey(), // e.g., 'admin', 'guest', 'client', 'owner'
|
|
description: text("description"),
|
|
});
|
|
|
|
export const userRoles = pgTable("user_roles", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id").references(() => users.id),
|
|
roleId: text("role_id").references(() => roles.id),
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
updated_on: bigint("updated_on", { mode: "number" }),
|
|
});
|
|
|
|
export const stateKeys = pgTable("state_keys", {
|
|
seal_key: varchar("seal_key", { length: 32 }).primaryKey(),
|
|
state_id: varchar("state_id", { length: 36 }).notNull(),
|
|
cookie_hash: varchar("cookie_hash", { length: 64 }).notNull(),
|
|
created_on: bigint("created_on", { mode: "number" }).notNull(),
|
|
});
|
|
|
|
export const userTokens = pgTable("user_tokens", {
|
|
user_id: text("user_id").primaryKey().references(() => users.id),
|
|
state_id: text("state_id").notNull(),
|
|
access_token: text("access_token").notNull(),
|
|
refresh_token: text("refresh_token").notNull(),
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
updated_on: bigint("updated_on", { mode: "number" }),
|
|
});
|
|
|
|
export const webTokens = pgTable("web_tokens", {
|
|
token_id: varchar("token_id", { length: 64 }).primaryKey(),
|
|
state_id: varchar("state_id", { length: 36 }).notNull(),
|
|
user_id: text("user_id").notNull().references(() => users.id),
|
|
token_type: text("token_type").notNull(),
|
|
token_value: text("token_value").notNull(),
|
|
expires_on: bigint("expires_on", { mode: "number" }).notNull(),
|
|
created_on: bigint("created_on", { mode: "number" }).notNull(),
|
|
updated_on: bigint("updated_on", { mode: "number" }).notNull(),
|
|
last_used_on: bigint("last_used_on", { mode: "number" }),
|
|
is_active: boolean("is_active").default(true),
|
|
metadata: json("metadata"),
|
|
});
|
|
|
|
// Obligations table
|
|
export const obligations = pgTable("obligations", {
|
|
id: text("id").primaryKey(),
|
|
user_id: text("user_id").references(() => users.id),
|
|
description: text("description"),
|
|
amount: doublePrecision("amount"),
|
|
frequency: text("frequency"), // e.g., 'monthly', 'yearly'
|
|
due_date: bigint("due_date", { mode: "number" }), // unix timestamp
|
|
encrypted_data: text("encrypted_data"), // Encrypted financial information
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
updated_on: bigint("updated_on", { mode: "number" }),
|
|
});
|
|
|
|
// Incomes table
|
|
export const incomes = pgTable("incomes", {
|
|
id: text("id").primaryKey(),
|
|
user_id: text("user_id").references(() => users.id),
|
|
source: text("source"),
|
|
amount: doublePrecision("amount"),
|
|
frequency: text("frequency"), // e.g., 'monthly', 'yearly'
|
|
received_date: bigint("received_date", { mode: "number" }), // unix timestamp
|
|
encrypted_data: text("encrypted_data"), // Encrypted financial information
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
updated_on: bigint("updated_on", { mode: "number" }),
|
|
});
|
|
|
|
// Financial balances table - stores balance-form data from FinancialCalendar
|
|
export const financial_balances = pgTable("financial_balances", {
|
|
id: text("id").primaryKey(),
|
|
user_id: text("user_id").references(() => users.id),
|
|
balance: doublePrecision("balance"), // balance amount
|
|
balance_date: bigint("balance_date", { mode: "number" }), // date when balance was set (YYYY-MM-DD format)
|
|
encrypted_data: text("encrypted_data"), // Encrypted financial information
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
updated_on: bigint("updated_on", { mode: "number" }),
|
|
});
|
|
|
|
export const properties = pgTable("properties", {
|
|
id: text("id").primaryKey(),
|
|
title: text("title"),
|
|
description: text("description"),
|
|
address_1: text("address_1"),
|
|
address_2: text("address_2"),
|
|
city: text("city"),
|
|
state: text("state"),
|
|
zip: text("zip"),
|
|
country: text("country"),
|
|
bedrooms: integer("bedrooms"),
|
|
bathrooms: integer("bathrooms"),
|
|
square_footage: integer("square_footage"),
|
|
lot_size: integer("lot_size"),
|
|
year_built: integer("year_built"),
|
|
price: doublePrecision("price"),
|
|
property_type: text("property_type"),
|
|
listing_status: text("listing_status"),
|
|
rental_status: text("rental_status"), // 'available', 'occupied', 'maintenance', 'draft'
|
|
monthly_rent: doublePrecision("monthly_rent"),
|
|
security_deposit: doublePrecision("security_deposit"),
|
|
default_payment_method_id: text("default_payment_method_id").references(() =>
|
|
payment_methods.id
|
|
),
|
|
lease_duration_months: integer("lease_duration_months"),
|
|
amenities: text("amenities"), // JSON string of amenities
|
|
property_photos_count: integer("property_photos_count").default(0),
|
|
image_url: text("image_url"),
|
|
encrypted_data: text("encrypted_data"), // Encrypted property information
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
updated_on: bigint("updated_on", { mode: "number" }),
|
|
owner_id: text("owner_id").references(() => users.id),
|
|
});
|
|
|
|
// Rent Schedules & Rules
|
|
export const rent_schedules = pgTable("rent_schedules", {
|
|
id: text("id").primaryKey(),
|
|
property_id: text("property_id").references(() => properties.id),
|
|
rent_amount: doublePrecision("rent_amount"),
|
|
due_day: integer("due_day"), // Day of month rent is due (1-31)
|
|
late_fee_amount: doublePrecision("late_fee_amount"),
|
|
grace_period_days: integer("grace_period_days").default(0),
|
|
payment_methods_allowed: text("payment_methods_allowed"), // JSON array of allowed methods
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
updated_on: bigint("updated_on", { mode: "number" }),
|
|
});
|
|
|
|
export const leases = pgTable("leases", {
|
|
id: text("id").primaryKey(),
|
|
title: text("title"),
|
|
property_id: text("property_id").references(() => properties.id),
|
|
property_address: text("property_address"),
|
|
tenant_id: text("tenant_id").references(() => users.id),
|
|
owner_id: text("owner_id").references(() => users.id),
|
|
start_date: bigint("start_date", { mode: "number" }),
|
|
end_date: bigint("end_date", { mode: "number" }),
|
|
rent: doublePrecision("rent"),
|
|
security_deposit: doublePrecision("security_deposit"),
|
|
default_payment_method_id: text("default_payment_method_id").references(() =>
|
|
payment_methods.id
|
|
),
|
|
lease_status: text("lease_status"),
|
|
lease_document_url: text("lease_document_url"),
|
|
payment_schedule_id: text("payment_schedule_id").references(() =>
|
|
rent_schedules.id
|
|
),
|
|
late_fee_rules: text("late_fee_rules"), // JSON string of late fee rules
|
|
auto_renewal: boolean("auto_renewal").default(false),
|
|
encrypted_data: text("encrypted_data"), // Encrypted lease information
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
updated_on: bigint("updated_on", { mode: "number" }),
|
|
});
|
|
|
|
// Payment Methods & Banking
|
|
export const payment_methods = pgTable("payment_methods", {
|
|
id: text("id").primaryKey(),
|
|
user_id: text("user_id").references(() => users.id),
|
|
method_type: text("method_type"), // 'bank_account', 'credit_card'
|
|
bank_name: text("bank_name"),
|
|
account_type: text("account_type"), // 'checking', 'savings'
|
|
routing_number_encrypted: text("routing_number_encrypted"),
|
|
account_number_encrypted: text("account_number_encrypted"),
|
|
is_default: boolean("is_default").default(false),
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
updated_on: bigint("updated_on", { mode: "number" }),
|
|
});
|
|
|
|
// Property Photos & Documents
|
|
export const property_media = pgTable("property_media", {
|
|
id: text("id").primaryKey(),
|
|
property_id: text("property_id").references(() => properties.id),
|
|
media_type: text("media_type"), // 'photo', 'video', 'document'
|
|
file_url: text("file_url"),
|
|
file_name: text("file_name"),
|
|
display_order: integer("display_order").default(0),
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
});
|
|
|
|
// Lease Documents & Templates
|
|
export const lease_documents = pgTable("lease_documents", {
|
|
id: text("id").primaryKey(),
|
|
lease_id: text("lease_id").references(() => leases.id),
|
|
document_type: text("document_type"), // 'lease', 'addendum', 'receipt'
|
|
file_url: text("file_url"),
|
|
file_name: text("file_name"),
|
|
signed_by_landlord: boolean("signed_by_landlord").default(false),
|
|
signed_by_tenant: boolean("signed_by_tenant").default(false),
|
|
signature_date: bigint("signature_date", { mode: "number" }),
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
});
|
|
|
|
// Tenant Applications
|
|
export const tenant_applications = pgTable("tenant_applications", {
|
|
id: text("id").primaryKey(),
|
|
property_id: text("property_id").references(() => properties.id),
|
|
applicant_id: text("applicant_id").references(() => users.id),
|
|
application_status: text("application_status"), // 'pending', 'approved', 'rejected'
|
|
application_data_encrypted: text("application_data_encrypted"), // Encrypted application details
|
|
document_urls_json: json("document_urls_json"), // JSON array of document URLs
|
|
submitted_on: bigint("submitted_on", { mode: "number" }),
|
|
reviewed_on: bigint("reviewed_on", { mode: "number" }),
|
|
});
|
|
|
|
export const tenant_invites = pgTable("tenant_invites", {
|
|
id: text("id").primaryKey(),
|
|
landlord_id: text("landlord_id").references(() => users.id),
|
|
property_id: text("property_id").references(() => properties.id),
|
|
available_on: bigint("available_on", { mode: "number" }),
|
|
invited_emails: text("invited_emails"), // JSON array of invited emails
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
updated_on: bigint("updated_on", { mode: "number" }),
|
|
});
|
|
|
|
// Payment Transactions
|
|
export const payment_transactions = pgTable("payment_transactions", {
|
|
id: text("id").primaryKey(),
|
|
lease_id: text("lease_id").references(() => leases.id),
|
|
amount: doublePrecision("amount"),
|
|
payment_date: bigint("payment_date", { mode: "number" }),
|
|
payment_method: text("payment_method"), // 'card', 'ach', 'check', 'cash'
|
|
transaction_status: text("transaction_status"), // 'pending', 'completed', 'failed'
|
|
stripe_payment_id: text("stripe_payment_id"), // External payment processor ID
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
});
|
|
|
|
// Reminders
|
|
export const reminders = pgTable("reminders", {
|
|
id: text("id").primaryKey(),
|
|
user_id: text("user_id").references(() => users.id).notNull(),
|
|
lease_id: text("lease_id").references(() => leases.id),
|
|
reminder_type: text("reminder_type").notNull(), // 'rent_payment', 'maintenance_followup', 'custom', 'document_expiration'
|
|
title: text("title").notNull(),
|
|
description: text("description"),
|
|
due_date: bigint("due_date", { mode: "number" }).notNull(),
|
|
is_completed: boolean("is_completed").default(false),
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
updated_on: bigint("updated_on", { mode: "number" }),
|
|
});
|
|
|
|
// Communication/Messages
|
|
export const messages = pgTable("messages", {
|
|
id: text("id").primaryKey(),
|
|
lease_id: text("lease_id").references(() => leases.id),
|
|
sender_id: text("sender_id").references(() => users.id),
|
|
recipient_id: text("recipient_id").references(() => users.id),
|
|
message_type: text("message_type"), // 'general', 'maintenance', 'payment', 'notice'
|
|
subject: text("subject"),
|
|
parent_message_id: text("parent_message_id"), // For threading
|
|
attachments_json: json("attachments_json"), // JSON array of file URLs/metadata
|
|
content: text("content"), // Encrypted message content
|
|
content_encrypted: text("content_encrypted"), // Encrypted message content
|
|
is_read: boolean("is_read").default(false),
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
});
|
|
|
|
// Maintenance Requests
|
|
export const maintenance_requests = pgTable("maintenance_requests", {
|
|
id: text("id").primaryKey(),
|
|
property_id: text("property_id").references(() => properties.id),
|
|
tenant_id: text("tenant_id").references(() => users.id),
|
|
request_type: text("request_type"), // 'plumbing', 'electrical', 'hvac', 'general'
|
|
description: text("description"), // Encrypted request details
|
|
status: text("status"), // 'open', 'in_progress', 'completed', 'cancelled'
|
|
priority: text("priority"), // 'low', 'medium', 'high', 'urgent'
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
resolved_on: bigint("resolved_on", { mode: "number" }),
|
|
});
|
|
|
|
// User Pins
|
|
export const user_pins = pgTable("user_pins", {
|
|
user_id: text("user_id").primaryKey().references(() => users.id),
|
|
pin: text("pin").notNull(), // Encrypted pin/password
|
|
created_on: bigint("created_on", { mode: "number" }).notNull(),
|
|
modified_on: bigint("modified_on", { mode: "number" }).notNull(),
|
|
});
|
|
|
|
// Session Config table
|
|
export const session_configs = pgTable("session_configs", {
|
|
device_id: text("device_id").primaryKey(),
|
|
user_id: text("user_id").references(() => users.id),
|
|
other_data: text("other_data"),
|
|
active_date: bigint("active_date", { mode: "number" }),
|
|
quiz_configs: json("quiz_configs"), // Array<Array<string>>
|
|
active_route: text("active_route"),
|
|
page_data: text("page_data"),
|
|
active_quiz_index: integer("active_quiz_index"),
|
|
active_question_index: integer("active_question_index"),
|
|
});
|
|
|
|
// Quiz Config table
|
|
export const quiz_configs = pgTable("quiz_configs", {
|
|
quiz_config_id: text("quiz_config_id").primaryKey(),
|
|
quiz_name: text("quiz_name"),
|
|
owner_id: text("owner_id").references(() => users.id),
|
|
quiz_mode: text("quiz_mode"),
|
|
active_date: bigint("active_date", { mode: "number" }),
|
|
tab_name: text("tab_name"),
|
|
tab_index: integer("tab_index").default(0),
|
|
list_index: integer("list_index").default(0),
|
|
questions: json("questions"), // Array of question objects
|
|
results: json("results"), // Array of result objects
|
|
});
|
|
|
|
// Quiz Questions table (normalized for better querying)
|
|
export const quiz_questions = pgTable("quiz_questions", {
|
|
id: text("id").primaryKey(),
|
|
quiz_id: text("quiz_id").references(() => quiz_configs.quiz_config_id),
|
|
owner_id: text("owner_id").references(() => users.id),
|
|
question_index: integer("question_index").default(0),
|
|
language_1: text("language_1"),
|
|
language_2: text("language_2"),
|
|
text_1: text("text_1"),
|
|
audio_file_1: text("audio_file_1"),
|
|
text_2: text("text_2"),
|
|
audio_file_2: text("audio_file_2"),
|
|
order_num: integer("order_num"),
|
|
background_file: text("background_file"),
|
|
background_image_data: text("background_image_data"),
|
|
video_file: text("video_file"),
|
|
video_file_data: text("video_file_data"),
|
|
});
|
|
|
|
// Quiz Results table (normalized for better querying)
|
|
export const quiz_results = pgTable("quiz_results", {
|
|
id: text("id").primaryKey(),
|
|
quiz_config_id: text("quiz_config_id").references(() =>
|
|
quiz_configs.quiz_config_id
|
|
),
|
|
user_id: text("user_id").references(() => users.id),
|
|
success: boolean("success"),
|
|
total_time: bigint("total_time", { mode: "number" }),
|
|
help_count: integer("help_count"),
|
|
completed_on: bigint("completed_on", { mode: "number" }),
|
|
});
|
|
|
|
// Brain Question Progress table (for spaced repetition learning with mode progression)
|
|
export const brain_question_progress = pgTable("brain_question_progress", {
|
|
id: text("id").primaryKey(), // composite: `${user_id}_${question_id}` (mode-agnostic)
|
|
user_id: text("user_id").references(() => users.id).notNull(),
|
|
quiz_id: text("quiz_id").references(() => quiz_configs.quiz_config_id),
|
|
question_id: text("question_id").notNull(),
|
|
current_mode: text("current_mode").notNull().default("photo-see-bisaya"), // Current mode in progression
|
|
mode_index: integer("mode_index").default(0), // Index in QUIZ_MODES array (0-5)
|
|
attempts: integer("attempts").default(0),
|
|
first_try_successes: integer("first_try_successes").default(0),
|
|
consecutive_first_try: integer("consecutive_first_try").default(0), // Streak of first-try correct
|
|
last_result: boolean("last_result"), // true = correct on first try
|
|
next_due_at: bigint("next_due_at", { mode: "number" }), // timestamp for spaced repetition
|
|
interval_days: integer("interval_days").default(1), // current spacing interval
|
|
ease_factor: doublePrecision("ease_factor").default(2.5), // SM-2 algorithm factor
|
|
is_review_only: boolean("is_review_only").default(false), // Card has mastered all modes
|
|
created_on: bigint("created_on", { mode: "number" }),
|
|
updated_on: bigint("updated_on", { mode: "number" }),
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|