Skip to content

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

  1. delivered_at is 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).

  2. condition_notes is required when condition is damaged or rejected. A delivery with condition = damaged without condition_notes → 422.

  3. photo_count is denormalised on the Delivery record and kept in sync by the photo add/delete operations. It is not recomputed from child records on every list query.

  4. There is no sign-off workflow for deliveries. The record is created and remains editable until soft-deleted. Audit trail via updated_by / updated_at is sufficient.

  5. received_by defaults to the authenticated user creating the record. Project Managers can set received_by to another user (for retrospective entry). Site Managers can only set received_by to themselves.

  6. Deliveries are not linked to the Documents or Plant modules in V1. The description field is free text. Cross-referencing with purchase orders or delivery notes is a V2 feature.

  7. Up to 20 photos per delivery. The 21st upload → 422.

  8. 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, no condition_notes → 422
  • Create delivery — condition = rejected, no condition_notes → 422
  • Create delivery — delivered_at in the future → 422
  • List deliveries — filter by condition, from_date, to_date
  • q search 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_count decremented on delivery
  • Maximum 20 photos — 21st upload → 422

Cross-tenant isolation (mandatory)

  • GET delivery from another tenant's site → 404
  • POST delivery with site_id from 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