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
528 lines
15 KiB
Markdown
528 lines
15 KiB
Markdown
# 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<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
|
|
```
|