7694788387
- Complete ProCanvas redesign with retro sports game aesthetic - Card-game style photo frame with tilt, shimmer, corner star accents - Daily Missions card with weekly challenge + individual quest progress bars - Log Action card (Door Knocked, Lead Gained, Appointment Set, Client Meeting) - Each log action opens themed modal with relevant input fields - Challenges modal with 6 active challenges and progress tracking - Achievements modal with all badges, unlock status, descriptions - Nav bar tabs (Leaderboard, Challenges, Achievements) wired to modals - Rewards & Checkpoints with named stages (Daily Grind to Legend Run) - Smoother Hot Streak pulse animation (2.5s float + 3s pulse rings) - Hit The Map button with smooth pulsing glow animation - Grid pattern overlay in Leaderboard card - Light mode support via dark: Tailwind variants throughout - Top 3 badges displayed on profile card - Fixed dropdown option visibility in dark mode
837 lines
30 KiB
Markdown
837 lines
30 KiB
Markdown
# B2 — Database Schema
|
||
|
||
**Module:** B2
|
||
**Depends on:** B0 (Project Overview)
|
||
**Read before:** B1, B3, B4, B5, B6, B7 (every other module references tables defined here)
|
||
|
||
---
|
||
|
||
## 1. Overview
|
||
|
||
This document defines the full **PostgreSQL 16** schema for LynkedUpPro. Every table, column, type, constraint, and relationship is specified here. This is the single source of truth — all ORM models, migrations, and API schemas must match this document.
|
||
|
||
### Design Principles
|
||
|
||
- **Soft deletes everywhere:** No `DELETE` in production. All tables have `deleted_at TIMESTAMPTZ NULL`. Rows with a non-null `deleted_at` are hidden from all queries by default.
|
||
- **Audit trail:** Every write-capable table has `created_at`, `updated_at`, and the critical tables also write to `audit_logs`.
|
||
- **Money in cents:** All monetary columns are `INTEGER` (cents). Never `DECIMAL` or `FLOAT` for money.
|
||
- **UUIDs for public-facing IDs:** User-facing IDs use `UUID` to prevent enumeration attacks. Internal join keys use `INTEGER` sequences for performance.
|
||
- **JSONB for flexible data:** Fields that are truly schema-less (insurance metadata, gamification achievements) use `JSONB`.
|
||
|
||
---
|
||
|
||
## 2. Entity Relationship Summary
|
||
|
||
```
|
||
users ──────────────────────────────────────────────────────────────┐
|
||
│ │
|
||
├─── properties (assigned_agent_id → users.id) │
|
||
│ └─── property_photos │
|
||
│ │
|
||
├─── meetings (agent_id → users.id, customer_id → users.id) │
|
||
│ └─── meeting_change_requests │
|
||
│ │
|
||
├─── projects (owner_id → users.id) │
|
||
│ ├─── project_tasks (assigned_to → users.id) │
|
||
│ ├─── project_contractors (contractor_id → users.id) │
|
||
│ └─── change_orders │
|
||
│ │
|
||
├─── vendors (managed by OWNER users) │
|
||
│ ├─── vendor_compliance_docs │
|
||
│ └─── vendor_orders (project_id → projects.id) │
|
||
│ │
|
||
├─── invoices (issued_by → users.id, approved_by → users.id) │
|
||
│ │
|
||
├─── documents (uploaded_by → users.id) │
|
||
│ │
|
||
├─── notifications (user_id → users.id) │
|
||
│ │
|
||
└─── audit_logs (actor_id → users.id) │
|
||
│
|
||
sales_history (agent_id → users.id) ◄──────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Table Definitions
|
||
|
||
### 3.1 `users`
|
||
|
||
The central identity table. All roles share this table, differentiated by the `role` column.
|
||
|
||
```sql
|
||
CREATE TYPE user_role AS ENUM (
|
||
'OWNER', 'ADMIN', 'FIELD_AGENT', 'CONTRACTOR', 'SUBCONTRACTOR', 'VENDOR', 'CUSTOMER'
|
||
);
|
||
|
||
CREATE TYPE user_type AS ENUM (
|
||
'employee', 'owner', 'contractor', 'subcontractor', 'vendor', 'customer'
|
||
);
|
||
|
||
CREATE TABLE users (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
||
-- Identity
|
||
legacy_id VARCHAR(20) UNIQUE, -- Preserves mock IDs ('e1', 'own_001', etc.) during migration
|
||
email VARCHAR(255) UNIQUE NOT NULL,
|
||
username VARCHAR(100) UNIQUE, -- For customers + non-employee login
|
||
emp_id VARCHAR(20) UNIQUE, -- For FIELD_AGENT and ADMIN ('FA001', 'ADM01')
|
||
password_hash VARCHAR(255) NOT NULL, -- bcrypt hash
|
||
|
||
-- Profile
|
||
full_name VARCHAR(255) NOT NULL,
|
||
phone VARCHAR(30),
|
||
role user_role NOT NULL,
|
||
user_type user_type NOT NULL,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
|
||
-- Role-specific optional fields
|
||
company_name VARCHAR(255), -- OWNER, CONTRACTOR, SUBCONTRACTOR, VENDOR
|
||
license_number VARCHAR(100), -- CONTRACTOR (e.g. 'TX-GC-123456')
|
||
trade_type VARCHAR(100), -- SUBCONTRACTOR ('electrical'), VENDOR ('materials')
|
||
|
||
-- Customer-specific
|
||
property_id VARCHAR(50), -- Links customer to their property (pre-migration)
|
||
|
||
-- Gamification (FIELD_AGENT only — others NULL)
|
||
xp INTEGER DEFAULT 0,
|
||
doors_knocked INTEGER DEFAULT 0,
|
||
leads_gained INTEGER DEFAULT 0,
|
||
appointments_set INTEGER DEFAULT 0,
|
||
streak_days INTEGER DEFAULT 0,
|
||
achievements JSONB DEFAULT '[]', -- ["Hot Spot Hunter", "Storm Chaser"]
|
||
|
||
-- Auth
|
||
last_login_at TIMESTAMPTZ,
|
||
refresh_token_hash VARCHAR(255), -- Hashed refresh token for rotation
|
||
|
||
-- Timestamps
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ -- Soft delete
|
||
);
|
||
|
||
CREATE INDEX idx_users_role ON users(role) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_users_email ON users(email) WHERE deleted_at IS NULL;
|
||
```
|
||
|
||
---
|
||
|
||
### 3.2 `properties`
|
||
|
||
The core geospatial asset table. Each row is one property in Plano, TX.
|
||
|
||
```sql
|
||
CREATE TYPE property_type AS ENUM ('Residential', 'Commercial', 'Apartments');
|
||
|
||
CREATE TYPE canvassing_status AS ENUM (
|
||
'Hot Lead', 'Customer', 'Neutral', 'Not Interested', 'Renovated'
|
||
);
|
||
|
||
CREATE TYPE roof_condition AS ENUM ('Excellent', 'Good', 'Fair', 'Needs Repair');
|
||
|
||
CREATE TABLE properties (
|
||
id SERIAL PRIMARY KEY,
|
||
property_id VARCHAR(20) UNIQUE NOT NULL, -- 'P-2600', 'P-2612', etc.
|
||
|
||
-- Location
|
||
address VARCHAR(500) NOT NULL,
|
||
city VARCHAR(100) NOT NULL DEFAULT 'Plano',
|
||
state VARCHAR(10) NOT NULL DEFAULT 'TX',
|
||
zip_code VARCHAR(10),
|
||
latitude DECIMAL(10, 8) NOT NULL,
|
||
longitude DECIMAL(11, 8) NOT NULL,
|
||
polygon JSONB, -- [[lat,lng], [lat,lng], [lat,lng], [lat,lng]]
|
||
|
||
-- Classification
|
||
property_type property_type NOT NULL,
|
||
canvassing_status canvassing_status NOT NULL DEFAULT 'Neutral',
|
||
|
||
-- Physical attributes
|
||
total_sqft INTEGER,
|
||
year_built INTEGER,
|
||
bedrooms SMALLINT,
|
||
bathrooms DECIMAL(3,1),
|
||
parking_spaces SMALLINT,
|
||
lot_size VARCHAR(50), -- '0.22 Acres'
|
||
|
||
-- Roof
|
||
roof_condition roof_condition,
|
||
last_roof_repair_date DATE,
|
||
last_renovation_date DATE,
|
||
renovation_description TEXT,
|
||
renovation_cost INTEGER, -- Cents
|
||
|
||
-- Financials
|
||
estimated_market_value INTEGER, -- Cents
|
||
tax_assessment_value INTEGER, -- Cents
|
||
latest_purchase_price INTEGER, -- Cents
|
||
latest_purchase_date DATE,
|
||
asking_price INTEGER, -- Cents (if listed)
|
||
listing_date DATE,
|
||
|
||
-- CRM fields
|
||
assigned_agent_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||
last_contact_date TIMESTAMPTZ,
|
||
pending_signature BOOLEAN NOT NULL DEFAULT FALSE,
|
||
proposal_sent_date DATE,
|
||
proposal_value INTEGER DEFAULT 0, -- Cents
|
||
closed_date DATE,
|
||
|
||
-- Owner info (denormalized for fast access)
|
||
owner_name VARCHAR(255),
|
||
owner_phone VARCHAR(30),
|
||
owner_email VARCHAR(255),
|
||
owner_occupation VARCHAR(255),
|
||
owner_employer VARCHAR(255),
|
||
owner_annual_income VARCHAR(50), -- '$80k - $120k'
|
||
owner_credit_range VARCHAR(50), -- '720+'
|
||
willing_to_sell BOOLEAN DEFAULT FALSE,
|
||
desired_selling_price INTEGER, -- Cents
|
||
min_selling_price INTEGER, -- Cents
|
||
willing_to_rent BOOLEAN DEFAULT FALSE,
|
||
desired_monthly_rent INTEGER, -- Cents
|
||
|
||
-- Rental / tenant info
|
||
currently_rented BOOLEAN NOT NULL DEFAULT FALSE,
|
||
tenant_name VARCHAR(255),
|
||
tenant_phone VARCHAR(30),
|
||
tenant_email VARCHAR(255),
|
||
tenant_occupation VARCHAR(255),
|
||
tenant_employer VARCHAR(255),
|
||
tenant_annual_income VARCHAR(50),
|
||
living_status VARCHAR(50), -- 'Family', 'Single'
|
||
lease_type VARCHAR(100), -- 'Residential Standard', 'Commercial NNN'
|
||
monthly_rent_amount INTEGER, -- Cents
|
||
lease_start_date DATE,
|
||
lease_end_date DATE,
|
||
lease_signed_date DATE,
|
||
|
||
-- Insurance
|
||
insurance_company VARCHAR(255),
|
||
claim_filed BOOLEAN DEFAULT FALSE,
|
||
claim_number VARCHAR(50),
|
||
date_of_loss DATE,
|
||
damage_location TEXT,
|
||
adjuster_name VARCHAR(255),
|
||
adjuster_phone VARCHAR(30),
|
||
adjuster_type VARCHAR(100),
|
||
has_paperwork BOOLEAN DEFAULT FALSE,
|
||
insurance_meta JSONB DEFAULT '{}', -- Any additional insurance fields
|
||
|
||
-- Misc
|
||
school_district VARCHAR(100) DEFAULT 'Plano ISD',
|
||
neighborhood_rating SMALLINT CHECK (neighborhood_rating BETWEEN 1 AND 10),
|
||
crime_rate_index VARCHAR(50) DEFAULT 'Low',
|
||
notes TEXT,
|
||
condition_rating SMALLINT CHECK (condition_rating BETWEEN 1 AND 5),
|
||
|
||
-- Timestamps
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ
|
||
);
|
||
|
||
CREATE INDEX idx_properties_status ON properties(canvassing_status) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_properties_agent ON properties(assigned_agent_id) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_properties_location ON properties USING GIST (point(longitude, latitude));
|
||
```
|
||
|
||
**`property_photos`**
|
||
|
||
```sql
|
||
CREATE TABLE property_photos (
|
||
id SERIAL PRIMARY KEY,
|
||
property_id INTEGER NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
||
url VARCHAR(1000) NOT NULL,
|
||
caption VARCHAR(255),
|
||
sort_order SMALLINT DEFAULT 0,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.3 `meetings`
|
||
|
||
Scheduled inspections/consultations between agents and customers.
|
||
|
||
```sql
|
||
CREATE TYPE meeting_status AS ENUM (
|
||
'Scheduled', 'Rescheduled', 'In Progress', 'Completed', 'Converted', 'Cancelled'
|
||
);
|
||
|
||
CREATE TABLE meetings (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
legacy_id VARCHAR(20) UNIQUE, -- Preserves 'm1', 'm-alice-1', etc.
|
||
|
||
-- Participants
|
||
agent_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||
customer_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||
customer_name VARCHAR(255), -- Denormalized for guests without accounts
|
||
|
||
-- Location
|
||
property_id INTEGER REFERENCES properties(id) ON DELETE SET NULL,
|
||
property_address VARCHAR(500), -- Snapshot at time of booking
|
||
|
||
-- Scheduling
|
||
meeting_date DATE NOT NULL,
|
||
meeting_time TIME NOT NULL,
|
||
status meeting_status NOT NULL DEFAULT 'Scheduled',
|
||
|
||
-- Content
|
||
issue_description TEXT,
|
||
customer_comments TEXT,
|
||
notes TEXT,
|
||
outcome VARCHAR(100), -- 'Signed Contract', 'Quote Sent', 'Pending'
|
||
deal_value INTEGER, -- Cents; populated on Completed/Converted
|
||
|
||
-- Timestamps
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ
|
||
);
|
||
|
||
CREATE INDEX idx_meetings_agent ON meetings(agent_id, meeting_date) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_meetings_status ON meetings(status) WHERE deleted_at IS NULL;
|
||
```
|
||
|
||
**`meeting_change_requests`** *(Phase 9 — Meeting Governance)*
|
||
|
||
```sql
|
||
CREATE TYPE change_request_status AS ENUM ('Pending', 'Approved', 'Rejected');
|
||
|
||
CREATE TABLE meeting_change_requests (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
meeting_id UUID NOT NULL REFERENCES meetings(id) ON DELETE CASCADE,
|
||
requested_by UUID NOT NULL REFERENCES users(id),
|
||
reviewed_by UUID REFERENCES users(id),
|
||
|
||
-- Proposed changes
|
||
proposed_date DATE,
|
||
proposed_time TIME,
|
||
proposed_status meeting_status,
|
||
reason TEXT NOT NULL,
|
||
|
||
-- Resolution
|
||
status change_request_status NOT NULL DEFAULT 'Pending',
|
||
reviewer_note TEXT,
|
||
resolved_at TIMESTAMPTZ,
|
||
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.4 `vendors`
|
||
|
||
External companies providing materials or services (not users; managed by OWNERs).
|
||
|
||
```sql
|
||
CREATE TYPE vendor_status AS ENUM ('Active', 'Inactive', 'Pending Review', 'Suspended');
|
||
|
||
CREATE TABLE vendors (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
legacy_id VARCHAR(20) UNIQUE, -- 'v1', 'v3', etc.
|
||
|
||
company_name VARCHAR(255) NOT NULL,
|
||
contact_name VARCHAR(255),
|
||
email VARCHAR(255),
|
||
phone VARCHAR(30),
|
||
website VARCHAR(500),
|
||
trade_type VARCHAR(100), -- 'materials', 'electrical', 'roofing', etc.
|
||
status vendor_status NOT NULL DEFAULT 'Active',
|
||
|
||
-- Performance (updated by scheduler/trigger)
|
||
on_time_delivery_rate DECIMAL(5,2), -- Percentage 0.00–100.00
|
||
defect_rate DECIMAL(5,2),
|
||
avg_response_hours DECIMAL(6,2),
|
||
total_spend_cents INTEGER DEFAULT 0,
|
||
total_orders INTEGER DEFAULT 0,
|
||
|
||
-- Compliance snapshot (updated from vendor_compliance_docs)
|
||
coi_expiry_date DATE,
|
||
w9_on_file BOOLEAN DEFAULT FALSE,
|
||
license_expiry_date DATE,
|
||
is_compliant BOOLEAN DEFAULT FALSE, -- Computed from compliance docs
|
||
|
||
notes TEXT,
|
||
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ
|
||
);
|
||
```
|
||
|
||
**`vendor_compliance_docs`**
|
||
|
||
```sql
|
||
CREATE TYPE compliance_doc_type AS ENUM ('COI', 'W9', 'LICENSE', 'CONTRACT', 'OTHER');
|
||
CREATE TYPE compliance_doc_status AS ENUM ('Active', 'Expiring Soon', 'Expired', 'Missing');
|
||
|
||
CREATE TABLE vendor_compliance_docs (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
vendor_id UUID NOT NULL REFERENCES vendors(id) ON DELETE CASCADE,
|
||
doc_type compliance_doc_type NOT NULL,
|
||
file_url VARCHAR(1000), -- S3 URL
|
||
file_name VARCHAR(255),
|
||
status compliance_doc_status NOT NULL DEFAULT 'Active',
|
||
issue_date DATE,
|
||
expiry_date DATE,
|
||
notes TEXT,
|
||
uploaded_by UUID REFERENCES users(id),
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_compliance_expiry ON vendor_compliance_docs(expiry_date)
|
||
WHERE expiry_date IS NOT NULL;
|
||
```
|
||
|
||
**`vendor_orders`**
|
||
|
||
```sql
|
||
CREATE TYPE order_status AS ENUM ('Pending', 'Confirmed', 'Shipped', 'Delivered', 'Cancelled', 'Disputed');
|
||
|
||
CREATE TABLE vendor_orders (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
vendor_id UUID NOT NULL REFERENCES vendors(id),
|
||
project_id UUID REFERENCES projects(id),
|
||
order_number VARCHAR(100) UNIQUE NOT NULL,
|
||
description TEXT,
|
||
quantity INTEGER,
|
||
unit_price_cents INTEGER, -- Per unit, cents
|
||
total_cents INTEGER NOT NULL, -- Quantity * unit_price
|
||
status order_status NOT NULL DEFAULT 'Pending',
|
||
ordered_date DATE,
|
||
expected_date DATE,
|
||
delivered_date DATE,
|
||
notes TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.5 `projects`
|
||
|
||
Construction/roofing projects managed through the Owner's Box.
|
||
|
||
```sql
|
||
CREATE TYPE project_status AS ENUM (
|
||
'active', 'completed', 'delayed', 'on_hold', 'disputed', 'cancelled'
|
||
);
|
||
|
||
CREATE TABLE projects (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
legacy_id VARCHAR(20) UNIQUE,
|
||
|
||
owner_id UUID NOT NULL REFERENCES users(id), -- Must have role=OWNER
|
||
property_id INTEGER REFERENCES properties(id),
|
||
|
||
title VARCHAR(500) NOT NULL,
|
||
description TEXT,
|
||
status project_status NOT NULL DEFAULT 'active',
|
||
health_score SMALLINT CHECK (health_score BETWEEN 0 AND 100),
|
||
|
||
-- Budget
|
||
approved_budget_cents INTEGER NOT NULL DEFAULT 0,
|
||
actual_cost_cents INTEGER NOT NULL DEFAULT 0,
|
||
|
||
-- Dates
|
||
start_date DATE,
|
||
target_end_date DATE,
|
||
actual_end_date DATE,
|
||
|
||
-- Progress
|
||
completion_pct SMALLINT DEFAULT 0 CHECK (completion_pct BETWEEN 0 AND 100),
|
||
total_tasks INTEGER DEFAULT 0,
|
||
completed_tasks INTEGER DEFAULT 0,
|
||
|
||
notes TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ
|
||
);
|
||
|
||
CREATE INDEX idx_projects_owner ON projects(owner_id) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_projects_status ON projects(status) WHERE deleted_at IS NULL;
|
||
```
|
||
|
||
**`project_tasks`**
|
||
|
||
```sql
|
||
CREATE TYPE task_status AS ENUM ('pending', 'in_progress', 'completed', 'blocked');
|
||
CREATE TYPE task_priority AS ENUM ('low', 'medium', 'high', 'critical');
|
||
|
||
CREATE TABLE project_tasks (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||
assigned_to UUID REFERENCES users(id),
|
||
|
||
title VARCHAR(500) NOT NULL,
|
||
description TEXT,
|
||
status task_status NOT NULL DEFAULT 'pending',
|
||
priority task_priority NOT NULL DEFAULT 'medium',
|
||
due_date DATE,
|
||
completed_at TIMESTAMPTZ,
|
||
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
```
|
||
|
||
**`change_orders`**
|
||
|
||
```sql
|
||
CREATE TYPE change_order_status AS ENUM ('Pending', 'Approved', 'Rejected', 'Implemented');
|
||
|
||
CREATE TABLE change_orders (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||
requested_by UUID NOT NULL REFERENCES users(id),
|
||
approved_by UUID REFERENCES users(id),
|
||
|
||
title VARCHAR(500) NOT NULL,
|
||
description TEXT,
|
||
cost_impact_cents INTEGER NOT NULL DEFAULT 0, -- Can be negative (savings)
|
||
schedule_impact_days INTEGER DEFAULT 0,
|
||
status change_order_status NOT NULL DEFAULT 'Pending',
|
||
approved_at TIMESTAMPTZ,
|
||
notes TEXT,
|
||
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.6 `invoices`
|
||
|
||
Tracks all financial transactions: customer billing, vendor payments, subcontractor payouts.
|
||
|
||
```sql
|
||
CREATE TYPE invoice_type AS ENUM ('customer', 'vendor', 'subcontractor', 'internal');
|
||
CREATE TYPE invoice_status AS ENUM (
|
||
'Draft', 'Sent', 'Viewed', 'Partial', 'Paid', 'Overdue', 'Disputed', 'Void'
|
||
);
|
||
|
||
CREATE TABLE invoices (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
invoice_number VARCHAR(50) UNIQUE NOT NULL, -- 'INV-2026-0042'
|
||
invoice_type invoice_type NOT NULL,
|
||
|
||
-- Parties
|
||
issued_by UUID NOT NULL REFERENCES users(id),
|
||
billed_to_user UUID REFERENCES users(id), -- For customer invoices
|
||
vendor_id UUID REFERENCES vendors(id), -- For vendor invoices
|
||
project_id UUID REFERENCES projects(id),
|
||
|
||
-- Amounts (all cents)
|
||
subtotal_cents INTEGER NOT NULL DEFAULT 0,
|
||
tax_cents INTEGER NOT NULL DEFAULT 0,
|
||
discount_cents INTEGER NOT NULL DEFAULT 0,
|
||
total_cents INTEGER NOT NULL DEFAULT 0, -- subtotal + tax - discount
|
||
amount_paid_cents INTEGER NOT NULL DEFAULT 0,
|
||
balance_cents INTEGER GENERATED ALWAYS AS (total_cents - amount_paid_cents) STORED,
|
||
|
||
-- Dates
|
||
issue_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||
due_date DATE,
|
||
paid_date DATE,
|
||
|
||
status invoice_status NOT NULL DEFAULT 'Draft',
|
||
line_items JSONB DEFAULT '[]', -- Array of {description, qty, unit_price_cents, total_cents}
|
||
notes TEXT,
|
||
file_url VARCHAR(1000), -- S3 URL for PDF copy
|
||
|
||
-- Approval workflow (for payout invoices)
|
||
approved_by UUID REFERENCES users(id),
|
||
approved_at TIMESTAMPTZ,
|
||
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ
|
||
);
|
||
|
||
CREATE INDEX idx_invoices_project ON invoices(project_id) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_invoices_status ON invoices(status) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_invoices_due ON invoices(due_date) WHERE status NOT IN ('Paid', 'Void') AND deleted_at IS NULL;
|
||
```
|
||
|
||
---
|
||
|
||
### 3.7 `documents`
|
||
|
||
Contract PDFs, compliance files, inspection reports — all role-accessible files.
|
||
|
||
```sql
|
||
CREATE TYPE document_category AS ENUM (
|
||
'contract', 'invoice', 'compliance', 'inspection', 'proposal', 'permit', 'insurance', 'other'
|
||
);
|
||
|
||
CREATE TYPE document_review_status AS ENUM (
|
||
'Pending Review', 'Under Review', 'Approved', 'Rejected', 'Expired'
|
||
);
|
||
|
||
CREATE TABLE documents (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
title VARCHAR(500) NOT NULL,
|
||
category document_category NOT NULL,
|
||
review_status document_review_status NOT NULL DEFAULT 'Pending Review',
|
||
|
||
-- Associations (any combination can be set)
|
||
project_id UUID REFERENCES projects(id),
|
||
vendor_id UUID REFERENCES vendors(id),
|
||
property_id INTEGER REFERENCES properties(id),
|
||
related_user_id UUID REFERENCES users(id),
|
||
|
||
-- File
|
||
file_url VARCHAR(1000) NOT NULL, -- S3 URL
|
||
file_name VARCHAR(255) NOT NULL,
|
||
file_size_bytes INTEGER,
|
||
mime_type VARCHAR(100),
|
||
|
||
-- Metadata
|
||
uploaded_by UUID NOT NULL REFERENCES users(id),
|
||
reviewed_by UUID REFERENCES users(id),
|
||
review_notes TEXT,
|
||
expiry_date DATE,
|
||
tags JSONB DEFAULT '[]', -- ['W9', '2026', 'vendor']
|
||
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.8 `sales_history`
|
||
|
||
Closed deal records used for leaderboard and revenue reporting.
|
||
|
||
```sql
|
||
CREATE TYPE deal_status AS ENUM ('closed_won', 'closed_lost');
|
||
|
||
CREATE TABLE sales_history (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
legacy_id VARCHAR(20) UNIQUE, -- 'tx_1', etc.
|
||
|
||
agent_id UUID NOT NULL REFERENCES users(id),
|
||
property_id INTEGER REFERENCES properties(id),
|
||
meeting_id UUID REFERENCES meetings(id),
|
||
|
||
closed_date DATE NOT NULL,
|
||
amount_cents INTEGER NOT NULL DEFAULT 0,
|
||
status deal_status NOT NULL,
|
||
notes TEXT,
|
||
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_sales_agent ON sales_history(agent_id, closed_date);
|
||
CREATE INDEX idx_sales_date ON sales_history(closed_date);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.9 `notifications`
|
||
|
||
In-app notification records for all roles.
|
||
|
||
```sql
|
||
CREATE TYPE notification_type AS ENUM (
|
||
'lead_assigned', 'meeting_reminder', 'compliance_expiring', 'compliance_expired',
|
||
'invoice_due', 'invoice_overdue', 'payout_approved', 'payout_rejected',
|
||
'change_order_submitted', 'change_order_approved', 'document_uploaded',
|
||
'system'
|
||
);
|
||
|
||
CREATE TABLE notifications (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||
type notification_type NOT NULL,
|
||
title VARCHAR(255) NOT NULL,
|
||
body TEXT,
|
||
is_read BOOLEAN NOT NULL DEFAULT FALSE,
|
||
deep_link VARCHAR(500), -- Frontend route, e.g. '/owner/projects/uuid'
|
||
metadata JSONB DEFAULT '{}', -- Contextual data for rendering
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_notifications_user ON notifications(user_id, is_read, created_at DESC);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.10 `audit_logs`
|
||
|
||
Immutable record of all write operations on sensitive data.
|
||
|
||
```sql
|
||
CREATE TABLE audit_logs (
|
||
id BIGSERIAL PRIMARY KEY, -- High-volume; use BIGSERIAL not UUID
|
||
actor_id UUID REFERENCES users(id), -- NULL if system action
|
||
action VARCHAR(100) NOT NULL, -- 'property.assigned', 'invoice.approved', etc.
|
||
resource_type VARCHAR(100) NOT NULL, -- 'property', 'invoice', 'user'
|
||
resource_id VARCHAR(100) NOT NULL, -- UUID or legacy ID as string
|
||
old_value JSONB, -- Snapshot before change
|
||
new_value JSONB, -- Snapshot after change
|
||
ip_address INET,
|
||
user_agent TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_audit_resource ON audit_logs(resource_type, resource_id);
|
||
CREATE INDEX idx_audit_actor ON audit_logs(actor_id);
|
||
CREATE INDEX idx_audit_created ON audit_logs(created_at DESC);
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Shared Triggers & Conventions
|
||
|
||
### Auto-update `updated_at`
|
||
|
||
Apply to every table that has `updated_at`:
|
||
|
||
```sql
|
||
CREATE OR REPLACE FUNCTION set_updated_at()
|
||
RETURNS TRIGGER AS $$
|
||
BEGIN
|
||
NEW.updated_at = NOW();
|
||
RETURN NEW;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
-- Apply to each table:
|
||
CREATE TRIGGER trg_users_updated_at
|
||
BEFORE UPDATE ON users
|
||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||
-- (repeat for properties, meetings, vendors, etc.)
|
||
```
|
||
|
||
### Soft Delete Filter
|
||
|
||
All application queries **must** include `WHERE deleted_at IS NULL`. Use a SQLAlchemy default query filter on all models:
|
||
|
||
```python
|
||
# app/models/base.py
|
||
from sqlalchemy.orm import DeclarativeBase, declared_attr
|
||
from sqlalchemy import Column, TIMESTAMP, func
|
||
|
||
class Base(DeclarativeBase):
|
||
pass
|
||
|
||
class SoftDeleteMixin:
|
||
deleted_at = Column(TIMESTAMP(timezone=True), nullable=True)
|
||
|
||
@classmethod
|
||
def active(cls):
|
||
"""Returns a filter expression for non-deleted rows."""
|
||
return cls.deleted_at.is_(None)
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Alembic Migration Strategy
|
||
|
||
### Initial Migration
|
||
|
||
```bash
|
||
# After defining all SQLAlchemy models:
|
||
alembic revision --autogenerate -m "initial_schema"
|
||
alembic upgrade head
|
||
```
|
||
|
||
### Naming Convention
|
||
|
||
```
|
||
YYYYMMDD_HHMM_short_description.py
|
||
|
||
Examples:
|
||
20260224_1430_initial_schema.py
|
||
20260301_0900_add_vendor_orders.py
|
||
20260310_1100_meeting_change_requests.py
|
||
```
|
||
|
||
### Safe Migration Rules
|
||
|
||
- **Never** rename a column in production — add a new one, backfill, drop the old one in a follow-up migration.
|
||
- **Never** drop a table — soft-delete all rows first, then drop in a follow-up after verification.
|
||
- **Always** make migrations backwards-compatible so a rollback doesn't break the running app.
|
||
|
||
---
|
||
|
||
## 6. Seed Data Strategy
|
||
|
||
The `app/scripts/seed_dev.py` script creates development data that exactly matches the frontend mock data:
|
||
|
||
```python
|
||
SEED_USERS = [
|
||
# Customers
|
||
{"legacy_id": "c1", "email": "alice@example.com", "username": "alice",
|
||
"full_name": "Alice Customer", "role": "CUSTOMER", "user_type": "customer",
|
||
"password": "password"}, # Will be bcrypt-hashed on seed
|
||
|
||
# Field Agents
|
||
{"legacy_id": "e1", "email": "agent1@plano.com", "emp_id": "FA001",
|
||
"full_name": "Frank Agent", "role": "FIELD_AGENT", "user_type": "employee",
|
||
"xp": 12450, "doors_knocked": 342, "streak_days": 5,
|
||
"achievements": ["Hot Spot Hunter", "Storm Chaser"], "password": "password"},
|
||
|
||
# Admins
|
||
{"legacy_id": "a1", "email": "admin@plano.com", "emp_id": "ADM01",
|
||
"full_name": "Adam Admin", "role": "ADMIN", "user_type": "employee",
|
||
"password": "password"},
|
||
|
||
# Owners
|
||
{"legacy_id": "own_001", "email": "justin@johnsondev.com", "username": "justin",
|
||
"full_name": "Justin Johnson", "role": "OWNER", "user_type": "owner",
|
||
"company_name": "Johnson Development Group", "password": "password"},
|
||
|
||
# Contractor
|
||
{"legacy_id": "con_001", "email": "mike@texasbuilders.com", "username": "mike",
|
||
"full_name": "Mike Contractor", "role": "CONTRACTOR", "user_type": "contractor",
|
||
"company_name": "Texas Builders LLC", "license_number": "TX-GC-123456",
|
||
"password": "password"},
|
||
|
||
# Vendor
|
||
{"legacy_id": "ven_001", "email": "sales@abcsupply.com", "username": "abc_supply",
|
||
"full_name": "ABC Supply Co.", "role": "VENDOR", "user_type": "vendor",
|
||
"company_name": "ABC Supply", "trade_type": "materials", "password": "password"},
|
||
|
||
# (and so on for all 17 mock users)
|
||
]
|
||
```
|
||
|
||
**Why `legacy_id` matters:** During the transition period, the frontend will still reference IDs like `'e1'`, `'own_001'`. The backend stores these in `legacy_id` and the Integration Team uses them as lookup keys until the frontend is fully migrated to UUIDs.
|
||
|
||
---
|
||
|
||
## 7. Role-to-Table Access Matrix
|
||
|
||
This is the **policy contract** for the backend services layer. Routers enforce this via `require_role()`.
|
||
|
||
| Table | OWNER | ADMIN | FIELD_AGENT | CONTRACTOR | SUBCONTRACTOR | VENDOR | CUSTOMER |
|
||
|-------|-------|-------|-------------|------------|---------------|--------|----------|
|
||
| `users` | Read all | Read all, Write own team | Read own | Read own | Read own | Read own | Read own |
|
||
| `properties` | Read/Write all | Read/Write all | Read assigned, Write status | None | None | None | Read own |
|
||
| `meetings` | Read all | Read/Write all | Read/Write own | None | None | None | Read own |
|
||
| `projects` | Read/Write own | Read all | None | Read assigned | Read assigned | None | None |
|
||
| `vendors` | Read/Write all | Read all | None | None | None | Read own | None |
|
||
| `vendor_orders` | Read/Write all | Read all | None | None | None | Read own | None |
|
||
| `invoices` | Read/Write all | Read all, Write | None | Read own | Read own | Read own | Read own |
|
||
| `documents` | Read/Write all | Read/Write all | Read assigned | Read assigned | Read assigned | Read own | Read own |
|
||
| `notifications` | Read own | Read own | Read own | Read own | Read own | Read own | Read own |
|
||
| `audit_logs` | Read all | Read all | None | None | None | None | None |
|
||
|
||
---
|
||
|
||
*Next: Read `01_authentication_module.md` to build the auth system on top of this schema.*
|