Files
rentman-clone/.a0/design.md
T
Agent Zero 7f7da15965 Initial commit: Rentman Clone - Phase 0-6 (T001-T023)
Completed:
- Phase 0: Project Setup (T001-T003) - Docker Compose, FastAPI skeleton, React SPA
- Phase 1: Auth System (T004-T008) - DB models, JWT auth, RBAC middleware, user management
- Phase 2: Contacts & Tags (T009-T011) - CRUD API + UI
- Phase 3: Equipment Catalog (T012-T014) - Models, API, UI with barcode/QR
- Phase 4: Crew Management (T015-T017) - Models, availability, UI
- Phase 5: Vehicle Fleet (T018-T020) - Models, assignments, UI
- Phase 6: Projects (T021-T023) - Project hierarchy models, CRUD API, list/detail UI
2026-05-31 20:36:42 +00:00

731 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Database Design Rentman.io Clone
> **Version:** 1.0
> **Date:** 2026-05-31
> **Project:** Rentman.io-Nachbau
> **Architect:** Solution Architect (A0 Software Orchestrator)
---
## 1. Entity-Relationship Diagram (Textual)
```
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Account │ │ User │ │ Role │
│ (tenant) │ │ │ │ │
│ id (PK) │1───*│ id (PK) │*───*│ id (PK) │
│ name │ │ account_id (FK) │ │ name │
│ created_at │ │ email │ │ permissions (JSON│
└──────────────────┘ │ full_name │ └──────────────────┘
│ password_hash │
│ role_id (FK) │
└────────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Tag │ │ Contact │ │ Document │
│ id (PK) │*───*│ id (PK) │1───*│ id (PK) │
│ name │ │ account_id (FK) │ │ account_id (FK) │
│ color │ │ type │ │ name │
└──────────────────┘ │ company_name │ │ file_path │
│ first_name │ │ mime_type │
│ last_name │ │ size_bytes │
│ email, phone, mobile│ │ project_id (FK) │
│ street, number, │ │ uploaded_by (FK) │
│ postalcode, city, │ │ created_at │
│ country, tax_number│ └──────────────────┘
│ note │
└───────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Project │ │ Subproject │ │ ProjectFunctionGroup│
│ id (PK) │1───*│ id (PK) │1───*│ id (PK) │
│ account_id (FK) │ │ project_id (FK) │ │ project_id (FK) │
│ name │ │ name │ │ subproject_id (FK)│
│ reference │ │ custom_fields (JSON)│ │ name │
│ number │ └──────────────────┘ │ usageperiod_start/end│
│ customer_id (FK) │ │ planperiod_start/end│
│ contact_person_id │ │ remark │
│ account_manager_id│ └──────┬───────────┘
│ project_type │ │
│ planperiod_start/end│ │
│ usageperiod_start/end│ │
│ status │ ┌──────▼──────────┐
│ location_* │ │ ProjectFunction │
│ remark │ │ id (PK) │
│ price, cost │ │ project_id (FK) │
│ created_at, updated_at│ │ subproject_id (FK)│
│ archived │ │ group_id (FK) │
└──────┬────────────┘ │ type (enum) │
│ │ amount, usg/plan│
│ │ cost_rate, price_rate│
│ │ remark │
│ └──────┬──────────┘
│ │
│ ┌──────▼──────────┐
├───────────────────────────────────────────►│ ProjectCrew │
│ │ id (PK) │
│ ProjectEquipmentGroup │ project_id (FK) │
├───────────────────────────────────────────►│ function_id (FK) │
│ │ crew_id (FK) │
│ ProjectEquipment (within group) │ start, end │
│ └──────────────────┘
├──► Quote
│ (id, project_id, number, status, valid_until,
│ template, content_html, total_net, total_gross, created_at)
├──► Invoice
│ (id, project_id, number, status, due_date,
│ total_net, total_gross, created_at)
├──► ProjectRequest (id, name, contact fields, location, periods, status, remark)
│ │
│ └──► ProjectRequestEquipment (id, request_id, name, quantity, unit_price, linked_equipment_id)
└──► VehicleAssignment
(id, vehicle_id, project_id, start, end, driver_id)
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Equipment │ │ Crew │ │ Vehicle │
│ id (PK) │ │ id (PK) │ │ id (PK) │
│ account_id (FK) │ │ account_id (FK) │ │ account_id (FK) │
│ name │ │ first_name │ │ name │
│ category │ │ last_name │ │ license_plate │
│ serial_number │ │ email, phone │ │ type │
│ barcode, qr_code │ │ type (internal/ │ │ payload_kg │
│ stock_location_id │ │ freelancer) │ │ volume_m3 │
│ status │ │ hourly_rate │ │ is_active │
│ purchase_price │ │ daily_rate │ └──────────────────┘
│ current_value │ │ skills (JSON) │
│ weight_kg, dims, │ │ note │
│ power_watt, notes │ │ user_id (FK) │
│ custom_fields, img│ │ is_active │
└──────────────────┘ └────────┬──────────┘
┌──────────────────┐ ┌────────▼──────────┐
│ EquipmentGroup │ │ CrewAvailability │
│ (Bundle) │ │ id (PK) │
│ id (PK) │ │ crew_id (FK) │
│ account_id (FK) │ │ start, end │
│ name │ │ type (avail/unavail│
│ items (JSON/M2M) │ │ reason │
└──────────────────┘ └───────────────────┘
┌───────────────────┐
│ StockLocation │
│ id (PK) │
│ account_id (FK) │
│ name │
│ address │
│ is_default │
└───────────────────┘
┌───────────────────┐
│ AuditLog │
│ id (PK) │
│ account_id (FK) │
│ user_id (FK) │
│ action │
│ entity_type │
│ entity_id │
│ details (JSON) │
│ timestamp │
└───────────────────┘
```
## 2. Table Definitions
### 2.1 Account
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PRIMARY KEY, DEFAULT gen_random_uuid() (on PG) |
| name | VARCHAR(255) | NOT NULL |
| created_at | TIMESTAMP WITH TIME ZONE | NOT NULL, DEFAULT now() |
**Indexes:** None extra beyond PK.
---
### 2.2 User
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| email | VARCHAR(255) | NOT NULL, UNIQUE |
| full_name | VARCHAR(255) | NOT NULL |
| password_hash | VARCHAR(255) | NOT NULL |
| role_id | UUID | NOT NULL, FK → Role(id) |
| is_active | BOOLEAN | NOT NULL, DEFAULT TRUE |
| created_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() |
| updated_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() |
**Indexes:**
- `ix_user_account_id` on `account_id`
- `ix_user_email` UNIQUE on `email`
---
### 2.3 Role
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| name | VARCHAR(100) | NOT NULL |
| permissions | JSONB | NOT NULL (array of strings like `["projects:read", "projects:write"]`) |
**Indexes:**
- `ix_role_account_id` on `account_id`
---
### 2.4 Tag
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| name | VARCHAR(100) | NOT NULL |
| color | VARCHAR(7) | NULLABLE (hex color) |
**Indexes:**
- `ix_tag_account_id` on `account_id`
**Relationships:**
- Many-to-Many with Equipment, Project, Crew (via join tables: `equipment_tags`, `project_tags`, `crew_tags`).
---
### 2.5 Contact
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| type | ENUM('company','person') | NOT NULL |
| company_name | VARCHAR(255) | NULLABLE |
| first_name | VARCHAR(100) | NULLABLE |
| last_name | VARCHAR(100) | NULLABLE |
| email | VARCHAR(255) | NULLABLE |
| phone | VARCHAR(50) | NULLABLE |
| mobile | VARCHAR(50) | NULLABLE |
| street | VARCHAR(255) | NULLABLE |
| number | VARCHAR(20) | NULLABLE |
| postalcode | VARCHAR(20) | NULLABLE |
| city | VARCHAR(100) | NULLABLE |
| country | VARCHAR(100) | NULLABLE |
| tax_number | VARCHAR(50) | NULLABLE |
| note | TEXT | NULLABLE |
**Indexes:**
- `ix_contact_account_id` on `account_id`
- `ix_contact_type` on `type`
---
### 2.6 Document
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| name | VARCHAR(255) | NOT NULL |
| original_filename | VARCHAR(255) | NOT NULL |
| file_path | VARCHAR(1024) | NOT NULL (relative to storage root) |
| mime_type | VARCHAR(127) | NOT NULL |
| size_bytes | INTEGER | NOT NULL |
| project_id | UUID | NULLABLE, FK → Project(id) |
| uploaded_by | UUID | NOT NULL, FK → User(id) |
| created_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() |
**Indexes:**
- `ix_document_account_id` on `account_id`
- `ix_document_project_id` on `project_id`
---
### 2.7 Project
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| name | VARCHAR(255) | NOT NULL |
| reference | VARCHAR(50) | NULLABLE |
| number | VARCHAR(50) | NOT NULL, automatically generated (prefix + sequential) |
| displayname | VARCHAR(512) | GENERATED (name + number) |
| customer_id | UUID | NULLABLE, FK → Contact(id) |
| contact_person_id | UUID | NULLABLE, FK → Contact(id) |
| account_manager_id | UUID | NULLABLE, FK → User(id) |
| project_type | ENUM('rental','service','sale') | NULLABLE |
| planperiod_start | TIMESTAMPTZ | NOT NULL |
| planperiod_end | TIMESTAMPTZ | NOT NULL |
| usageperiod_start | TIMESTAMPTZ | NOT NULL |
| usageperiod_end | TIMESTAMPTZ | NOT NULL |
| status | ENUM('request','planned','confirmed','active','completed','cancelled') | DEFAULT 'planned' |
| location_name | VARCHAR(255) | NULLABLE |
| location_street | VARCHAR(255) | NULLABLE |
| location_city | VARCHAR(100) | NULLABLE |
| location_postalcode | VARCHAR(20) | NULLABLE |
| location_country | VARCHAR(100) | NULLABLE |
| remark | TEXT | NULLABLE |
| price | DECIMAL(12,2) | NULLABLE (calculated field, updated by triggers) |
| cost | DECIMAL(12,2) | NULLABLE |
| created_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() |
| updated_at | TIMESTAMPTZ | NOT NULL, DEFAULT now() |
| archived | BOOLEAN | NOT NULL, DEFAULT FALSE |
**Indexes:**
- `ix_project_account_id` on `account_id`
- `ix_project_status` on `status`
- `ix_project_planperiod` on `planperiod_start`, `planperiod_end` (for availability queries)
- `ix_project_customer_id` on `customer_id`
---
### 2.8 Subproject
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| project_id | UUID | NOT NULL, FK → Project(id) ON DELETE CASCADE |
| name | VARCHAR(255) | NOT NULL |
| custom_fields | JSONB | NULLABLE |
**Indexes:**
- `ix_subproject_project_id` on `project_id`
---
### 2.9 ProjectFunctionGroup
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| project_id | UUID | NOT NULL, FK → Project(id) ON DELETE CASCADE |
| subproject_id | UUID | NULLABLE, FK → Subproject(id) |
| name | VARCHAR(255) | NOT NULL |
| usageperiod_start | TIMESTAMPTZ | NULLABLE |
| usageperiod_end | TIMESTAMPTZ | NULLABLE |
| planperiod_start | TIMESTAMPTZ | NULLABLE |
| planperiod_end | TIMESTAMPTZ | NULLABLE |
| remark | TEXT | NULLABLE |
**Indexes:**
- `ix_pfg_project_id` on `project_id`
---
### 2.10 ProjectFunction
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| project_id | UUID | NOT NULL, FK → Project(id) ON DELETE CASCADE |
| subproject_id | UUID | NULLABLE, FK → Subproject(id) |
| group_id | UUID | NULLABLE, FK → ProjectFunctionGroup(id) |
| name | VARCHAR(255) | NOT NULL |
| type | ENUM('crew_function','equipment_function','transport_function') | NOT NULL |
| amount | INTEGER | DEFAULT 1 |
| usageperiod_start | TIMESTAMPTZ | NULLABLE |
| usageperiod_end | TIMESTAMPTZ | NULLABLE |
| planperiod_start | TIMESTAMPTZ | NULLABLE |
| planperiod_end | TIMESTAMPTZ | NULLABLE |
| cost_rate | DECIMAL(10,2) | NULLABLE |
| price_rate | DECIMAL(10,2) | NULLABLE |
| remark | TEXT | NULLABLE |
**Indexes:**
- `ix_projectfunction_project_id` on `project_id`
- `ix_projectfunction_group_id` on `group_id`
---
### 2.11 ProjectCrew
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| project_id | UUID | NOT NULL, FK → Project(id) |
| function_id | UUID | NOT NULL, FK → ProjectFunction(id) ON DELETE CASCADE |
| crew_id | UUID | NOT NULL, FK → Crew(id) |
| start | TIMESTAMPTZ | NULLABLE (can default to project usage period) |
| end | TIMESTAMPTZ | NULLABLE |
**Indexes:**
- `ix_projectcrew_project_id` on `project_id`
- `ix_projectcrew_function_id` on `function_id`
- `ix_projectcrew_crew_id` on `crew_id`
---
### 2.12 ProjectEquipmentGroup
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| project_id | UUID | NOT NULL, FK → Project(id) ON DELETE CASCADE |
| subproject_id | UUID | NULLABLE, FK → Subproject(id) |
| name | VARCHAR(255) | NOT NULL |
| usageperiod_start | TIMESTAMPTZ | NULLABLE |
| usageperiod_end | TIMESTAMPTZ | NULLABLE |
| planperiod_start | TIMESTAMPTZ | NULLABLE |
| planperiod_end | TIMESTAMPTZ | NULLABLE |
| in_price_calculation | BOOLEAN | DEFAULT TRUE |
| remark | TEXT | NULLABLE |
**Indexes:**
- `ix_peg_project_id` on `project_id`
---
### 2.13 ProjectEquipment
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| project_id | UUID | NOT NULL, FK → Project(id) |
| equipment_group_id | UUID | NOT NULL, FK → ProjectEquipmentGroup(id) ON DELETE CASCADE |
| equipment_id | UUID | NULLABLE, FK → Equipment(id) |
| name | VARCHAR(255) | NOT NULL (can be free-text if equipment_id is NULL) |
| quantity | INTEGER | NOT NULL, DEFAULT 1 |
| quantity_total | INTEGER | NULLABLE (available inventory count) |
| unit_price | DECIMAL(10,2) | NULLABLE |
| discount | DECIMAL(5,2) | NULLABLE (percentage) |
| external_remark | TEXT | NULLABLE |
| internal_remark | TEXT | NULLABLE |
**Indexes:**
- `ix_projectequipment_group_id` on `equipment_group_id`
- `ix_projectequipment_equipment_id` on `equipment_id`
---
### 2.14 Equipment (Catalog)
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| name | VARCHAR(255) | NOT NULL |
| category | VARCHAR(100) | NULLABLE |
| brand | VARCHAR(100) | NULLABLE |
| model | VARCHAR(100) | NULLABLE |
| serial_number | VARCHAR(100) | NULLABLE |
| barcode | VARCHAR(100) | NULLABLE, UNIQUE |
| qr_code | VARCHAR(100) | NULLABLE, UNIQUE |
| stock_location_id | UUID | NULLABLE, FK → StockLocation(id) |
| status | ENUM('available','maintenance','defect','retired') | DEFAULT 'available' |
| purchase_price | DECIMAL(10,2) | NULLABLE |
| current_value | DECIMAL(10,2) | NULLABLE |
| weight_kg | DECIMAL(10,2) | NULLABLE |
| dimensions_cm | VARCHAR(50) | NULLABLE |
| power_watt | INTEGER | NULLABLE |
| custom_fields | JSONB | NULLABLE |
| notes | TEXT | NULLABLE |
| image_url | VARCHAR(2048) | NULLABLE |
**Indexes:**
- `ix_equipment_account_id` on `account_id`
- `ix_equipment_category` on `category`
- `ix_equipment_status` on `status`
- `ix_equipment_barcode` UNIQUE on `barcode`
- `ix_equipment_qrcode` UNIQUE on `qr_code`
---
### 2.15 StockLocation
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| name | VARCHAR(100) | NOT NULL |
| address | VARCHAR(255) | NULLABLE |
| is_default | BOOLEAN | DEFAULT FALSE |
**Indexes:**
- `ix_stocklocation_account_id` on `account_id`
---
### 2.16 Crew
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| first_name | VARCHAR(100) | NOT NULL |
| last_name | VARCHAR(100) | NOT NULL |
| email | VARCHAR(255) | NULLABLE |
| phone | VARCHAR(50) | NULLABLE |
| type | ENUM('internal','freelancer') | NOT NULL |
| user_id | UUID | NULLABLE, FK → User(id) (if they have login) |
| hourly_rate | DECIMAL(10,2) | NULLABLE |
| daily_rate | DECIMAL(10,2) | NULLABLE |
| skills | JSONB | NULLABLE (array of strings) |
| is_active | BOOLEAN | DEFAULT TRUE |
| note | TEXT | NULLABLE |
**Indexes:**
- `ix_crew_account_id` on `account_id`
- `ix_crew_type` on `type`
- `ix_crew_is_active` on `is_active`
---
### 2.17 CrewAvailability
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| crew_id | UUID | NOT NULL, FK → Crew(id) ON DELETE CASCADE |
| start | TIMESTAMPTZ | NOT NULL |
| end | TIMESTAMPTZ | NOT NULL |
| type | ENUM('available','unavailable','tentative') | DEFAULT 'unavailable' |
| reason | VARCHAR(255) | NULLABLE |
**Indexes:**
- `ix_crewavailability_crew_id` on `crew_id`
- `ix_crewavailability_period` on `start`, `end` (for overlap checks)
---
### 2.18 Vehicle
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| name | VARCHAR(100) | NOT NULL |
| license_plate | VARCHAR(20) | NULLABLE |
| type | VARCHAR(50) | NULLABLE |
| payload_kg | INTEGER | NULLABLE |
| volume_m3 | DECIMAL(10,2) | NULLABLE |
| is_active | BOOLEAN | DEFAULT TRUE |
**Indexes:**
- `ix_vehicle_account_id` on `account_id`
---
### 2.19 VehicleAssignment
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| vehicle_id | UUID | NOT NULL, FK → Vehicle(id) |
| project_id | UUID | NOT NULL, FK → Project(id) |
| start | TIMESTAMPTZ | NOT NULL |
| end | TIMESTAMPTZ | NOT NULL |
| driver_id | UUID | NULLABLE, FK → Crew(id) |
**Indexes:**
- `ix_vehicleassignment_vehicle_id` on `vehicle_id`
- `ix_vehicleassignment_project_id` on `project_id`
- `ix_vehicleassignment_period` on `start`, `end`
---
### 2.20 ProjectRequest
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| name | VARCHAR(255) | NOT NULL |
| contact_name | VARCHAR(255) | NULLABLE |
| contact_person_first_name | VARCHAR(100) | NULLABLE |
| contact_person_lastname | VARCHAR(100) | NULLABLE |
| contact_person_email | VARCHAR(255) | NULLABLE |
| location_name | VARCHAR(255) | NULLABLE |
| location_mailing_street | VARCHAR(255) | NULLABLE |
| location_mailing_number | VARCHAR(20) | NULLABLE |
| location_mailing_postalcode | VARCHAR(20) | NULLABLE |
| location_mailing_city | VARCHAR(100) | NULLABLE |
| location_mailing_country | VARCHAR(100) | NULLABLE |
| planperiod_start | TIMESTAMPTZ | NULLABLE |
| planperiod_end | TIMESTAMPTZ | NULLABLE |
| usageperiod_start | TIMESTAMPTZ | NULLABLE |
| usageperiod_end | TIMESTAMPTZ | NULLABLE |
| status | ENUM('new','processing','converted','rejected') | DEFAULT 'new' |
| remark | TEXT | NULLABLE |
| created_at | TIMESTAMPTZ | DEFAULT now() |
**Indexes:**
- `ix_projectrequest_account_id` on `account_id`
- `ix_projectrequest_status` on `status`
---
### 2.21 ProjectRequestEquipment
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| project_request_id | UUID | NOT NULL, FK → ProjectRequest(id) ON DELETE CASCADE |
| name | VARCHAR(255) | NOT NULL |
| quantity | INTEGER | NOT NULL |
| quantity_total | INTEGER | NULLABLE |
| unit_price | DECIMAL(10,2) | NULLABLE |
| linked_equipment_id | UUID | NULLABLE, FK → Equipment(id) |
**Indexes:**
- `ix_prequestequip_request_id` on `project_request_id`
---
### 2.22 Quote
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| project_id | UUID | NOT NULL, FK → Project(id) |
| number | VARCHAR(50) | NOT NULL (auto-generated) |
| status | ENUM('draft','sent','accepted','rejected') | DEFAULT 'draft' |
| valid_until | DATE | NULLABLE |
| template | VARCHAR(50) | NULLABLE |
| content_html | TEXT | NULLABLE (generated PDF content) |
| total_net | DECIMAL(12,2) | NULLABLE |
| total_gross | DECIMAL(12,2) | NULLABLE |
| created_at | TIMESTAMPTZ | DEFAULT now() |
**Indexes:**
- `ix_quote_account_id` on `account_id`
- `ix_quote_project_id` on `project_id`
- `ix_quote_status` on `status`
---
### 2.23 Invoice
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| project_id | UUID | NOT NULL, FK → Project(id) |
| number | VARCHAR(50) | NOT NULL (auto-generated) |
| status | ENUM('draft','sent','paid','overdue','cancelled') | DEFAULT 'draft' |
| due_date | DATE | NULLABLE |
| total_net | DECIMAL(12,2) | NULLABLE |
| total_gross | DECIMAL(12,2) | NULLABLE |
| created_at | TIMESTAMPTZ | DEFAULT now() |
**Indexes:**
- `ix_invoice_account_id` on `account_id`
- `ix_invoice_project_id` on `project_id`
- `ix_invoice_status` on `status`
---
### 2.24 Template
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| type | ENUM('quote','invoice','packing_list','delivery_note') | NOT NULL |
| name | VARCHAR(100) | NOT NULL |
| content_html | TEXT | NOT NULL |
| is_default | BOOLEAN | DEFAULT FALSE |
**Indexes:**
- `ix_template_account_id` on `account_id`
---
### 2.25 Webhook
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| url | VARCHAR(2048) | NOT NULL |
| secret | VARCHAR(255) | NOT NULL |
| events | JSONB | NOT NULL (array of event names) |
| is_active | BOOLEAN | DEFAULT TRUE |
**Indexes:**
- `ix_webhook_account_id` on `account_id`
---
### 2.26 WebhookLog
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| webhook_id | UUID | NOT NULL, FK → Webhook(id) ON DELETE CASCADE |
| event | VARCHAR(100) | NOT NULL |
| request_body | JSONB | NOT NULL |
| response_status | INTEGER | NOT NULL |
| response_body | TEXT | NULLABLE |
| success | BOOLEAN | NOT NULL |
| created_at | TIMESTAMPTZ | DEFAULT now() |
**Indexes:**
- `ix_webhooklog_webhook_id` on `webhook_id`
---
### 2.27 AuditLog
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| user_id | UUID | NOT NULL, FK → User(id) |
| action | VARCHAR(50) | NOT NULL (e.g., 'create', 'update', 'delete') |
| entity_type | VARCHAR(50) | NOT NULL (e.g., 'project', 'equipment') |
| entity_id | UUID | NOT NULL |
| details | JSONB | NULLABLE (changed fields) |
| timestamp | TIMESTAMPTZ | NOT NULL, DEFAULT now() |
**Indexes:**
- `ix_auditlog_account_id` on `account_id`
- `ix_auditlog_user_id` on `user_id`
- `ix_auditlog_entity` on `entity_type`, `entity_id`
- `ix_auditlog_timestamp` on `timestamp`
---
### 2.28 EquipmentGroup (Bundle)
| Column | Type | Constraints |
|--------|------|-------------|
| id | UUID | PK |
| account_id | UUID | NOT NULL, FK → Account(id) |
| name | VARCHAR(255) | NOT NULL |
**Relationships:**
- Items stored in junction table `equipment_group_items` with columns `equipment_group_id (FK)`, `equipment_id (FK)`, `quantity` (INTEGER).
---
## 3. Junction / Many-to-Many Tables
### 3.1 EquipmentGroupItems
| Column | Type | Constraints |
|--------|------|-------------|
| equipment_group_id | UUID | NOT NULL, FK → EquipmentGroup(id) ON DELETE CASCADE |
| equipment_id | UUID | NOT NULL, FK → Equipment(id) ON DELETE CASCADE |
| quantity | INTEGER | NOT NULL |
| PRIMARY KEY (equipment_group_id, equipment_id) |
### 3.2 EquipmentTags
| Column | Type | Constraints |
|--------|------|-------------|
| equipment_id | UUID | NOT NULL, FK → Equipment(id) ON DELETE CASCADE |
| tag_id | UUID | NOT NULL, FK → Tag(id) ON DELETE CASCADE |
| PRIMARY KEY (equipment_id, tag_id) |
### 3.3 ProjectTags
| Column | Type | Constraints |
|--------|------|-------------|
| project_id | UUID | NOT NULL, FK → Project(id) ON DELETE CASCADE |
| tag_id | UUID | NOT NULL, FK → Tag(id) ON DELETE CASCADE |
| PRIMARY KEY (project_id, tag_id) |
### 3.4 CrewTags
| Column | Type | Constraints |
|--------|------|-------------|
| crew_id | UUID | NOT NULL, FK → Crew(id) ON DELETE CASCADE |
| tag_id | UUID | NOT NULL, FK → Tag(id) ON DELETE CASCADE |
| PRIMARY KEY (crew_id, tag_id) |
---
## 4. Migration Strategy
We use Alembic for database migrations. The migration environment is configured with `async` support for SQLAlchemy (since our backend uses async sessions).
Initial migration creates all tables with UUID generation appropriate to the dialect:
- PostgreSQL: `server_default=func.gen_random_uuid()`
- SQLite: a Python side UUID generation on insert (via model default)
For development, SQLite will be used; however, tests should run against PostgreSQL to catch dialect-specific issues early.
---
## 5. Key Database Constraints
- **Unique constraints:** User `email` globally; Equipment `barcode` and `qr_code` per account (not globally, since multi-tenant). We'll make `(account_id, barcode)` unique if needed.
- **Foreign key cascades:** Sub-objects (subprojects, functions, equipment groups, crew assignments) should be deleted when the parent project is deleted (CASCADE). AuditLog and other history are kept (NO ACTION or SET NULL).
- **Check constraints:** Project `planperiod_end >= planperiod_start`, etc.
- **Default values:** Most ENUMs have a sensible default.