- 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
15 KiB
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.
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.
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.
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.
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.
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.
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.
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.
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.
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<br/>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<br/>for audit trail but<br/>no longer queried by FE