Skip to main content

Data Models & Schemas

Wire schemas (Pydantic v2 strict)

app/schemas/*.py defines strict models (StrictModel sets extra="forbid", strict=True). LowerCamelCase field names are intentional — per-file-ignores N815 is scoped to app/schemas/*.py so renaming them would break the wire.

ModuleExported models
common.pyStrictModel, EntityId, IsoUtcInstant, NonEmptyText, CurrencyCode, Envelope[T], StreamEnvelope[T], HealthData
charge.pyCharge, Photo, ContestAttachment, AppNotification, HubSnapshot, SyncSnapshotData, ChargeCommandRequest, ContestMetadata, NotificationReadRequest
booking.pyBooking, InventoryReport
inspection.pyInspection, GeneralNote, ItemAction, ItemUpdate
task.pyMaintenanceTask, CreateTaskRequest
event.pyStoredEvent shape for SSE

Charge key fields:

class Charge(StrictModel):
id: EntityId
bookingId: EntityId; inspectionId: EntityId | None
itemName: NonEmptyText; type: Literal["replace","repair","clean"]
notes: str; location: NonEmptyText
amountMinor: int = Field(ge=0); currency: CurrencyCode
status: Literal["outstanding","accepted","contested","resolved","paid"]
raisedAt: IsoUtcInstant; gracePeriodDays: int; deadlineAt: IsoUtcInstant
photos: list[Photo]; contestReason: str | None
contestAttachments: list[ContestAttachment]
acceptedAt, contestedAt, resolvedAt, paidAt: IsoUtcInstant | None
acceptanceOrigin: Literal["student","deadline","operator"] | None
version: int = Field(ge=1); updatedAt: IsoUtcInstant

Primitives: IsoUtcInstant enforces Z suffix, rejects offsets/naive; CurrencyCode restricts to ISO 4217; NonEmptyText rejects empty/whitespace.

Storage snapshot (app/db/models.py)

StoreSnapshot is not a DB — it's a versioned JSON document:

class StoreSnapshot(TypedDict):
version: int # == storeRevision, increments per commit, survives restart
stateEpoch: str # uuid, changes only on reset/reseed
streamEpoch: str; streamCursor: str
entities: Entities # bookings, inventoryReports, inspections, tasks, charges, notifications
events: list[StoredEvent] # ring, capped at EVENT_RING_CAPACITY=100
idempotency: dict[str, IdempotencyRecord]
storeRevision: int

ENTITY_KEYS maps each entity kind to its id field; SEED_FILES lists the six seed JSON files. StoredEvent carries id, type, entityId, resourceType, changeType, bookingId?, entityVersion?, createdAt, streamCursor.

Validation helpers reject unknown enums, non-UTC instants, and float money before the store accepts a write.

Seed data (app/seed/*.json)

Deterministic demo data authored at referenceNow=2026-08-01T12:00:00Z, Europe/London display TZ:

  • bookings.json — BKG-001 etc., with propertyCode, roomName, displayLocation, startDate/endDate
  • charges.json — CHG-001..CHG-006 across statuses (outstanding/contested/accepted/resolved/paid) with deadlines straddling referenceNow
  • inspections.json — INS-001 with generalNotes[], itemActions[] (chargeId?→CHG-*) and itemUpdates[]
  • tasks.json, inventory_reports.json, notifications.json
  • app/static/photos/att_*.png + thumbs, att_rpt_*.pdf — referenced by URL last segment

Seed invariants checked by scripts/export_fixtures.py and the contract test: every charge.bookingId exists, every itemAction.chargeId that is non-null points at a real charge, every attachment URL id is opaque att_*.

Mapping seam (Flutter parity)

Flutter mirrors these shapes in two places:

  • lib/core/network/api_models.dart — json_serializable models for HTTP — strict by construction
  • lib/core/database/kx_database.dart — Drift tables (Bookings, InventoryReports, Inspections, Tasks, Charges, ChargePhotos, ContestDrafts, AttachmentBlobs…) + domain_mapper.dart for round-trip mapping

A change to app/schemas/charge.py must propagate to both Dart layers and to docs/openapi-v1.json/contracts/examples/*.json.

OpenAPI snapshot

docs/openapi-v1.json is the frozen export of app/main.py's FastAPI openapi_url="/openapi.json". scripts/export_openapi.py --check diffs the live generation byte-for-byte; on mismatch you negotiate at G-01, you don't overwrite.

Example envelopes

Fifteen goldens in docs/contracts/examples/ — one per success/error shape:

  • get_charge_200.json, get_health_200.json, get_booking_hub_200.json, get_sync_snapshot_meta_200.json, post_charge_contest_200.json, sse_frames.json
  • error_charge_not_found_404.json, error_charge_invalid_transition_409.json, error_charge_version_conflict_409.json, error_store_epoch_mismatch_409.json, etc.

Each golden is {data, meta} or {error, meta} with schemaVersion/stateEpoch/storeRevision populated, so code generators can be tested without inventing any rule.