Deliveries¶
Status: Specified — ready to build Build order: 9 of 10 — depends on Projects & Sites
Purpose¶
Deliveries is the materials intake log for a site. Each delivery is recorded at the point of receipt — what was delivered, by whom, in what condition, and against which order or reference. Photos of the delivery (including any damage) are attached at the time of receipt. The log gives the project a complete record of materials arriving on site and supports claims under NEC compensation events where delivery conditions are disputed.
Entities¶
Delivery¶
One record per delivery event at a site.
| Field | Type | Required | Notes |
|---|---|---|---|
| id | UUID | System | PK, generated |
| site_id | UUID | ✓ | FK → sites.id |
| project_id | UUID | ✓ | Denormalised |
| reference | VARCHAR(100) | Delivery note / order reference / docket number | |
| delivered_at | TIMESTAMPTZ | ✓ | Date and time of delivery |
| supplier | VARCHAR(255) | ✓ | Supplier or haulier name |
| description | TEXT | ✓ | Materials delivered — free text narrative |
| quantity | VARCHAR(100) | Quantity with unit (e.g. 24 m³, 12 tonnes, 1 pallet) |
|
| condition | VARCHAR(20) | ✓ | See values below — default: acceptable |
| condition_notes | TEXT | Required if condition is damaged or rejected |
|
| received_by | UUID | ✓ | FK → users.id — the user recording the delivery |
| photo_count | INTEGER | System | Denormalised count; updated on photo add/delete |
| custom_fields | JSONB | ✓ | Default {} |
| created_at | TIMESTAMPTZ | System | |
| updated_at | TIMESTAMPTZ | System | Trigger-maintained |
| created_by | UUID | System | FK → users.id |
| updated_by | UUID | System | |
| deleted_at | TIMESTAMPTZ |
CREATE INDEX idx_deliveries_site_id
ON deliveries(site_id, delivered_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_deliveries_project_id
ON deliveries(project_id, delivered_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_deliveries_condition
ON deliveries(site_id, condition) WHERE deleted_at IS NULL;
Condition values:
| Value | Meaning |
|---|---|
acceptable |
Delivery received in good condition |
damaged |
Delivery accepted but damage noted — condition_notes required |
rejected |
Delivery refused and returned — condition_notes required |
DeliveryPhoto¶
Photos attached to a delivery record.
| Field | Type | Required | Notes |
|---|---|---|---|
| id | UUID | System | PK, generated |
| delivery_id | UUID | ✓ | FK → deliveries.id |
| site_id | UUID | ✓ | Denormalised |
| project_id | UUID | ✓ | Denormalised |
| photo_key | VARCHAR(500) | ✓ | S3 object key |
| caption | VARCHAR(255) | Optional description | |
| taken_at | TIMESTAMPTZ | From EXIF or upload time if not available | |
| sort_order | INTEGER | ✓ | Default: 0 |
| created_at | TIMESTAMPTZ | System | |
| created_by | UUID | System | |
| deleted_at | TIMESTAMPTZ |
CREATE INDEX idx_delivery_photo_delivery
ON delivery_photos(delivery_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_delivery_photo_project
ON delivery_photos(project_id, taken_at DESC) WHERE deleted_at IS NULL;
Business Rules¶
-
delivered_atis set by the user — it records when the delivery arrived on site, not when the record was created. It can be set to any time on the current day or a past date (not future). -
condition_notesis required whenconditionisdamagedorrejected. A delivery withcondition = damagedwithoutcondition_notes→ 422. -
photo_countis denormalised on theDeliveryrecord and kept in sync by the photo add/delete operations. It is not recomputed from child records on every list query. -
There is no sign-off workflow for deliveries. The record is created and remains editable until soft-deleted. Audit trail via
updated_by/updated_atis sufficient. -
received_bydefaults to the authenticated user creating the record. Project Managers can setreceived_byto another user (for retrospective entry). Site Managers can only setreceived_byto themselves. -
Deliveries are not linked to the Documents or Plant modules in V1. The
descriptionfield is free text. Cross-referencing with purchase orders or delivery notes is a V2 feature. -
Up to 20 photos per delivery. The 21st upload → 422.
-
Soft deleting a delivery soft-deletes all its photos in the same transaction.
API Endpoints¶
Deliveries¶
| Method | Path | Description |
|---|---|---|
| GET | /sites/{siteId}/deliveries |
List all deliveries for this site (paginated, filterable) |
| POST | /sites/{siteId}/deliveries |
Record a new delivery |
| GET | /sites/{siteId}/deliveries/{id} |
Full detail with photos |
| PATCH | /sites/{siteId}/deliveries/{id} |
Update delivery record |
| DELETE | /sites/{siteId}/deliveries/{id} |
Soft delete |
Photos¶
| Method | Path | Description |
|---|---|---|
| GET | /sites/{siteId}/deliveries/{id}/photos |
List photos |
| POST | /sites/{siteId}/deliveries/{id}/photos |
Add photo (returns presigned PUT URL) |
| PATCH | /sites/{siteId}/deliveries/{id}/photos/{photoId} |
Update caption or sort order |
| DELETE | /sites/{siteId}/deliveries/{id}/photos/{photoId} |
Soft delete photo |
Project-level view¶
| Method | Path | Description |
|---|---|---|
| GET | /projects/{projectId}/deliveries |
Deliveries across all sites (paginated, filterable) |
Request / Response Shapes¶
POST /sites/{siteId}/deliveries¶
// Request
{
"reference": "DN-2026-1042",
"delivered_at": "2026-05-28T10:30:00Z",
"supplier": "Hanson UK",
"description": "Ready-mix concrete C30/37 — 24 m³ for pad foundations P7–P12",
"quantity": "24 m³",
"condition": "acceptable",
"condition_notes": null,
"custom_fields": {}
}
// Response 201
{
"id": "uuid-delivery",
"site_id": "uuid-site",
"project_id": "uuid-project",
"reference": "DN-2026-1042",
"delivered_at": "2026-05-28T10:30:00Z",
"supplier": "Hanson UK",
"description": "Ready-mix concrete C30/37 — 24 m³ for pad foundations P7–P12",
"quantity": "24 m³",
"condition": "acceptable",
"condition_notes": null,
"received_by": "uuid-user",
"received_by_name": "James Fletcher",
"photo_count": 0,
"custom_fields": {},
"created_at": "2026-05-28T10:35:00Z"
}
GET /sites/{siteId}/deliveries¶
Query params: condition (acceptable|damaged|rejected), from_date,
to_date, supplier (partial match), q (description/reference search),
sort (default -delivered_at), limit, cursor
// Response 200
{
"data": [
{
"id": "uuid-delivery",
"reference": "DN-2026-1042",
"delivered_at": "2026-05-28T10:30:00Z",
"supplier": "Hanson UK",
"description": "Ready-mix concrete C30/37 — 24 m³ for pad foundations P7–P12",
"quantity": "24 m³",
"condition": "acceptable",
"condition_notes": null,
"received_by_name": "James Fletcher",
"photo_count": 3,
"created_at": "2026-05-28T10:35:00Z"
}
],
"pagination": { "total": 18, "cursor": null, "has_more": false }
}
Permission Matrix¶
| Action | Platform Admin | Tenant Admin | Project Manager | Site Manager | Viewer |
|---|---|---|---|---|---|
| List / view deliveries | ✓ | ✓ | ✓ assigned | ✓ own site | ✓ assigned |
| Create delivery | ✓ | ✓ | ✓ assigned | ✓ own site | ✗ |
| Edit delivery | ✓ | ✓ | ✓ assigned | ✓ own site | ✗ |
| Delete delivery | ✓ | ✓ | ✓ assigned | ✗ | ✗ |
| Add photos | ✓ | ✓ | ✓ assigned | ✓ own site | ✗ |
| View photos | ✓ | ✓ | ✓ assigned | ✓ own site | ✓ assigned |
| Delete photos | ✓ | ✓ | ✓ assigned | ✓ own site | ✗ |
Notifications Triggered¶
None. The Deliveries module does not fire any notifications.
Offline Sync Scope¶
| Data | Syncs to | Notes |
|---|---|---|
| Deliveries | Site Manager | Last 90 days for assigned sites |
| DeliveryPhotos metadata | Site Manager | Last 90 days — keys only, not S3 data |
| Photo content | Not synced | Loaded on demand via presigned GET URL |
Offline create flow: - Site Manager creates delivery record on device, writes to local SQLite - Photos captured offline are stored in device storage - On reconnect: delivery record and photos upload sequentially
Cross-Module Dependencies¶
Upstream¶
| Module | Dependency |
|---|---|
| Projects & Sites | site_id, project_id |
Downstream¶
None. Deliveries is a terminal module.
Tests Required¶
Deliveries¶
- Create delivery — valid payload → 201
- Create delivery —
condition = damaged, nocondition_notes→ 422 - Create delivery —
condition = rejected, nocondition_notes→ 422 - Create delivery —
delivered_atin the future → 422 - List deliveries — filter by
condition,from_date,to_date qsearch matches description and reference- Update delivery — PATCH semantics
- Soft delete — excluded from list
Photos¶
- Add photo — returns presigned PUT URL
- List photos — ordered by
sort_order - Update caption — success
- Delete photo — soft delete;
photo_countdecremented on delivery - Maximum 20 photos — 21st upload → 422
Cross-tenant isolation (mandatory)¶
- GET delivery from another tenant's site → 404
- POST delivery with
site_idfrom another tenant → 404 - POST photo on a delivery from another tenant → 404
Permission boundaries¶
- Site Manager can create delivery → 201
- Site Manager cannot delete delivery → 403
- Viewer cannot create or edit → 403
- Viewer can view deliveries and photos → 200
Default Tenant Labels¶
| Key | Default |
|---|---|
module.deliveries |
Deliveries |
field.delivery.reference |
Delivery Reference |
field.delivery.supplier |
Supplier |
field.delivery.description |
Materials Description |
field.delivery.quantity |
Quantity |
field.delivery.condition |
Condition on Receipt |
field.delivery.condition_notes |
Condition Notes |
field.delivery.received_by |
Received By |
status.delivery.acceptable |
Acceptable |
status.delivery.damaged |
Damaged |
status.delivery.rejected |
Rejected |
Foundation Dependencies¶
| Component | Location | Used for |
|---|---|---|
TenantMiddleware |
core/middleware.py |
Sets request.state.tenant |
get_db |
core/db.py |
Schema-scoped AsyncSession |
get_current_user |
core/security.py |
Auth |
require_permission |
core/security.py |
RBAC |
EntityMixin |
core/models.py |
Universal columns |
PaginatedResponse |
core/pagination.py |
List endpoint wrapper |
ConstruoError hierarchy |
core/exceptions.py |
Typed exceptions |
presigned_put_url / presigned_get_url |
core/storage.py |
Photo upload and download |
audit_entity_change trigger |
DB foundation | Applied to deliveries, delivery_photos |
factories.py |
tests/factories.py |
create_delivery, create_delivery_photo |