import { sql } from "drizzle-orm" import { relations } from "drizzle-orm" import { pgTable, uuid, text, integer, numeric, boolean, timestamp, date, jsonb, doublePrecision, } from "drizzle-orm/pg-core" // ============================================================ // Helpers // ============================================================ // timestamptz returned as ISO strings (the shape the app code relies on). // `date` columns returned as "YYYY-MM-DD". const tstz = (name: string) => timestamp(name, { withTimezone: true, mode: "string" }) const createdAt = () => tstz("created_at").notNull().defaultNow() const updatedAt = () => tstz("updated_at") .notNull() .defaultNow() .$onUpdate(() => new Date().toISOString()) // ============================================================ // BETTER AUTH TABLES (managed by Better Auth — field names must // stay camelCase to match the Better Auth schema model) // ============================================================ export const user = pgTable("user", { id: text("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), emailVerified: boolean("email_verified").notNull().default(false), image: text("image"), // Better Auth `admin` plugin fields (names must match the plugin model). role: text("role").default("user"), banned: boolean("banned").default(false), banReason: text("ban_reason"), banExpires: timestamp("ban_expires"), createdAt: timestamp("created_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(), }) export const session = pgTable("session", { id: text("id").primaryKey(), expiresAt: timestamp("expires_at").notNull(), token: text("token").notNull().unique(), createdAt: timestamp("created_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(), ipAddress: text("ip_address"), userAgent: text("user_agent"), // Better Auth `admin` plugin: set when an admin impersonates this user. impersonatedBy: text("impersonated_by"), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), }) export const account = pgTable("account", { id: text("id").primaryKey(), accountId: text("account_id").notNull(), providerId: text("provider_id").notNull(), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), accessToken: text("access_token"), refreshToken: text("refresh_token"), idToken: text("id_token"), accessTokenExpiresAt: timestamp("access_token_expires_at"), refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), scope: text("scope"), password: text("password"), createdAt: timestamp("created_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(), }) export const verification = pgTable("verification", { id: text("id").primaryKey(), identifier: text("identifier").notNull(), value: text("value").notNull(), expiresAt: timestamp("expires_at").notNull(), createdAt: timestamp("created_at").notNull().defaultNow(), updatedAt: timestamp("updated_at").notNull().defaultNow(), }) // ============================================================ // PROFILES (extends Better Auth user) // ============================================================ export const profiles = pgTable("profiles", { id: text("id") .primaryKey() .references(() => user.id, { onDelete: "cascade" }), email: text("email").notNull(), full_name: text("full_name"), avatar_url: text("avatar_url"), phone: text("phone"), company_name: text("company_name"), plan: text("plan").$type<"starter" | "pro" | "landlord" | "lifetime">().notNull().default("starter"), plan_expires_at: tstz("plan_expires_at"), stripe_customer_id: text("stripe_customer_id").unique(), stripe_subscription_id: text("stripe_subscription_id"), subscription_status: text("subscription_status"), // Which processor owns the active subscription. Stripe fields above and the // PayPal id below are mutually exclusive per active subscription. paypal_subscription_id: text("paypal_subscription_id"), billing_provider: text("billing_provider").$type<"stripe" | "paypal">(), trial_ends_at: tstz("trial_ends_at"), onboarding_completed: boolean("onboarding_completed").notNull().default(false), usage_count: integer("usage_count").notNull().default(0), // White-label branding (Landlord/Lifetime plans). Applied to the tenant portal. brand_name: text("brand_name"), brand_logo_url: text("brand_logo_url"), brand_color: text("brand_color"), hide_powered_by: boolean("hide_powered_by").notNull().default(false), // Read-only iCal (ICS) subscription feed token — served at /api/calendar/.ics calendar_token: text("calendar_token").unique().default(sql`gen_random_uuid()::text`), created_at: createdAt(), updated_at: updatedAt(), }) // ============================================================ // PROPERTIES // ============================================================ export const properties = pgTable("properties", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), name: text("name").notNull(), address_line1: text("address_line1").notNull(), address_line2: text("address_line2"), city: text("city").notNull(), state: text("state"), postal_code: text("postal_code"), country: text("country").notNull().default("US"), // Geocoded from the address on save (OpenStreetMap Nominatim). Null until // geocoding succeeds; drives the property map view. latitude: doublePrecision("latitude"), longitude: doublePrecision("longitude"), property_type: text("property_type") .$type<"residential" | "commercial" | "mixed">() .notNull() .default("residential"), total_units: integer("total_units").notNull().default(1), image_url: text("image_url"), notes: text("notes"), created_at: createdAt(), updated_at: updatedAt(), }) // ============================================================ // UNITS // ============================================================ export const units = pgTable("units", { id: uuid("id").primaryKey().defaultRandom(), property_id: uuid("property_id") .notNull() .references(() => properties.id, { onDelete: "cascade" }), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), unit_number: text("unit_number").notNull(), bedrooms: integer("bedrooms").notNull().default(1), bathrooms: numeric("bathrooms", { precision: 3, scale: 1 }).$type().notNull().default(sql`1`), sq_ft: integer("sq_ft"), rent_amount: numeric("rent_amount", { precision: 10, scale: 2 }).$type().notNull(), status: text("status") .$type<"vacant" | "occupied" | "maintenance" | "unavailable">() .notNull() .default("vacant"), current_tenant_id: uuid("current_tenant_id"), notes: text("notes"), created_at: createdAt(), updated_at: updatedAt(), }) // ============================================================ // TENANTS // ============================================================ export const tenants = pgTable("tenants", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), property_id: uuid("property_id") .notNull() .references(() => properties.id, { onDelete: "cascade" }), unit_id: uuid("unit_id").references(() => units.id, { onDelete: "set null" }), first_name: text("first_name").notNull(), last_name: text("last_name").notNull(), email: text("email"), phone: text("phone"), emergency_contact_name: text("emergency_contact_name"), emergency_contact_phone: text("emergency_contact_phone"), move_in_date: date("move_in_date", { mode: "string" }), move_out_date: date("move_out_date", { mode: "string" }), status: text("status").$type<"active" | "moved_out" | "evicted">().notNull().default("active"), portal_token: text("portal_token").notNull().unique().default(sql`gen_random_uuid()::text`), notes: text("notes"), created_at: createdAt(), updated_at: updatedAt(), }) // ============================================================ // RENT PAYMENTS // ============================================================ export const rent_payments = pgTable("rent_payments", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), tenant_id: uuid("tenant_id") .notNull() .references(() => tenants.id, { onDelete: "cascade" }), property_id: uuid("property_id") .notNull() .references(() => properties.id, { onDelete: "cascade" }), unit_id: uuid("unit_id").references(() => units.id, { onDelete: "set null" }), amount: numeric("amount", { precision: 10, scale: 2 }).$type().notNull(), due_date: date("due_date", { mode: "string" }).notNull(), paid_date: date("paid_date", { mode: "string" }), status: text("status") .$type<"pending" | "paid" | "overdue" | "partial" | "waived">() .notNull() .default("pending"), payment_method: text("payment_method"), stripe_payment_link_id: text("stripe_payment_link_id"), stripe_payment_intent_id: text("stripe_payment_intent_id"), reminder_sent_at: tstz("reminder_sent_at"), late_fee_applied: boolean("late_fee_applied").notNull().default(false), notes: text("notes"), created_at: createdAt(), updated_at: updatedAt(), }) // ============================================================ // MAINTENANCE REQUESTS // ============================================================ export const maintenance_requests = pgTable("maintenance_requests", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), tenant_id: uuid("tenant_id").references(() => tenants.id, { onDelete: "set null" }), property_id: uuid("property_id") .notNull() .references(() => properties.id, { onDelete: "cascade" }), unit_id: uuid("unit_id").references(() => units.id, { onDelete: "set null" }), title: text("title").notNull(), description: text("description").notNull(), category: text("category") .$type<"plumbing" | "electrical" | "hvac" | "appliance" | "structural" | "pest" | "general">() .notNull() .default("general"), priority: text("priority").$type<"low" | "medium" | "high" | "emergency">().notNull().default("medium"), status: text("status").$type<"open" | "in_progress" | "resolved" | "closed">().notNull().default("open"), images: text("images").array().notNull().default(sql`'{}'::text[]`), assigned_to: text("assigned_to"), estimated_cost: numeric("estimated_cost", { precision: 10, scale: 2 }).$type(), actual_cost: numeric("actual_cost", { precision: 10, scale: 2 }).$type(), resolved_at: tstz("resolved_at"), resolution_notes: text("resolution_notes"), created_at: createdAt(), updated_at: updatedAt(), }) // ============================================================ // LEASES // ============================================================ export const leases = pgTable("leases", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), tenant_id: uuid("tenant_id") .notNull() .references(() => tenants.id, { onDelete: "cascade" }), property_id: uuid("property_id") .notNull() .references(() => properties.id, { onDelete: "cascade" }), unit_id: uuid("unit_id").references(() => units.id, { onDelete: "set null" }), lease_start: date("lease_start", { mode: "string" }).notNull(), lease_end: date("lease_end", { mode: "string" }).notNull(), rent_amount: numeric("rent_amount", { precision: 10, scale: 2 }).$type().notNull(), security_deposit: numeric("security_deposit", { precision: 10, scale: 2 }).$type(), lease_type: text("lease_type").$type<"fixed" | "month_to_month">().notNull().default("fixed"), document_url: text("document_url"), status: text("status").$type<"active" | "expired" | "terminated" | "renewed">().notNull().default("active"), auto_renew: boolean("auto_renew").notNull().default(false), reminder_60_sent: boolean("reminder_60_sent").notNull().default(false), reminder_30_sent: boolean("reminder_30_sent").notNull().default(false), reminder_7_sent: boolean("reminder_7_sent").notNull().default(false), notes: text("notes"), created_at: createdAt(), updated_at: updatedAt(), }) // ============================================================ // EXPENSES // ============================================================ export const expenses = pgTable("expenses", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), property_id: uuid("property_id") .notNull() .references(() => properties.id, { onDelete: "cascade" }), unit_id: uuid("unit_id").references(() => units.id, { onDelete: "set null" }), category: text("category") .$type<"repairs" | "utilities" | "insurance" | "mortgage" | "taxes" | "management" | "supplies" | "other">() .notNull(), description: text("description").notNull(), amount: numeric("amount", { precision: 10, scale: 2 }).$type().notNull(), expense_date: date("expense_date", { mode: "string" }).notNull(), vendor: text("vendor"), receipt_url: text("receipt_url"), is_recurring: boolean("is_recurring").notNull().default(false), recurrence: text("recurrence"), notes: text("notes"), created_at: createdAt(), updated_at: updatedAt(), }) // ============================================================ // DOCUMENTS // ============================================================ export const documents = pgTable("documents", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), property_id: uuid("property_id") .notNull() .references(() => properties.id, { onDelete: "cascade" }), tenant_id: uuid("tenant_id").references(() => tenants.id, { onDelete: "set null" }), name: text("name").notNull(), file_url: text("file_url").notNull(), storage_path: text("storage_path"), file_type: text("file_type"), file_size: integer("file_size"), category: text("category") .$type<"lease" | "insurance" | "inspection" | "receipt" | "tax" | "other">() .notNull() .default("other"), created_at: createdAt(), }) // ============================================================ // NOTIFICATIONS // ============================================================ export const notifications = pgTable("notifications", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), type: text("type").notNull(), recipient_email: text("recipient_email").notNull(), subject: text("subject").notNull(), status: text("status").$type<"sent" | "failed" | "pending">().notNull().default("sent"), read: boolean("read").notNull().default(false), metadata: jsonb("metadata").$type>().notNull().default({}), sent_at: tstz("sent_at").notNull().defaultNow(), }) // ============================================================ // USAGE EVENTS // ============================================================ export const usage_events = pgTable("usage_events", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), event_type: text("event_type").notNull(), metadata: jsonb("metadata").$type>().notNull().default({}), created_at: createdAt(), }) // ============================================================ // VENDORS // ============================================================ export const vendors = pgTable("vendors", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), property_id: uuid("property_id").references(() => properties.id, { onDelete: "set null" }), name: text("name").notNull(), trade: text("trade"), phone: text("phone"), email: text("email"), notes: text("notes"), created_at: createdAt(), updated_at: updatedAt(), }) // ============================================================ // INSPECTIONS // ============================================================ export const inspections = pgTable("inspections", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), property_id: uuid("property_id") .notNull() .references(() => properties.id, { onDelete: "cascade" }), unit_id: uuid("unit_id").references(() => units.id, { onDelete: "set null" }), type: text("type").$type<"move_in" | "move_out" | "routine">().notNull(), date: date("date", { mode: "string" }).notNull(), status: text("status").$type<"draft" | "complete">().notNull().default("draft"), items: jsonb("items").$type().notNull().default([]), notes: text("notes"), created_at: createdAt(), updated_at: updatedAt(), }) // ============================================================ // AI RECOMMENDATIONS // ============================================================ export const ai_recommendations = pgTable("ai_recommendations", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), type: text("type").notNull(), title: text("title").notNull(), description: text("description").notNull(), impact: text("impact"), priority: text("priority").notNull().default("medium"), status: text("status").notNull().default("pending"), action_label: text("action_label"), action_data: jsonb("action_data").$type>(), applied_at: tstz("applied_at"), dismissed_at: tstz("dismissed_at"), created_at: createdAt(), updated_at: updatedAt(), }) // ============================================================ // AI PREDICTIONS (new — referenced in code, was missing from old schema) // ============================================================ export const ai_predictions = pgTable("ai_predictions", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), type: text("type").notNull(), title: text("title").notNull(), prediction: text("prediction").notNull(), confidence: text("confidence"), timeframe: text("timeframe"), risk_level: text("risk_level"), data: jsonb("data").$type>(), created_at: createdAt(), }) // ============================================================ // FOLLOW-UPS // ============================================================ export const follow_up_rules = pgTable("follow_up_rules", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), type: text("type").notNull(), name: text("name").notNull(), trigger_days: integer("trigger_days").notNull().default(3), message_template: text("message_template"), is_active: boolean("is_active").notNull().default(true), last_run_at: tstz("last_run_at"), created_at: createdAt(), updated_at: updatedAt(), }) export const follow_up_log = pgTable("follow_up_log", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), rule_id: uuid("rule_id") .notNull() .references(() => follow_up_rules.id, { onDelete: "cascade" }), type: text("type").notNull(), recipient_name: text("recipient_name"), recipient_email: text("recipient_email"), subject: text("subject").notNull(), message: text("message").notNull(), status: text("status").notNull().default("sent"), created_at: createdAt(), }) // ============================================================ // ACTIVITY LOG // ============================================================ export const activity_log = pgTable("activity_log", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), type: text("type").notNull(), title: text("title").notNull(), description: text("description"), entity_type: text("entity_type"), entity_id: uuid("entity_id"), metadata: jsonb("metadata").$type>().notNull().default({}), created_at: createdAt(), }) // ============================================================ // ADMIN AUDIT LOG (records every mutating superadmin action) // ============================================================ // target_user_id is intentionally NOT a cascading FK — the audit record must // survive deletion of the affected user. admin_id is set-null on admin removal. export const admin_audit_log = pgTable("admin_audit_log", { id: uuid("id").primaryKey().defaultRandom(), admin_id: text("admin_id").references(() => user.id, { onDelete: "set null" }), action: text("action").notNull(), target_user_id: text("target_user_id"), metadata: jsonb("metadata").$type>().notNull().default({}), ip_address: text("ip_address"), created_at: createdAt(), }) // ============================================================ // APP SETTINGS (global key/value — e.g. site maintenance mode) // ============================================================ // A tiny key/value store for runtime-toggled platform settings that must // persist and be changeable from the admin dashboard without a redeploy. export const app_settings = pgTable("app_settings", { key: text("key").primaryKey(), value: jsonb("value").$type>().notNull().default({}), updated_at: updatedAt(), }) // ============================================================ // ACCOUNT MEMBERS (team access — Landlord/Lifetime plans) // ============================================================ // Lets an account OWNER invite other users to access their portfolio. A member // with status='active' operates under the owner's data (resolved by // getEffectiveOwnerId in lib/account.ts). owner_id is the portfolio owner; // member_id is set once the invite is accepted. export const account_members = pgTable("account_members", { id: uuid("id").primaryKey().defaultRandom(), owner_id: text("owner_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), member_id: text("member_id").references(() => profiles.id, { onDelete: "cascade" }), email: text("email").notNull(), role: text("role").$type<"member" | "viewer">().notNull().default("member"), status: text("status").$type<"pending" | "active" | "revoked">().notNull().default("pending"), invite_token: text("invite_token").notNull().unique().default(sql`gen_random_uuid()::text`), accepted_at: tstz("accepted_at"), created_at: createdAt(), updated_at: updatedAt(), }) // ============================================================ // API KEYS (public REST API — Bearer auth for /api/v1) // ============================================================ // Each key belongs to a user. We store ONLY a SHA-256 hash of the secret; the // plaintext is shown once at creation and never persisted. `key_prefix` is a // short, non-secret identifier for display in the dashboard. export const api_keys = pgTable("api_keys", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), name: text("name").notNull(), key_hash: text("key_hash").notNull().unique(), key_prefix: text("key_prefix").notNull(), last_used_at: tstz("last_used_at"), revoked_at: tstz("revoked_at"), created_at: createdAt(), }) // ============================================================ // ACCOUNTING CONNECTIONS (QuickBooks / Xero OAuth sync) // ============================================================ // One row per (owner, provider). OAuth tokens are stored AES-256-GCM encrypted // (see lib/crypto.ts). A landlord connects their books and rent income + // expenses are pushed one-way into QuickBooks Online or Xero. export const accounting_connections = pgTable("accounting_connections", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), provider: text("provider").$type<"quickbooks" | "xero">().notNull(), access_token: text("access_token").notNull(), // encrypted refresh_token: text("refresh_token").notNull(), // encrypted expires_at: tstz("expires_at"), // Provider account id: QuickBooks realmId / Xero tenantId. realm_id: text("realm_id"), org_name: text("org_name"), status: text("status").$type<"active" | "error" | "revoked">().notNull().default("active"), last_sync_at: tstz("last_sync_at"), last_error: text("last_error"), created_at: createdAt(), updated_at: updatedAt(), }) // ============================================================ // SIGNATURE REQUESTS (e-signature — DocuSign / Dropbox Sign) // ============================================================ // Tracks a lease document sent out for e-signature. `external_id` is the // provider's envelope / signature_request id; the webhook flips status to // "signed" and records the completed document. export const signature_requests = pgTable("signature_requests", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), lease_id: uuid("lease_id").references(() => leases.id, { onDelete: "set null" }), provider: text("provider").$type<"docusign" | "dropbox_sign">().notNull(), external_id: text("external_id"), status: text("status") .$type<"sent" | "signed" | "declined" | "voided" | "error">() .notNull() .default("sent"), signer_email: text("signer_email").notNull(), signer_name: text("signer_name"), document_name: text("document_name"), signed_document_url: text("signed_document_url"), last_error: text("last_error"), sent_at: createdAt(), completed_at: tstz("completed_at"), updated_at: updatedAt(), }) // ============================================================ // WEBHOOK ENDPOINTS (outbound webhooks / Zapier integration) // ============================================================ // A landlord registers HTTPS endpoints that receive a signed JSON POST every // time a subscribed event occurs (e.g. tenant.created, payment.paid). Scoped by // the account owner id so every event in the portfolio is delivered. The // `secret` is the HMAC-SHA256 signing key surfaced in the dashboard so the // receiver can verify the `X-PMN-Signature` header. `events` is the set of // subscribed event ids; an empty array means "all events". `source` records who // created it (dashboard, the REST API, or a Zapier REST-hook subscription). export const webhook_endpoints = pgTable("webhook_endpoints", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), url: text("url").notNull(), description: text("description"), events: text("events").array().notNull().default(sql`'{}'::text[]`), secret: text("secret").notNull(), status: text("status").$type<"active" | "disabled">().notNull().default("active"), source: text("source").$type<"dashboard" | "api" | "zapier">().notNull().default("dashboard"), last_success_at: tstz("last_success_at"), last_error_at: tstz("last_error_at"), last_error: text("last_error"), // Consecutive delivery failures; reset to 0 on any success. failure_count: integer("failure_count").notNull().default(0), created_at: createdAt(), updated_at: updatedAt(), }) // ============================================================ // WEBHOOK DELIVERIES (per-endpoint delivery log + retry queue) // ============================================================ // One row per (event, endpoint). Created "pending"; the emitter attempts an // immediate delivery and the webhooks cron retries anything still pending/failed // with exponential backoff until max_attempts is reached. export const webhook_deliveries = pgTable("webhook_deliveries", { id: uuid("id").primaryKey().defaultRandom(), user_id: text("user_id") .notNull() .references(() => profiles.id, { onDelete: "cascade" }), endpoint_id: uuid("endpoint_id") .notNull() .references(() => webhook_endpoints.id, { onDelete: "cascade" }), event: text("event").notNull(), payload: jsonb("payload").$type>().notNull(), status: text("status").$type<"pending" | "success" | "failed">().notNull().default("pending"), attempts: integer("attempts").notNull().default(0), max_attempts: integer("max_attempts").notNull().default(5), next_attempt_at: tstz("next_attempt_at").notNull().defaultNow(), response_status: integer("response_status"), response_body: text("response_body"), error: text("error"), delivered_at: tstz("delivered_at"), created_at: createdAt(), updated_at: updatedAt(), }) // ============================================================ // RELATIONS (for Drizzle relational queries) // ============================================================ export const profilesRelations = relations(profiles, ({ one }) => ({ user: one(user, { fields: [profiles.id], references: [user.id] }), })) export const propertiesRelations = relations(properties, ({ many }) => ({ units: many(units), tenants: many(tenants), })) export const unitsRelations = relations(units, ({ one, many }) => ({ property: one(properties, { fields: [units.property_id], references: [properties.id] }), // unit's current tenant (units.current_tenant_id -> tenants.id) current_tenant: one(tenants, { fields: [units.current_tenant_id], references: [tenants.id], relationName: "current_tenant", }), // tenants whose unit_id points here (inverse of tenants.unit) tenants: many(tenants, { relationName: "unit_tenants" }), })) export const tenantsRelations = relations(tenants, ({ one, many }) => ({ property: one(properties, { fields: [tenants.property_id], references: [properties.id] }), unit: one(units, { fields: [tenants.unit_id], references: [units.id], relationName: "unit_tenants", }), // inverse of units.current_tenant current_of_units: many(units, { relationName: "current_tenant" }), leases: many(leases), rent_payments: many(rent_payments), })) export const rent_paymentsRelations = relations(rent_payments, ({ one }) => ({ tenant: one(tenants, { fields: [rent_payments.tenant_id], references: [tenants.id] }), property: one(properties, { fields: [rent_payments.property_id], references: [properties.id] }), unit: one(units, { fields: [rent_payments.unit_id], references: [units.id] }), })) export const leasesRelations = relations(leases, ({ one }) => ({ tenant: one(tenants, { fields: [leases.tenant_id], references: [tenants.id] }), property: one(properties, { fields: [leases.property_id], references: [properties.id] }), unit: one(units, { fields: [leases.unit_id], references: [units.id] }), })) export const maintenance_requestsRelations = relations(maintenance_requests, ({ one }) => ({ tenant: one(tenants, { fields: [maintenance_requests.tenant_id], references: [tenants.id] }), property: one(properties, { fields: [maintenance_requests.property_id], references: [properties.id] }), unit: one(units, { fields: [maintenance_requests.unit_id], references: [units.id] }), })) export const expensesRelations = relations(expenses, ({ one }) => ({ property: one(properties, { fields: [expenses.property_id], references: [properties.id] }), unit: one(units, { fields: [expenses.unit_id], references: [units.id] }), })) export const inspectionsRelations = relations(inspections, ({ one }) => ({ property: one(properties, { fields: [inspections.property_id], references: [properties.id] }), unit: one(units, { fields: [inspections.unit_id], references: [units.id] }), })) export const documentsRelations = relations(documents, ({ one }) => ({ property: one(properties, { fields: [documents.property_id], references: [properties.id] }), tenant: one(tenants, { fields: [documents.tenant_id], references: [tenants.id] }), })) export const vendorsRelations = relations(vendors, ({ one }) => ({ property: one(properties, { fields: [vendors.property_id], references: [properties.id] }), })) export const follow_up_logRelations = relations(follow_up_log, ({ one }) => ({ rule: one(follow_up_rules, { fields: [follow_up_log.rule_id], references: [follow_up_rules.id] }), })) export const webhook_endpointsRelations = relations(webhook_endpoints, ({ many }) => ({ deliveries: many(webhook_deliveries), })) export const webhook_deliveriesRelations = relations(webhook_deliveries, ({ one }) => ({ endpoint: one(webhook_endpoints, { fields: [webhook_deliveries.endpoint_id], references: [webhook_endpoints.id], }), }))