# LLD — Database Schema (B2) **Scope:** PostgreSQL schema — entity relationships, state machines, query patterns, data lifecycle **Companion doc:** `docs/backend/02_database_schema.md` --- ## Diagram 1 — Core Entity Relationship Diagram All 16 tables and their foreign-key relationships. Key fields shown per entity. ```mermaid erDiagram USERS { uuid id PK varchar legacy_id varchar email varchar emp_id varchar username varchar password_hash varchar full_name user_role role int xp int streak_days jsonb achievements timestamptz deleted_at } PROPERTIES { serial id PK varchar property_id varchar address decimal latitude decimal longitude jsonb polygon property_type property_type canvassing_status canvassing_status int estimated_market_value uuid assigned_agent_id FK bool pending_signature int proposal_value bool currently_rented timestamptz deleted_at } PROPERTY_PHOTOS { serial id PK int property_id FK varchar url varchar caption smallint sort_order } MEETINGS { uuid id PK varchar legacy_id uuid agent_id FK uuid customer_id FK int property_id FK date meeting_date time meeting_time meeting_status status int deal_value text notes timestamptz deleted_at } MEETING_CHANGE_REQUESTS { uuid id PK uuid meeting_id FK uuid requested_by FK uuid reviewed_by FK date proposed_date change_request_status status } PROJECTS { uuid id PK uuid owner_id FK int property_id FK varchar title project_status status smallint health_score int approved_budget_cents int actual_cost_cents smallint completion_pct timestamptz deleted_at } PROJECT_TASKS { uuid id PK uuid project_id FK uuid assigned_to FK varchar title task_status status task_priority priority date due_date } CHANGE_ORDERS { uuid id PK uuid project_id FK uuid requested_by FK uuid approved_by FK varchar title int cost_impact_cents change_order_status status } VENDORS { uuid id PK varchar company_name varchar trade_type vendor_status status decimal on_time_delivery_rate date coi_expiry_date bool is_compliant int total_spend_cents timestamptz deleted_at } VENDOR_COMPLIANCE_DOCS { uuid id PK uuid vendor_id FK compliance_doc_type doc_type compliance_doc_status status date expiry_date varchar file_url uuid uploaded_by FK } VENDOR_ORDERS { uuid id PK uuid vendor_id FK uuid project_id FK varchar order_number int total_cents order_status status date expected_date date delivered_date timestamptz deleted_at } INVOICES { uuid id PK varchar invoice_number invoice_type invoice_type uuid issued_by FK uuid billed_to_user FK uuid vendor_id FK uuid project_id FK int total_cents int amount_paid_cents int balance_cents invoice_status status date due_date uuid approved_by FK timestamptz deleted_at } DOCUMENTS { uuid id PK varchar title document_category category document_review_status review_status uuid project_id FK uuid vendor_id FK uuid uploaded_by FK varchar file_url date expiry_date timestamptz deleted_at } SALES_HISTORY { uuid id PK uuid agent_id FK int property_id FK uuid meeting_id FK date closed_date int amount_cents deal_status status } NOTIFICATIONS { uuid id PK uuid user_id FK notification_type type varchar title bool is_read varchar deep_link jsonb metadata } AUDIT_LOGS { bigserial id PK uuid actor_id FK varchar action varchar resource_type varchar resource_id jsonb old_value jsonb new_value inet ip_address } USERS ||--o{ PROPERTIES : "assigned_agent_id" USERS ||--o{ MEETINGS : "agent_id" USERS ||--o{ MEETINGS : "customer_id" USERS ||--o{ PROJECTS : "owner_id" USERS ||--o{ PROJECT_TASKS : "assigned_to" USERS ||--o{ CHANGE_ORDERS : "requested_by" USERS ||--o{ INVOICES : "issued_by" USERS ||--o{ INVOICES : "billed_to_user" USERS ||--o{ SALES_HISTORY : "agent_id" USERS ||--o{ NOTIFICATIONS : "user_id" USERS ||--o{ AUDIT_LOGS : "actor_id" USERS ||--o{ VENDOR_COMPLIANCE_DOCS : "uploaded_by" PROPERTIES ||--o{ PROPERTY_PHOTOS : "property_id" PROPERTIES ||--o{ MEETINGS : "property_id" PROPERTIES ||--o{ PROJECTS : "property_id" PROPERTIES ||--o{ SALES_HISTORY : "property_id" MEETINGS ||--o{ MEETING_CHANGE_REQUESTS : "meeting_id" MEETINGS ||--o{ SALES_HISTORY : "meeting_id" PROJECTS ||--o{ PROJECT_TASKS : "project_id" PROJECTS ||--o{ CHANGE_ORDERS : "project_id" PROJECTS ||--o{ VENDOR_ORDERS : "project_id" PROJECTS ||--o{ INVOICES : "project_id" PROJECTS ||--o{ DOCUMENTS : "project_id" VENDORS ||--o{ VENDOR_COMPLIANCE_DOCS : "vendor_id" VENDORS ||--o{ VENDOR_ORDERS : "vendor_id" VENDORS ||--o{ INVOICES : "vendor_id" VENDORS ||--o{ DOCUMENTS : "vendor_id" ``` --- ## Diagram 2 — Meeting Status State Machine A meeting moves through these states. Only ADMIN/OWNER can force-advance to any state. FIELD_AGENTs can only request changes. ```mermaid stateDiagram-v2 [*] --> Scheduled : POST /meetings\n(ADMIN creates) Scheduled --> Rescheduled : PATCH status=Rescheduled\n(ADMIN approves change request) Rescheduled --> Scheduled : PATCH status=Scheduled\n(new date confirmed) Scheduled --> InProgress : PATCH status=In Progress\n(agent marks on-site) Rescheduled --> InProgress : PATCH status=In Progress InProgress --> Completed : PATCH status=Completed\n+ outcome + notes InProgress --> Cancelled : PATCH status=Cancelled\n+ reason Completed --> Converted : PATCH status=Converted\n+ deal_value (closes deal) Scheduled --> Cancelled : PATCH status=Cancelled Rescheduled --> Cancelled : PATCH status=Cancelled Converted --> [*] : Terminal state\n→ writes to sales_history Cancelled --> [*] : Terminal state note right of Scheduled FIELD_AGENT submits MeetingChangeRequest (cannot directly edit) end note note right of Converted Triggers: sales_history INSERT notification to ADMIN end note ``` --- ## Diagram 3 — Invoice Status State Machine Invoices move through a lifecycle depending on payments, approvals, and time-based triggers. ```mermaid stateDiagram-v2 [*] --> Draft : POST /invoices\n(ADMIN / OWNER creates) Draft --> Sent : PATCH status=Sent\n(email triggered to recipient) Draft --> Void : PATCH status=Void\n(cancel before sending) Sent --> Viewed : Auto on first\nrecipient GET request Viewed --> Partial : PATCH\namount_paid_cents > 0\nbut < total_cents Viewed --> Paid : PATCH\namount_paid_cents = total_cents Partial --> Paid : PATCH\namount_paid_cents = total_cents Partial --> Overdue : Scheduler trigger\ndue_date < TODAY Sent --> Overdue : Scheduler trigger\ndue_date < TODAY Overdue --> Paid : PATCH\namount_paid_cents = total_cents Viewed --> Disputed : PATCH status=Disputed\n+ dispute_reason Partial --> Disputed : PATCH status=Disputed Overdue --> Disputed : PATCH status=Disputed Disputed --> Paid : OWNER resolves dispute Disputed --> Void : OWNER voids invoice Paid --> [*] : Terminal state Void --> [*] : Terminal state note right of Overdue Triggers: notification to OWNER + ADMIN SendGrid email to debtor end note ``` --- ## Diagram 4 — Project Status State Machine Projects managed through the Owner's Box. ```mermaid stateDiagram-v2 [*] --> active : POST /projects\n(OWNER creates) active --> completed : PATCH status=completed\nall tasks done\nfinal invoice paid active --> delayed : Auto-trigger:\ntarget_end_date passed\ncompletion_pct < 100 active --> on_hold : PATCH status=on_hold\n+ reason (OWNER decision) on_hold --> active : PATCH status=active\n(OWNER resumes) active --> disputed : PATCH status=disputed\n+ dispute details\n(OWNER or CONTRACTOR) disputed --> active : Dispute resolved\n(OWNER approves) disputed --> cancelled : Escalated dispute\n(OWNER decision) active --> cancelled : PATCH status=cancelled\n(OWNER decision) delayed --> active : PATCH status=active\n(new timeline set) delayed --> cancelled : PATCH status=cancelled completed --> [*] : Terminal state cancelled --> [*] : Terminal state note right of delayed Triggers: notification to OWNER health_score recalculated end note note right of disputed Triggers: notification to CONTRACTOR change_orders locked end note ``` --- ## Diagram 5 — Vendor Compliance Status Machine Compliance documents transition based on expiry dates. A scheduler runs daily to check all docs. ```mermaid stateDiagram-v2 [*] --> Active : Document uploaded\n(expiry_date in future) Active --> ExpiringSoon : Scheduler:\nexpiry_date ≤ TODAY + 30 days Active --> Expired : Scheduler:\nexpiry_date < TODAY Active --> Missing : Manual flag\nor doc deleted ExpiringSoon --> Active : New document uploaded\nwith future expiry_date ExpiringSoon --> Expired : Scheduler:\nexpiry_date < TODAY Expired --> Active : Replacement doc uploaded Missing --> Active : Doc uploaded note right of ExpiringSoon Triggers at 30, 14, 7 days: notification to OWNER email to vendor contact vendor.is_compliant = false end note note right of Expired Triggers immediately: urgent notification to OWNER vendor.is_compliant = false vendor orders may be blocked end note ``` --- ## Diagram 6 — Soft Delete Pattern How "deletion" works across all tables. No row is ever hard-deleted in production. ```mermaid flowchart TD REQ["DELETE /properties/42\nor\nDELETE /vendors/uuid"] --> AUTH_CHECK["Auth: require_role\nADMIN or OWNER"] AUTH_CHECK --> SOFT_DEL["UPDATE table\nSET deleted_at = NOW()\nWHERE id = ?"] SOFT_DEL --> AUDIT["INSERT audit_logs\nactor_id, action=resource.deleted\nold_value = full row snapshot"] SOFT_DEL --> NOTIFY["Trigger dependent cleanup:\n• Unassign agent from properties\n• Cancel pending meetings\n• Flag open invoices"] subgraph AllQueries["All Application Queries"] Q1["SELECT * FROM properties\nWHERE deleted_at IS NULL"] Q2["SELECT * FROM meetings\nWHERE deleted_at IS NULL"] Q3["SELECT * FROM vendors\nWHERE deleted_at IS NULL"] end SOFT_DEL -.->|"row hidden from"| AllQueries subgraph AdminOnly["Admin / Recovery Queries"] Q_ADMIN["SELECT * FROM properties\nWHERE deleted_at IS NOT NULL\n(recovery / audit only)"] end ``` --- ## Diagram 7 — Audit Log Write Pattern Which operations write to `audit_logs` and what data is captured. ```mermaid flowchart LR subgraph Triggers["Write Operations That Trigger Audit Log"] P1["Property:\nassign_agent\nchange_status\npending_signature"] P2["Meeting:\nstatus change\nchange_request approved"] P3["Invoice:\ncreated\napproved\nvoided"] P4["User:\nrole changed\ndeactivated"] P5["Project:\ncreated\nstatus changed\nchange_order approved"] P6["Chatbot:\nquery with sensitive role context"] end subgraph AuditLog["audit_logs INSERT"] AL["{\n actor_id: user.id,\n action: 'property.agent_assigned',\n resource_type: 'property',\n resource_id: '42',\n old_value: { assigned_agent_id: null },\n new_value: { assigned_agent_id: 'uuid' },\n ip_address: '1.2.3.4',\n created_at: NOW()\n}"] end P1 & P2 & P3 & P4 & P5 & P6 --> AL AL --> QUERY["OWNER / ADMIN\nGET /audit-logs?resource_type=property\n&resource_id=42"] ``` --- ## Diagram 8 — Key Query Patterns by Role How the ORM service layer scopes queries differently per role. ```mermaid flowchart TD subgraph PropertiesQuery["GET /properties — Role-Scoped Query"] PQ_START["Request arrives\nwith current_user"] PQ_START --> PQ_ROLE{current_user.role} PQ_ROLE -->|"OWNER / ADMIN"| PQ_ALL["SELECT * FROM properties\nWHERE deleted_at IS NULL\n+ any filters"] PQ_ROLE -->|"FIELD_AGENT"| PQ_AGENT["SELECT * FROM properties\nWHERE assigned_agent_id = current_user.id\nAND deleted_at IS NULL"] PQ_ROLE -->|"CUSTOMER"| PQ_CUS["SELECT * FROM properties\nWHERE property_id = current_user.property_id\nAND deleted_at IS NULL\nLIMIT 1"] end subgraph MeetingsQuery["GET /meetings — Role-Scoped Query"] MQ_START["Request arrives"] MQ_START --> MQ_ROLE{current_user.role} MQ_ROLE -->|"OWNER / ADMIN"| MQ_ALL["SELECT * FROM meetings\nWHERE deleted_at IS NULL"] MQ_ROLE -->|"FIELD_AGENT"| MQ_AGENT["SELECT * FROM meetings\nWHERE agent_id = current_user.id\nAND deleted_at IS NULL"] MQ_ROLE -->|"CUSTOMER"| MQ_CUS["SELECT * FROM meetings\nWHERE customer_id = current_user.id\nAND deleted_at IS NULL"] end subgraph InvoiceQuery["GET /invoices — Role-Scoped Query"] IQ_START["Request arrives"] IQ_START --> IQ_ROLE{current_user.role} IQ_ROLE -->|"OWNER"| IQ_ALL["SELECT * FROM invoices\nWHERE deleted_at IS NULL\n(full financial view)"] IQ_ROLE -->|"ADMIN"| IQ_ADM["SELECT * FROM invoices\nWHERE deleted_at IS NULL\nAND invoice_type != 'internal'"] IQ_ROLE -->|"CONTRACTOR / SUBCONTRACTOR"| IQ_CON["SELECT * FROM invoices\nWHERE billed_to_user = current_user.id\nOR issued_by = current_user.id"] IQ_ROLE -->|"VENDOR"| IQ_VEN["SELECT * FROM invoices\nWHERE vendor_id IN\n (SELECT id FROM vendors\n WHERE contact_user_id = current_user.id)"] end ``` --- ## Diagram 9 — Legacy ID Migration Path How the `legacy_id` column bridges the mock frontend IDs (`'e1'`, `'own_001'`) to real UUIDs during the transition period. ```mermaid sequenceDiagram participant FE as React Frontend participant API as FastAPI participant DB as PostgreSQL Note over FE,DB: ── Transition Phase (Integration Team replacing mock calls) ── FE->>API: GET /users/by-legacy-id/e1 API->>DB: SELECT * FROM users WHERE legacy_id = 'e1' DB-->>API: { id: "3f8a...(uuid)", legacy_id: "e1", ... } API-->>FE: User object with real UUID Note over FE: Frontend stores UUID in state
All subsequent calls use UUID FE->>API: GET /meetings?agent_id=3f8a...(uuid) API->>DB: SELECT * FROM meetings WHERE agent_id = '3f8a...' DB-->>API: Meeting rows API-->>FE: Meetings data Note over FE,DB: ── Post-Migration Phase (legacy_id no longer needed) ── Note over DB: legacy_id column remains
for audit trail but
no longer queried by FE ```