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.
| Module | Exported models |
|---|---|
common.py | StrictModel, EntityId, IsoUtcInstant, NonEmptyText, CurrencyCode, Envelope[T], StreamEnvelope[T], HealthData |
charge.py | Charge, Photo, ContestAttachment, AppNotification, HubSnapshot, SyncSnapshotData, ChargeCommandRequest, ContestMetadata, NotificationReadRequest |
booking.py | Booking, InventoryReport |
inspection.py | Inspection, GeneralNote, ItemAction, ItemUpdate |
task.py | MaintenanceTask, CreateTaskRequest |
event.py | StoredEvent 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., withpropertyCode, roomName, displayLocation, startDate/endDatecharges.json— CHG-001..CHG-006 across statuses (outstanding/contested/accepted/resolved/paid) with deadlines straddlingreferenceNowinspections.json— INS-001 withgeneralNotes[], itemActions[] (chargeId?→CHG-*)anditemUpdates[]tasks.json,inventory_reports.json,notifications.jsonapp/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 constructionlib/core/database/kx_database.dart— Drift tables (Bookings, InventoryReports, Inspections, Tasks, Charges, ChargePhotos, ContestDrafts, AttachmentBlobs…) +domain_mapper.dartfor 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.jsonerror_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.