Files
rentman-clone/.a0/design.md
T

731 lines
28 KiB
Markdown
Raw Normal View History

# 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.