7f7da15965
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
28 KiB
28 KiB
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) |
| 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_idonaccount_idix_user_emailUNIQUE onemail
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_idonaccount_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_idonaccount_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 |
| 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_idonaccount_idix_contact_typeontype
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_idonaccount_idix_document_project_idonproject_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_idonaccount_idix_project_statusonstatusix_project_planperiodonplanperiod_start,planperiod_end(for availability queries)ix_project_customer_idoncustomer_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_idonproject_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_idonproject_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_idonproject_idix_projectfunction_group_idongroup_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_idonproject_idix_projectcrew_function_idonfunction_idix_projectcrew_crew_idoncrew_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_idonproject_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_idonequipment_group_idix_projectequipment_equipment_idonequipment_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_idonaccount_idix_equipment_categoryoncategoryix_equipment_statusonstatusix_equipment_barcodeUNIQUE onbarcodeix_equipment_qrcodeUNIQUE onqr_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_idonaccount_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 |
| 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_idonaccount_idix_crew_typeontypeix_crew_is_activeonis_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_idoncrew_idix_crewavailability_periodonstart,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_idonaccount_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_idonvehicle_idix_vehicleassignment_project_idonproject_idix_vehicleassignment_periodonstart,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_idonaccount_idix_projectrequest_statusonstatus
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_idonproject_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_idonaccount_idix_quote_project_idonproject_idix_quote_statusonstatus
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_idonaccount_idix_invoice_project_idonproject_idix_invoice_statusonstatus
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_idonaccount_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_idonaccount_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_idonwebhook_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_idonaccount_idix_auditlog_user_idonuser_idix_auditlog_entityonentity_type,entity_idix_auditlog_timestampontimestamp
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_itemswith columnsequipment_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
emailglobally; Equipmentbarcodeandqr_codeper 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.