From 24188d9b10553be9a1da2e73fa92d1bf61b8fc1a Mon Sep 17 00:00:00 2001
From: NuklearRabbit <145918611+NuklearRabbit@users.noreply.github.com>
Date: Sat, 1 Aug 2026 20:34:43 +0200
Subject: [PATCH] Initial MobilityOps build pack (docs, contracts, scaffold)
---
.env.example | 34 +++
.gitignore | 15 ++
CLAUDE.md | 61 +++++
FILE_INDEX.md | 72 +++++
MASTER_BUILD_PROMPT.md | 21 ++
Makefile | 23 ++
PROJECT_STATE.md | 26 ++
README.md | 51 ++++
START_HERE.md | 39 +++
backend/Dockerfile | 8 +
backend/app/__init__.py | 0
backend/app/core/__init__.py | 0
backend/app/core/config.py | 22 ++
backend/app/main.py | 22 ++
backend/pyproject.toml | 39 +++
backend/tests/test_health.py | 9 +
compose.yaml | 76 ++++++
contracts/events.schema.json | 35 +++
contracts/mcp-tools.json | 61 +++++
contracts/openapi.yaml | 218 +++++++++++++++
contracts/ragcore-contract-assumptions.md | 13 +
docs/00-product-brief.md | 27 ++
docs/01-scope-and-non-goals.md | 36 +++
docs/02-user-stories.md | 32 +++
docs/03-architecture.md | 55 ++++
docs/04-domain-model.md | 118 +++++++++
docs/05-api-contract.md | 75 ++++++
docs/06-ui-ux.md | 78 ++++++
docs/07-data-quality.md | 47 ++++
docs/08-return-workflow.md | 42 +++
docs/09-ragcore-integration.md | 65 +++++
docs/10-mcp-hub-integration.md | 56 ++++
docs/11-n8n-integration.md | 41 +++
docs/12-security-and-audit.md | 47 ++++
docs/13-seed-and-demo-scenarios.md | 54 ++++
docs/14-testing-and-acceptance.md | 74 ++++++
docs/15-build-plan.md | 113 ++++++++
docs/16-portfolio-case-study.md | 43 +++
docs/17-runbook.md | 28 ++
docs/deferred.md | 14 +
frontend/Dockerfile | 10 +
frontend/index.html | 13 +
frontend/nginx.conf | 22 ++
frontend/package.json | 22 ++
frontend/src/App.tsx | 49 ++++
frontend/src/main.tsx | 10 +
frontend/src/styles.css | 19 ++
frontend/tsconfig.json | 21 ++
frontend/vite.config.ts | 6 +
knowledge/manifest.json | 107 ++++++++
knowledge/procedures/01-vehicle-checkout.md | 21 ++
knowledge/procedures/02-vehicle-return.md | 25 ++
knowledge/procedures/03-damage-handling.md | 21 ++
knowledge/procedures/04-odometer-anomalies.md | 21 ++
knowledge/procedures/05-cleaning-checklist.md | 21 ++
.../procedures/06-maintenance-escalation.md | 21 ++
knowledge/procedures/07-customer-documents.md | 21 ++
knowledge/procedures/08-privacy.md | 21 ++
knowledge/procedures/09-booking-conflicts.md | 21 ++
.../procedures/10-roles-and-escalation.md | 21 ++
n8n/README.md | 13 +
n8n/mobilityops-return-processing.json | 122 +++++++++
seed/README.md | 11 +
seed/bookings.csv | 247 +++++++++++++++++
seed/customers.csv | 181 +++++++++++++
seed/data_quality_issues.csv | 16 ++
seed/generate_seed.py | 250 ++++++++++++++++++
seed/inspections.csv | 76 ++++++
seed/maintenance.csv | 41 +++
seed/vehicles.csv | 51 ++++
seed/workflow_runs.csv | 21 ++
71 files changed, 3412 insertions(+)
create mode 100644 .env.example
create mode 100644 .gitignore
create mode 100644 CLAUDE.md
create mode 100644 FILE_INDEX.md
create mode 100644 MASTER_BUILD_PROMPT.md
create mode 100644 Makefile
create mode 100644 PROJECT_STATE.md
create mode 100644 README.md
create mode 100644 START_HERE.md
create mode 100644 backend/Dockerfile
create mode 100644 backend/app/__init__.py
create mode 100644 backend/app/core/__init__.py
create mode 100644 backend/app/core/config.py
create mode 100644 backend/app/main.py
create mode 100644 backend/pyproject.toml
create mode 100644 backend/tests/test_health.py
create mode 100644 compose.yaml
create mode 100644 contracts/events.schema.json
create mode 100644 contracts/mcp-tools.json
create mode 100644 contracts/openapi.yaml
create mode 100644 contracts/ragcore-contract-assumptions.md
create mode 100644 docs/00-product-brief.md
create mode 100644 docs/01-scope-and-non-goals.md
create mode 100644 docs/02-user-stories.md
create mode 100644 docs/03-architecture.md
create mode 100644 docs/04-domain-model.md
create mode 100644 docs/05-api-contract.md
create mode 100644 docs/06-ui-ux.md
create mode 100644 docs/07-data-quality.md
create mode 100644 docs/08-return-workflow.md
create mode 100644 docs/09-ragcore-integration.md
create mode 100644 docs/10-mcp-hub-integration.md
create mode 100644 docs/11-n8n-integration.md
create mode 100644 docs/12-security-and-audit.md
create mode 100644 docs/13-seed-and-demo-scenarios.md
create mode 100644 docs/14-testing-and-acceptance.md
create mode 100644 docs/15-build-plan.md
create mode 100644 docs/16-portfolio-case-study.md
create mode 100644 docs/17-runbook.md
create mode 100644 docs/deferred.md
create mode 100644 frontend/Dockerfile
create mode 100644 frontend/index.html
create mode 100644 frontend/nginx.conf
create mode 100644 frontend/package.json
create mode 100644 frontend/src/App.tsx
create mode 100644 frontend/src/main.tsx
create mode 100644 frontend/src/styles.css
create mode 100644 frontend/tsconfig.json
create mode 100644 frontend/vite.config.ts
create mode 100644 knowledge/manifest.json
create mode 100644 knowledge/procedures/01-vehicle-checkout.md
create mode 100644 knowledge/procedures/02-vehicle-return.md
create mode 100644 knowledge/procedures/03-damage-handling.md
create mode 100644 knowledge/procedures/04-odometer-anomalies.md
create mode 100644 knowledge/procedures/05-cleaning-checklist.md
create mode 100644 knowledge/procedures/06-maintenance-escalation.md
create mode 100644 knowledge/procedures/07-customer-documents.md
create mode 100644 knowledge/procedures/08-privacy.md
create mode 100644 knowledge/procedures/09-booking-conflicts.md
create mode 100644 knowledge/procedures/10-roles-and-escalation.md
create mode 100644 n8n/README.md
create mode 100644 n8n/mobilityops-return-processing.json
create mode 100644 seed/README.md
create mode 100644 seed/bookings.csv
create mode 100644 seed/customers.csv
create mode 100644 seed/data_quality_issues.csv
create mode 100644 seed/generate_seed.py
create mode 100644 seed/inspections.csv
create mode 100644 seed/maintenance.csv
create mode 100644 seed/vehicles.csv
create mode 100644 seed/workflow_runs.csv
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..0ca42e2
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,34 @@
+COMPOSE_PROJECT_NAME=mobilityops
+MOBILITYOPS_ENV=development
+MOBILITYOPS_DEMO_MODE=true
+MOBILITYOPS_PUBLIC_URL=http://localhost:1228
+MOBILITYOPS_API_URL=http://localhost:8128
+DATABASE_URL=postgresql+psycopg://mobilityops:mobilityops@db:5432/mobilityops
+POSTGRES_DB=mobilityops
+POSTGRES_USER=mobilityops
+POSTGRES_PASSWORD=mobilityops
+APP_SECRET=replace-in-production
+DEMO_TODAY=2026-08-01
+TZ=Europe/Brussels
+
+# n8n
+N8N_BASE_URL=http://n8n:5678
+N8N_WEBHOOK_URL=http://n8n:5678/webhook/mobilityops-return
+N8N_ENCRYPTION_KEY=replace-me
+N8N_BASIC_AUTH_ACTIVE=true
+N8N_BASIC_AUTH_USER=admin
+N8N_BASIC_AUTH_PASSWORD=change-me
+
+# RAGcore integration
+KNOWLEDGE_PROVIDER=demo
+RAGCORE_BASE_URL=http://ragcore-api:8000
+RAGCORE_TENANT=northstar-mobility-demo
+RAGCORE_WORKSPACE=mobilityops
+RAGCORE_COLLECTION=internal-procedures
+RAGCORE_API_TOKEN=
+
+# ITWorx MCP Hub integration
+MCP_HUB_REGISTRATION_ENABLED=false
+MCP_HUB_BASE_URL=http://itworx-mcp-hub:8000
+MCP_HUB_SERVICE_TOKEN=
+MCP_PROVIDER_ID=mobilityops
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6126a5f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+.env
+.venv/
+__pycache__/
+.pytest_cache/
+.mypy_cache/
+.ruff_cache/
+node_modules/
+dist/
+coverage/
+playwright-report/
+test-results/
+*.pyc
+.DS_Store
+.idea/
+.vscode/
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..fb51793
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,61 @@
+# Binding instructions for Claude
+
+## Operating mode
+
+Work autonomously. Do not ask the user product, architecture, naming, UI, scope or implementation questions already answered in this repository. Record a reasonable assumption in an ADR only when a genuine gap blocks implementation.
+
+Use normal or medium reasoning for routine work. Reserve high reasoning for an actual cross-service design conflict or a persistent failure after evidence-driven debugging.
+
+Continue from milestone to milestone until every acceptance criterion is satisfied. Do not stop merely because one milestone is complete.
+
+## Token and tool efficiency
+
+1. Read `START_HERE.md`, this file, `PROJECT_STATE.md` and `docs/15-build-plan.md` first.
+2. Read only the milestone-specific documents named in the build plan.
+3. Do not repeatedly reread all documentation.
+4. Keep explanations terse; spend effort on implementation and validation.
+5. Update `PROJECT_STATE.md` after each milestone with decisions, commands, evidence and the exact next action.
+6. Prefer focused file inspection and targeted tests over broad repository scans.
+7. Do not generate large speculative documents after implementation starts.
+
+## Scope discipline
+
+- Build the locked PoC only.
+- Do not add accounting, payments, public reservations, a generic CRM, inventory, HR, a second RAG stack, a separate MCP server or autonomous write actions.
+- Do not modify the RAGcore or ITWorx MCP Hub repositories. Integrate only through documented contracts and configurable adapters.
+- Keep critical business rules in MobilityOps code, not in n8n or prompts.
+- No direct MCP Hub or RAGcore access to the MobilityOps database.
+
+## Product quality
+
+- No dead buttons, empty routes, unexplained placeholders or hardcoded dashboard metrics.
+- Every visible number must derive from persisted data.
+- All important state changes must be audited.
+- AI must never invent an answer when RAGcore is unavailable or returns insufficient evidence.
+- Vehicle returns must commit locally even when n8n is unavailable; orchestration becomes pending and retryable.
+- External dependencies require timeouts, bounded retries, health state and graceful degradation.
+- Demo data must be clearly labelled synthetic.
+
+## Engineering rules
+
+- Backend: Python, FastAPI, SQLAlchemy 2, Alembic, PostgreSQL.
+- Frontend: React, TypeScript, Vite, accessible responsive UI.
+- Validation: Pydantic at API boundaries and database constraints for invariants.
+- Use UUID primary keys internally and stable human-readable public references.
+- Store UTC timestamps; render Europe/Brussels in the UI.
+- API paths start with `/api/v1`.
+- Use an outbox record for reliable post-commit n8n delivery.
+- Tests must cover domain rules, API contracts and the five-minute Playwright demo.
+- Generate and commit dependency lockfiles.
+
+## Git workflow
+
+Create one coherent commit per milestone after its validation passes. Suggested message format:
+
+`M1: implement operational core`
+
+Never rewrite already accepted milestone history unless necessary to fix a regression.
+
+## Definition of done
+
+The project is done only when `docs/14-testing-and-acceptance.md` passes from a clean checkout and `PROJECT_STATE.md` contains the final evidence summary.
diff --git a/FILE_INDEX.md b/FILE_INDEX.md
new file mode 100644
index 0000000..112b456
--- /dev/null
+++ b/FILE_INDEX.md
@@ -0,0 +1,72 @@
+# File index
+
+- `.env.example`
+- `.gitignore`
+- `CLAUDE.md`
+- `MASTER_BUILD_PROMPT.md`
+- `Makefile`
+- `PROJECT_STATE.md`
+- `README.md`
+- `START_HERE.md`
+- `backend/Dockerfile`
+- `backend/app/__init__.py`
+- `backend/app/core/__init__.py`
+- `backend/app/core/config.py`
+- `backend/app/main.py`
+- `backend/pyproject.toml`
+- `backend/tests/test_health.py`
+- `compose.yaml`
+- `contracts/events.schema.json`
+- `contracts/mcp-tools.json`
+- `contracts/openapi.yaml`
+- `contracts/ragcore-contract-assumptions.md`
+- `docs/00-product-brief.md`
+- `docs/01-scope-and-non-goals.md`
+- `docs/02-user-stories.md`
+- `docs/03-architecture.md`
+- `docs/04-domain-model.md`
+- `docs/05-api-contract.md`
+- `docs/06-ui-ux.md`
+- `docs/07-data-quality.md`
+- `docs/08-return-workflow.md`
+- `docs/09-ragcore-integration.md`
+- `docs/10-mcp-hub-integration.md`
+- `docs/11-n8n-integration.md`
+- `docs/12-security-and-audit.md`
+- `docs/13-seed-and-demo-scenarios.md`
+- `docs/14-testing-and-acceptance.md`
+- `docs/15-build-plan.md`
+- `docs/16-portfolio-case-study.md`
+- `docs/17-runbook.md`
+- `docs/deferred.md`
+- `frontend/Dockerfile`
+- `frontend/index.html`
+- `frontend/nginx.conf`
+- `frontend/package.json`
+- `frontend/src/App.tsx`
+- `frontend/src/main.tsx`
+- `frontend/src/styles.css`
+- `frontend/tsconfig.json`
+- `frontend/vite.config.ts`
+- `knowledge/manifest.json`
+- `knowledge/procedures/01-vehicle-checkout.md`
+- `knowledge/procedures/02-vehicle-return.md`
+- `knowledge/procedures/03-damage-handling.md`
+- `knowledge/procedures/04-odometer-anomalies.md`
+- `knowledge/procedures/05-cleaning-checklist.md`
+- `knowledge/procedures/06-maintenance-escalation.md`
+- `knowledge/procedures/07-customer-documents.md`
+- `knowledge/procedures/08-privacy.md`
+- `knowledge/procedures/09-booking-conflicts.md`
+- `knowledge/procedures/10-roles-and-escalation.md`
+- `n8n/README.md`
+- `n8n/mobilityops-return-processing.json`
+- `seed/README.md`
+- `seed/bookings.csv`
+- `seed/customers.csv`
+- `seed/data_quality_issues.csv`
+- `seed/generate_seed.py`
+- `seed/inspections.csv`
+- `seed/maintenance.csv`
+- `seed/vehicles.csv`
+- `seed/workflow_runs.csv`
diff --git a/MASTER_BUILD_PROMPT.md b/MASTER_BUILD_PROMPT.md
new file mode 100644
index 0000000..6934025
--- /dev/null
+++ b/MASTER_BUILD_PROMPT.md
@@ -0,0 +1,21 @@
+# Paste this once into Claude Code
+
+Build MobilityOps autonomously from this repository.
+
+First read `START_HERE.md`, `CLAUDE.md`, `PROJECT_STATE.md` and `docs/15-build-plan.md`. Treat the repository specifications and contracts as binding. Do not ask me questions that the files already answer, do not broaden the PoC, and do not stop after a milestone.
+
+Implement the milestones in order. For each milestone:
+
+1. read only the documents listed for that milestone;
+2. implement the smallest complete solution;
+3. run the specified validation plus relevant regression tests;
+4. fix failures using evidence rather than guesses;
+5. update `PROJECT_STATE.md` with concise evidence and the exact next step;
+6. commit the completed milestone;
+7. continue immediately.
+
+MobilityOps owns operational data and business rules. RAGcore owns retrieval and grounded answers. ITWorx MCP Hub owns MCP publication and policy. n8n only orchestrates post-commit workflows. Use configurable adapters and working degraded modes so the core demo remains usable when any external service is absent.
+
+The finished PoC must be reproducible from a clean checkout, have no dead UI, use deterministic synthetic data, support the documented five-minute demo, and satisfy every criterion in `docs/14-testing-and-acceptance.md`.
+
+Keep chat output brief. Spend the available context on code, tests, validation and final evidence. Begin now and continue until the repository is complete.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..d2f494a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,23 @@
+.PHONY: up down logs test seed reset lint
+
+up:
+ docker compose up --build -d
+
+down:
+ docker compose down
+
+logs:
+ docker compose logs -f --tail=200
+
+test:
+ docker compose run --rm api pytest
+
+lint:
+ docker compose run --rm api ruff check .
+
+seed:
+ docker compose exec api python -m app.cli seed --reset
+
+reset:
+ docker compose down -v
+ docker compose up --build -d
diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md
new file mode 100644
index 0000000..eb9a3f7
--- /dev/null
+++ b/PROJECT_STATE.md
@@ -0,0 +1,26 @@
+# Project state
+
+## Current milestone
+
+M0 — not started.
+
+## Locked decisions
+
+- Product name: MobilityOps.
+- Fictitious tenant: Northstar Mobility Demo.
+- PoC only; all operational and knowledge data are synthetic.
+- Core stack and boundaries are defined in `CLAUDE.md` and `docs/03-architecture.md`.
+- RAGcore and ITWorx MCP Hub are external central services.
+- n8n receives post-commit events through an outbox dispatcher.
+
+## Completed evidence
+
+None.
+
+## Known blockers
+
+None. External service credentials may be absent; use the documented demo/degraded providers.
+
+## Exact next action
+
+Read M0 inputs in `docs/15-build-plan.md`, verify the scaffold, generate lockfiles and establish clean-checkout validation.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f85cae6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,51 @@
+# MobilityOps
+
+**Connected operations for vehicle rental and service teams.**
+
+MobilityOps is a working proof of concept for a fictitious mobility company. It combines vehicle and booking operations, a controlled vehicle-return workflow, data-quality review, RAGcore-backed internal knowledge, n8n orchestration and read-only tools published through ITWorx MCP Hub.
+
+All people, companies, vehicles, bookings and documents are synthetic. The workflows, validation, integrations, audit logging and access boundaries are intended to be real.
+
+## Scope
+
+The PoC implements:
+
+- operations dashboard;
+- vehicle and booking views;
+- one complete vehicle-return workflow;
+- five deterministic data-quality checks;
+- human review and customer merge;
+- audit trail;
+- RAGcore-backed knowledge assistant with citations;
+- one n8n return-processing workflow;
+- four read-only MCP tools through ITWorx MCP Hub;
+- deterministic demo reset and five-minute showcase.
+
+It is not an ERP, CRM, accounting package, public booking site, payment system or autonomous agent.
+
+## Repository map
+
+- `CLAUDE.md` — binding implementation rules.
+- `MASTER_BUILD_PROMPT.md` — prompt to start an autonomous Claude run.
+- `PROJECT_STATE.md` — short persistent project memory.
+- `docs/` — product, architecture, UX and acceptance specification.
+- `contracts/` — OpenAPI, event and MCP contracts.
+- `knowledge/` — fictitious source documents for the MobilityOps RAGcore workspace.
+- `seed/` — deterministic synthetic dataset and generator.
+- `n8n/` — importable workflow definitions.
+- `backend/` and `frontend/` — minimal bootable scaffold, not the finished application.
+
+## Local scaffold
+
+```bash
+cp .env.example .env
+docker compose up --build
+```
+
+Expected scaffold endpoints:
+
+- Web: `http://localhost:1228`
+- API health: `http://localhost:8128/health`
+- n8n: `http://localhost:5678`
+
+The finished project must keep these defaults configurable.
diff --git a/START_HERE.md b/START_HERE.md
new file mode 100644
index 0000000..a594f8f
--- /dev/null
+++ b/START_HERE.md
@@ -0,0 +1,39 @@
+# MobilityOps — Claude build pack
+
+MobilityOps is a deliberately scoped, working proof of concept for a fictitious Belgian mobility-rental company. It demonstrates how operational data, workflow automation, data-quality review, a central RAG service and a central MCP hub can work together without pretending to be a full ERP.
+
+## Use this pack efficiently
+
+1. Extract this archive into a new repository.
+2. Open the repository in Claude Code.
+3. Paste the contents of `MASTER_BUILD_PROMPT.md` once.
+4. Let Claude continue autonomously through the milestones in `docs/15-build-plan.md`.
+5. Only intervene if the environment itself is unavailable or credentials for an external service are required.
+
+Claude must use `PROJECT_STATE.md` as its compact memory between sessions. Do not restate the full project in later prompts.
+
+## What is included
+
+- locked product scope and non-goals;
+- architecture and domain decisions;
+- API and event contracts;
+- realistic deterministic synthetic seed data;
+- ten fictitious procedures for RAGcore;
+- an initial n8n workflow export;
+- MCP tool definitions for ITWorx MCP Hub;
+- a minimal bootable frontend/API scaffold;
+- acceptance criteria and a five-minute demo script;
+- a master prompt optimized for one autonomous build run.
+
+## Core responsibilities
+
+| Component | Responsibility |
+|---|---|
+| MobilityOps | Operational data, business rules, UI, audit and data-quality review |
+| RAGcore | Document ingestion, retrieval and source-grounded answers |
+| ITWorx MCP Hub | Controlled AI access to MobilityOps tools and MobilityOps knowledge |
+| n8n | Cross-system orchestration after committed domain events |
+
+## Build principle
+
+Finish a small vertical slice completely. No placeholder pages, fake buttons, invented production claims or scope expansion.
diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000..691d1a0
--- /dev/null
+++ b/backend/Dockerfile
@@ -0,0 +1,8 @@
+FROM python:3.12-slim
+ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
+WORKDIR /app
+COPY pyproject.toml ./
+COPY app ./app
+RUN pip install --no-cache-dir .
+EXPOSE 8000
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/backend/app/__init__.py b/backend/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/app/core/config.py b/backend/app/core/config.py
new file mode 100644
index 0000000..b9f37e1
--- /dev/null
+++ b/backend/app/core/config.py
@@ -0,0 +1,22 @@
+from functools import lru_cache
+
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+
+class Settings(BaseSettings):
+ model_config = SettingsConfigDict(env_file=".env", extra="ignore")
+
+ mobilityops_env: str = "development"
+ mobilityops_demo_mode: bool = True
+ database_url: str = "postgresql+psycopg://mobilityops:mobilityops@db:5432/mobilityops"
+ knowledge_provider: str = "demo"
+ ragcore_base_url: str = "http://ragcore-api:8000"
+ ragcore_tenant: str = "northstar-mobility-demo"
+ ragcore_workspace: str = "mobilityops"
+ ragcore_collection: str = "internal-procedures"
+ n8n_webhook_url: str = "http://n8n:5678/webhook/mobilityops-return"
+
+
+@lru_cache
+def get_settings() -> Settings:
+ return Settings()
diff --git a/backend/app/main.py b/backend/app/main.py
new file mode 100644
index 0000000..2c4cb4a
--- /dev/null
+++ b/backend/app/main.py
@@ -0,0 +1,22 @@
+from fastapi import FastAPI
+
+from app.core.config import get_settings
+
+settings = get_settings()
+app = FastAPI(title="MobilityOps API", version="0.0.1")
+
+
+@app.get("/health")
+def health() -> dict[str, str]:
+ return {"status": "ok", "service": "mobilityops-api"}
+
+
+@app.get("/api/v1/system/status")
+def system_status() -> dict[str, object]:
+ return {
+ "service": "mobilityops-api",
+ "environment": settings.mobilityops_env,
+ "demo_mode": settings.mobilityops_demo_mode,
+ "knowledge_provider": settings.knowledge_provider,
+ "scaffold": True,
+ }
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
new file mode 100644
index 0000000..bb7e078
--- /dev/null
+++ b/backend/pyproject.toml
@@ -0,0 +1,39 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "mobilityops-api"
+version = "0.0.1"
+description = "MobilityOps API scaffold"
+requires-python = ">=3.12"
+dependencies = [
+ "fastapi>=0.115,<1",
+ "uvicorn[standard]>=0.30,<1",
+ "pydantic-settings>=2.5,<3",
+ "sqlalchemy>=2.0,<3",
+ "psycopg[binary]>=3.2,<4",
+ "alembic>=1.13,<2",
+ "httpx>=0.27,<1"
+]
+
+[project.optional-dependencies]
+dev = [
+ "pytest>=8,<9",
+ "pytest-asyncio>=0.24,<1",
+ "ruff>=0.8,<1",
+ "mypy>=1.13,<2"
+]
+
+[tool.hatch.build.targets.wheel]
+packages = ["app"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+asyncio_mode = "auto"
+
+[tool.ruff]
+line-length = 100
+
+[tool.ruff.lint]
+select = ["E", "F", "I", "B", "UP"]
diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py
new file mode 100644
index 0000000..6f6f646
--- /dev/null
+++ b/backend/tests/test_health.py
@@ -0,0 +1,9 @@
+from fastapi.testclient import TestClient
+
+from app.main import app
+
+
+def test_health() -> None:
+ response = TestClient(app).get("/health")
+ assert response.status_code == 200
+ assert response.json() == {"status": "ok", "service": "mobilityops-api"}
diff --git a/compose.yaml b/compose.yaml
new file mode 100644
index 0000000..09576df
--- /dev/null
+++ b/compose.yaml
@@ -0,0 +1,76 @@
+services:
+ db:
+ image: postgres:16-alpine
+ environment:
+ POSTGRES_DB: ${POSTGRES_DB:-mobilityops}
+ POSTGRES_USER: ${POSTGRES_USER:-mobilityops}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mobilityops}
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mobilityops} -d ${POSTGRES_DB:-mobilityops}"]
+ interval: 5s
+ timeout: 3s
+ retries: 20
+ volumes:
+ - mobilityops-db:/var/lib/postgresql/data
+ networks: [mobilityops]
+
+ api:
+ build: ./backend
+ environment:
+ MOBILITYOPS_ENV: ${MOBILITYOPS_ENV:-development}
+ MOBILITYOPS_DEMO_MODE: ${MOBILITYOPS_DEMO_MODE:-true}
+ DATABASE_URL: ${DATABASE_URL:-postgresql+psycopg://mobilityops:mobilityops@db:5432/mobilityops}
+ TZ: ${TZ:-Europe/Brussels}
+ KNOWLEDGE_PROVIDER: ${KNOWLEDGE_PROVIDER:-demo}
+ RAGCORE_BASE_URL: ${RAGCORE_BASE_URL:-http://ragcore-api:8000}
+ RAGCORE_TENANT: ${RAGCORE_TENANT:-northstar-mobility-demo}
+ RAGCORE_WORKSPACE: ${RAGCORE_WORKSPACE:-mobilityops}
+ RAGCORE_COLLECTION: ${RAGCORE_COLLECTION:-internal-procedures}
+ RAGCORE_API_TOKEN: ${RAGCORE_API_TOKEN:-}
+ N8N_WEBHOOK_URL: ${N8N_WEBHOOK_URL:-http://n8n:5678/webhook/mobilityops-return}
+ ports:
+ - "8128:8000"
+ depends_on:
+ db:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
+ interval: 10s
+ timeout: 5s
+ retries: 10
+ networks: [mobilityops]
+
+ web:
+ build: ./frontend
+ environment:
+ VITE_API_BASE_URL: ${MOBILITYOPS_API_URL:-http://localhost:8128}
+ ports:
+ - "1228:80"
+ depends_on:
+ api:
+ condition: service_healthy
+ networks: [mobilityops]
+
+ n8n:
+ image: n8nio/n8n:latest
+ environment:
+ TZ: ${TZ:-Europe/Brussels}
+ GENERIC_TIMEZONE: ${TZ:-Europe/Brussels}
+ N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY:-replace-me}
+ N8N_BASIC_AUTH_ACTIVE: ${N8N_BASIC_AUTH_ACTIVE:-true}
+ N8N_BASIC_AUTH_USER: ${N8N_BASIC_AUTH_USER:-admin}
+ N8N_BASIC_AUTH_PASSWORD: ${N8N_BASIC_AUTH_PASSWORD:-change-me}
+ N8N_SECURE_COOKIE: "false"
+ ports:
+ - "5678:5678"
+ volumes:
+ - mobilityops-n8n:/home/node/.n8n
+ - ./n8n:/imports:ro
+ networks: [mobilityops]
+
+networks:
+ mobilityops:
+
+volumes:
+ mobilityops-db:
+ mobilityops-n8n:
diff --git a/contracts/events.schema.json b/contracts/events.schema.json
new file mode 100644
index 0000000..07c1c99
--- /dev/null
+++ b/contracts/events.schema.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://itworx.tech/schemas/mobilityops-events-v1.json",
+ "title": "MobilityOps event envelope",
+ "type": "object",
+ "required": ["event_id", "event_type", "occurred_at", "correlation_id", "aggregate", "data"],
+ "properties": {
+ "event_id": {"type": "string", "format": "uuid"},
+ "event_type": {"const": "vehicle.returned.v1"},
+ "occurred_at": {"type": "string", "format": "date-time"},
+ "correlation_id": {"type": "string"},
+ "aggregate": {
+ "type": "object",
+ "required": ["type", "id", "public_ref"],
+ "properties": {
+ "type": {"const": "booking"},
+ "id": {"type": "string", "format": "uuid"},
+ "public_ref": {"type": "string"}
+ },
+ "additionalProperties": false
+ },
+ "data": {
+ "type": "object",
+ "required": ["vehicle_ref", "inspection_ref", "resulting_vehicle_status", "attention_reasons"],
+ "properties": {
+ "vehicle_ref": {"type": "string"},
+ "inspection_ref": {"type": "string"},
+ "resulting_vehicle_status": {"enum": ["cleaning", "maintenance", "blocked"]},
+ "attention_reasons": {"type": "array", "items": {"type": "string"}}
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/contracts/mcp-tools.json b/contracts/mcp-tools.json
new file mode 100644
index 0000000..d25bc37
--- /dev/null
+++ b/contracts/mcp-tools.json
@@ -0,0 +1,61 @@
+{
+ "provider_id": "mobilityops",
+ "version": "1.0.0",
+ "required_scope": "mobilityops.read",
+ "tools": [
+ {
+ "name": "mobilityops_get_operations_summary",
+ "description": "Return current high-level vehicle, data-quality and workflow counts for the synthetic MobilityOps demo tenant.",
+ "read_only": true,
+ "inputSchema": {"type": "object", "additionalProperties": false},
+ "endpoint": {"method": "GET", "path": "/api/v1/integrations/mcp/operations-summary"}
+ },
+ {
+ "name": "mobilityops_list_attention_vehicles",
+ "description": "List vehicles that require operational attention, optionally filtered by minimum severity and date.",
+ "read_only": true,
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "minimum_severity": {"enum": ["low", "medium", "high"], "default": "medium"},
+ "date": {"type": "string", "format": "date"},
+ "limit": {"type": "integer", "minimum": 1, "maximum": 50, "default": 20}
+ },
+ "additionalProperties": false
+ },
+ "endpoint": {"method": "GET", "path": "/api/v1/integrations/mcp/attention-vehicles"}
+ },
+ {
+ "name": "mobilityops_get_vehicle_details",
+ "description": "Return a read-only operational view of one vehicle by its stable public reference.",
+ "read_only": true,
+ "inputSchema": {
+ "type": "object",
+ "required": ["vehicle_ref"],
+ "properties": {"vehicle_ref": {"type": "string", "pattern": "^MO-[0-9]{3}$"}},
+ "additionalProperties": false
+ },
+ "endpoint": {"method": "GET", "path": "/api/v1/integrations/mcp/vehicles/{vehicle_ref}"}
+ },
+ {
+ "name": "mobilityops_search_knowledge",
+ "description": "Search versioned MobilityOps internal procedures through the dedicated RAGcore workspace and return grounded source references.",
+ "read_only": true,
+ "inputSchema": {
+ "type": "object",
+ "required": ["question"],
+ "properties": {
+ "question": {"type": "string", "minLength": 3, "maxLength": 1000},
+ "max_sources": {"type": "integer", "minimum": 1, "maximum": 8, "default": 4}
+ },
+ "additionalProperties": false
+ },
+ "routing": {
+ "preferred": "ragcore",
+ "tenant": "northstar-mobility-demo",
+ "workspace": "mobilityops",
+ "collection": "internal-procedures"
+ }
+ }
+ ]
+}
diff --git a/contracts/openapi.yaml b/contracts/openapi.yaml
new file mode 100644
index 0000000..09cad2e
--- /dev/null
+++ b/contracts/openapi.yaml
@@ -0,0 +1,218 @@
+openapi: 3.1.0
+info:
+ title: MobilityOps API
+ version: 0.1.0
+ description: Contract baseline for the MobilityOps proof of concept.
+servers:
+ - url: http://localhost:8128
+paths:
+ /health:
+ get:
+ operationId: health
+ responses:
+ '200':
+ description: Healthy
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Health'
+ /api/v1/dashboard:
+ get:
+ operationId: getDashboard
+ responses:
+ '200':
+ description: Persisted operational summary
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Dashboard'
+ /api/v1/vehicles:
+ get:
+ operationId: listVehicles
+ parameters:
+ - in: query
+ name: status
+ schema:
+ type: string
+ - in: query
+ name: attention_only
+ schema:
+ type: boolean
+ default: false
+ responses:
+ '200':
+ description: Vehicle list
+ /api/v1/vehicles/{public_ref}:
+ get:
+ operationId: getVehicle
+ parameters:
+ - $ref: '#/components/parameters/PublicRef'
+ responses:
+ '200':
+ description: Vehicle detail
+ '404':
+ $ref: '#/components/responses/NotFound'
+ /api/v1/bookings:
+ get:
+ operationId: listBookings
+ responses:
+ '200':
+ description: Booking list
+ /api/v1/bookings/{public_ref}:
+ get:
+ operationId: getBooking
+ parameters:
+ - $ref: '#/components/parameters/PublicRef'
+ responses:
+ '200':
+ description: Booking detail
+ /api/v1/bookings/{public_ref}/return:
+ post:
+ operationId: registerVehicleReturn
+ parameters:
+ - $ref: '#/components/parameters/PublicRef'
+ - in: header
+ name: Idempotency-Key
+ required: true
+ schema:
+ type: string
+ minLength: 8
+ maxLength: 128
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RegisterReturnRequest'
+ responses:
+ '200':
+ description: Existing idempotent result replayed
+ '201':
+ description: Return registered
+ '409':
+ description: State or concurrency conflict
+ /api/v1/data-quality/issues:
+ get:
+ operationId: listDataQualityIssues
+ responses:
+ '200':
+ description: Quality issues
+ /api/v1/data-quality/issues/{public_ref}/merge-customers:
+ post:
+ operationId: mergeDuplicateCustomers
+ parameters:
+ - $ref: '#/components/parameters/PublicRef'
+ responses:
+ '200':
+ description: Merge completed
+ '409':
+ description: Issue no longer mergeable
+ /api/v1/knowledge/questions:
+ post:
+ operationId: askKnowledgeQuestion
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required: [question]
+ properties:
+ question:
+ type: string
+ minLength: 3
+ maxLength: 1000
+ responses:
+ '200':
+ description: Grounded, insufficient or unavailable answer
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GroundedAnswer'
+ /api/v1/integrations/mcp/operations-summary:
+ get:
+ operationId: mcpOperationsSummary
+ security:
+ - serviceToken: []
+ responses:
+ '200':
+ description: Read-only operational summary
+components:
+ securitySchemes:
+ serviceToken:
+ type: http
+ scheme: bearer
+ parameters:
+ PublicRef:
+ in: path
+ name: public_ref
+ required: true
+ schema:
+ type: string
+ responses:
+ NotFound:
+ description: Not found
+ schemas:
+ Health:
+ type: object
+ required: [status, service]
+ properties:
+ status:
+ const: ok
+ service:
+ const: mobilityops-api
+ Dashboard:
+ type: object
+ required: [metrics, attention_items]
+ properties:
+ metrics:
+ type: object
+ additionalProperties:
+ type: integer
+ attention_items:
+ type: array
+ items:
+ type: object
+ RegisterReturnRequest:
+ type: object
+ required: [end_odometer_km, fuel_level_percent, cleanliness_ok, damage_reported, technical_warning]
+ properties:
+ end_odometer_km:
+ type: integer
+ minimum: 0
+ fuel_level_percent:
+ type: integer
+ minimum: 0
+ maximum: 100
+ cleanliness_ok:
+ type: boolean
+ damage_reported:
+ type: boolean
+ technical_warning:
+ type: boolean
+ notes:
+ type: string
+ maxLength: 2000
+ GroundedAnswer:
+ type: object
+ required: [answer, evidence_state, sources, provider, correlation_id]
+ properties:
+ answer:
+ type: string
+ evidence_state:
+ enum: [grounded, insufficient, unavailable]
+ sources:
+ type: array
+ items:
+ type: object
+ required: [document_id, title, version, section, excerpt]
+ properties:
+ document_id: {type: string}
+ title: {type: string}
+ version: {type: string}
+ section: {type: string}
+ excerpt: {type: string}
+ provider:
+ type: string
+ correlation_id:
+ type: string
diff --git a/contracts/ragcore-contract-assumptions.md b/contracts/ragcore-contract-assumptions.md
new file mode 100644
index 0000000..b25fd20
--- /dev/null
+++ b/contracts/ragcore-contract-assumptions.md
@@ -0,0 +1,13 @@
+# RAGcore contract assumptions
+
+RAGcore is being built separately, so Claude must inspect its actual exposed API before finalizing the adapter. Do not edit RAGcore from this repository.
+
+The MobilityOps adapter needs only these capabilities:
+
+1. health/status;
+2. synchronize or ingest a document with stable ID and metadata;
+3. ask/search within one tenant/workspace/collection;
+4. receive source document ID, title, version, section and excerpt;
+5. distinguish unavailable from insufficient evidence.
+
+When the actual RAGcore API differs, adapt MobilityOps behind `KnowledgeProvider`; do not leak RAGcore-specific response shapes into UI/domain code.
diff --git a/docs/00-product-brief.md b/docs/00-product-brief.md
new file mode 100644
index 0000000..e911718
--- /dev/null
+++ b/docs/00-product-brief.md
@@ -0,0 +1,27 @@
+# Product brief
+
+## Problem
+
+A small vehicle-rental and service company works with booking data, vehicle status, inspections, maintenance records and internal procedures spread across systems and spreadsheets. Employees manually reconcile information, miss incomplete returns and spend time locating procedures.
+
+## Solution
+
+MobilityOps provides one operational view, one complete return workflow, targeted data-quality review and a source-grounded knowledge assistant. It demonstrates realistic integration boundaries without claiming to replace the source systems.
+
+## Primary users
+
+### Operations Manager
+
+Monitors attention items, reviews data-quality issues, inspects workflow failures and uses the knowledge assistant.
+
+### Rental Employee
+
+Views bookings, registers a vehicle return, records inspection details and follows grounded procedures.
+
+## Product promise
+
+Within five minutes a visitor can see persisted operational data, complete a return, trigger automation, resolve a duplicate, inspect the audit trail, ask a policy question and query live data through MCP.
+
+## Honesty statement
+
+All entities and documents are synthetic. The application behaviour and integrations are real. Do not claim measured commercial savings or production use.
diff --git a/docs/01-scope-and-non-goals.md b/docs/01-scope-and-non-goals.md
new file mode 100644
index 0000000..149cf4c
--- /dev/null
+++ b/docs/01-scope-and-non-goals.md
@@ -0,0 +1,36 @@
+# Scope and non-goals
+
+## Required PoC scope
+
+- demo login for Operations Manager and Rental Employee;
+- dashboard derived from persisted data;
+- vehicle list/detail;
+- booking list/detail;
+- complete vehicle-return command;
+- five data-quality rule types;
+- customer duplicate review and merge;
+- audit log;
+- workflow/outbox view;
+- RAGcore-backed knowledge assistant plus demo fallback;
+- two n8n workflow definitions, one required for the live demo;
+- four read-only MCP tools published through ITWorx MCP Hub;
+- deterministic demo reset.
+
+## Non-goals
+
+Do not build:
+
+- accounting, invoices, payments or pricing;
+- public search or customer reservation pages;
+- generic CRM functionality;
+- workshop inventory or parts management;
+- document OCR or broad document-management features;
+- autonomous writes through MCP;
+- a second vector database or RAG pipeline inside MobilityOps;
+- complex notifications, email delivery or mobile apps;
+- multi-tenant administration beyond a fixed demo tenant;
+- ML predictions based on synthetic data.
+
+## Scope-change rule
+
+A feature may be added only when it is necessary to satisfy an existing acceptance criterion. Nice-to-have ideas go to `docs/deferred.md` and are not implemented during the PoC.
diff --git a/docs/02-user-stories.md b/docs/02-user-stories.md
new file mode 100644
index 0000000..4b76329
--- /dev/null
+++ b/docs/02-user-stories.md
@@ -0,0 +1,32 @@
+# User stories
+
+## Dashboard
+
+- As an Operations Manager, I can see available, rented, maintenance and blocked vehicles so I know the current operational state.
+- As an Operations Manager, I can open an attention item and reach the relevant record directly.
+
+## Return workflow
+
+- As a Rental Employee, I can register a return with mileage, fuel, cleanliness, damage and technical notes.
+- The system validates mileage and required fields before deriving the new vehicle state.
+- A problematic return is committed, audited and blocked even when n8n is unavailable.
+
+## Data quality
+
+- As an Operations Manager, I can review possible duplicate customers with explainable match signals.
+- I can merge, reject or defer the duplicate and see an audit event.
+- I can review falling mileage, overlapping bookings, missing required data and status conflicts.
+
+## Knowledge
+
+- As an employee, I can ask a question about one of the supplied procedures.
+- The answer contains sources and document versions, or explicitly states that evidence is insufficient.
+
+## Automation
+
+- As an Operations Manager, I can see pending, successful and failed workflow deliveries.
+- Failed deliveries can be retried safely without duplicating domain actions.
+
+## MCP
+
+- As an authorised external AI client, I can request the operations summary, attention vehicles, vehicle details and procedure search without database or write access.
diff --git a/docs/03-architecture.md b/docs/03-architecture.md
new file mode 100644
index 0000000..a81544f
--- /dev/null
+++ b/docs/03-architecture.md
@@ -0,0 +1,55 @@
+# Architecture
+
+## Context
+
+```text
+External AI client
+ |
+ v
+ITWorx MCP Hub ---------> RAGcore
+ | ^
+ v |
+MobilityOps API <------ MobilityOps UI
+ |
+ +---- PostgreSQL
+ |
+ +---- Outbox dispatcher ----> n8n
+```
+
+## Ownership
+
+### MobilityOps
+
+Owns vehicles, customers, bookings, inspections, maintenance summaries, data-quality issues, audit events and integration delivery state.
+
+### RAGcore
+
+Owns procedure ingestion, chunking, embeddings, retrieval and grounded answer generation. MobilityOps stores only document references and last-known synchronization state where useful.
+
+### ITWorx MCP Hub
+
+Owns MCP transport, tool publication, client policy and central tool audit. The hub calls versioned MobilityOps APIs and RAGcore APIs. It never reads the MobilityOps database.
+
+### n8n
+
+Receives committed events and orchestrates secondary work. It does not decide whether a vehicle return is valid or what the canonical vehicle status is.
+
+## Reliability boundaries
+
+1. A return command and its outbox event commit in one database transaction.
+2. Outbox delivery is at-least-once; the n8n workflow and callback endpoint are idempotent by event ID.
+3. RAGcore failure disables knowledge answers only.
+4. MCP Hub failure does not affect the MobilityOps web application.
+5. n8n failure leaves events pending with bounded retries and visible status.
+
+## Security boundaries
+
+- browser uses application authentication;
+- service-to-service calls use scoped tokens;
+- no arbitrary SQL, shell or generic HTTP tools;
+- read-only MCP tools only for the PoC;
+- user and service actions are distinguishable in audit events.
+
+## Deployment
+
+MobilityOps may run in its own Compose project. RAGcore and MCP Hub are configured by URL and credentials and may be on another internal Docker network or behind TLS endpoints.
diff --git a/docs/04-domain-model.md b/docs/04-domain-model.md
new file mode 100644
index 0000000..d83cfb4
--- /dev/null
+++ b/docs/04-domain-model.md
@@ -0,0 +1,118 @@
+# Domain model
+
+Use UUID primary keys internally. Human-facing references are immutable unique strings.
+
+## User
+
+- id
+- public_ref (`USR-...`)
+- display_name
+- role: `operations_manager | rental_employee`
+- active
+
+## Customer
+
+- id
+- public_ref (`CUS-0001`)
+- first_name, last_name
+- email, phone
+- postal_code, city
+- date_of_birth (optional)
+- merged_into_customer_id (optional)
+- created_at, updated_at
+
+## Vehicle
+
+- id
+- public_ref (`MO-001`)
+- make, model, model_year
+- registration_number
+- location
+- operational_status: `available | rented | cleaning | maintenance | blocked`
+- odometer_km
+- next_service_km
+- active
+- version for optimistic concurrency
+
+## Booking
+
+- id
+- public_ref (`BK-...`)
+- customer_id, vehicle_id
+- starts_at, ends_at
+- status: `reserved | active | returned | cancelled | blocked`
+- start_odometer_km, end_odometer_km
+- requirements_complete
+
+Database invariant: active/reserved bookings for the same vehicle may not overlap after validation. A deliberate seeded conflict may be imported as an external-data quality issue through a controlled bypass, never created through the normal command API.
+
+## Inspection
+
+- id
+- public_ref
+- booking_id, vehicle_id
+- type: `checkout | return`
+- fuel_level_percent
+- cleanliness_ok
+- damage_reported
+- technical_warning
+- notes
+- odometer_km
+- completed_at, completed_by
+
+## MaintenanceRecord
+
+- id
+- vehicle_id
+- occurred_at
+- odometer_km
+- category
+- summary
+
+## DataQualityIssue
+
+- id
+- public_ref
+- rule_type:
+ - `possible_duplicate_customer`
+ - `missing_required_field`
+ - `odometer_regression`
+ - `booking_overlap`
+ - `vehicle_status_conflict`
+- entity_type, entity_id
+- severity: `low | medium | high`
+- status: `open | deferred | resolved | rejected`
+- evidence_json
+- proposed_action_json
+- detected_at, resolved_at, resolved_by
+
+## OutboxEvent / WorkflowRun
+
+- id / event_id
+- event_type
+- aggregate_type, aggregate_id
+- payload_json
+- occurred_at
+- delivery_status: `pending | delivering | succeeded | failed`
+- attempts, next_attempt_at, last_error
+- external_run_id
+
+## AuditEvent
+
+- id
+- actor_type: `user | service | system`
+- actor_id / actor_label
+- action
+- entity_type, entity_id
+- correlation_id
+- before_json, after_json, metadata_json
+- occurred_at
+
+## Required invariants
+
+- a vehicle return cannot reduce the canonical odometer;
+- a return with a lower submitted reading is recorded as an inspection and issue, while canonical odometer remains unchanged;
+- damage or a critical warning blocks the vehicle;
+- merged customers remain as tombstones linked to the survivor;
+- resolving a quality issue and applying its correction is transactional and audited;
+- public references never change after creation.
diff --git a/docs/05-api-contract.md b/docs/05-api-contract.md
new file mode 100644
index 0000000..5b851ba
--- /dev/null
+++ b/docs/05-api-contract.md
@@ -0,0 +1,75 @@
+# API contract summary
+
+The machine-readable baseline is `contracts/openapi.yaml`.
+
+## Authentication
+
+The demo may use signed server-issued sessions or short-lived JWTs. Demo-role buttons create an authenticated session; they do not bypass authorization middleware.
+
+## Required routes
+
+### System and demo
+
+- `GET /health`
+- `GET /api/v1/system/status`
+- `POST /api/v1/demo/login`
+- `POST /api/v1/demo/reset` — Operations Manager only
+
+### Dashboard
+
+- `GET /api/v1/dashboard`
+
+### Vehicles
+
+- `GET /api/v1/vehicles`
+- `GET /api/v1/vehicles/{public_ref}`
+
+### Bookings and return
+
+- `GET /api/v1/bookings`
+- `GET /api/v1/bookings/{public_ref}`
+- `POST /api/v1/bookings/{public_ref}/return`
+
+Return commands require an `Idempotency-Key` header and optimistic version where relevant.
+
+### Data quality
+
+- `GET /api/v1/data-quality/issues`
+- `GET /api/v1/data-quality/issues/{public_ref}`
+- `POST /api/v1/data-quality/issues/{public_ref}/defer`
+- `POST /api/v1/data-quality/issues/{public_ref}/reject`
+- `POST /api/v1/data-quality/issues/{public_ref}/merge-customers`
+
+### Knowledge
+
+- `POST /api/v1/knowledge/questions`
+- `GET /api/v1/knowledge/status`
+
+### Automation and audit
+
+- `GET /api/v1/workflows`
+- `POST /api/v1/workflows/{event_id}/retry`
+- `GET /api/v1/audit`
+
+### MCP-provider endpoints
+
+Service-token protected:
+
+- `GET /api/v1/integrations/mcp/operations-summary`
+- `GET /api/v1/integrations/mcp/attention-vehicles`
+- `GET /api/v1/integrations/mcp/vehicles/{public_ref}`
+
+Knowledge search may be routed by the Hub directly to RAGcore or through a narrowly scoped MobilityOps façade. Use the contract chosen in `docs/10-mcp-hub-integration.md`.
+
+## Error shape
+
+```json
+{
+ "error": {
+ "code": "ODOMETER_REGRESSION",
+ "message": "The submitted reading is below the current canonical odometer.",
+ "correlation_id": "...",
+ "details": {}
+ }
+}
+```
diff --git a/docs/06-ui-ux.md b/docs/06-ui-ux.md
new file mode 100644
index 0000000..6bf8d1a
--- /dev/null
+++ b/docs/06-ui-ux.md
@@ -0,0 +1,78 @@
+# UI and UX specification
+
+## Design goal
+
+A recruiter or non-technical manager must understand the business value within thirty seconds. Use a restrained professional interface, clear language and direct links from attention items to records.
+
+## Persistent demo disclosure
+
+Show a compact banner on every authenticated page:
+
+> Synthetic demo environment — no real customer or vehicle data.
+
+## Navigation
+
+- Dashboard
+- Vehicles
+- Bookings
+- Data Quality
+- Knowledge
+- Automation
+- Audit
+
+## Demo login
+
+Two primary buttons:
+
+- Open as Operations Manager
+- Open as Rental Employee
+
+Also show one sentence explaining what each role can demonstrate.
+
+## Dashboard
+
+Top metrics:
+
+- available;
+- rented;
+- maintenance;
+- blocked;
+- open quality issues;
+- failed/pending workflows.
+
+Sections:
+
+1. Attention required — sorted by severity and proximity to next booking.
+2. Today — departures, returns and incomplete inspections.
+3. Recent automation — latest five runs.
+
+## Vehicle list/detail
+
+List filters: status, location, attention only. Detail tabs: overview, bookings, inspections, maintenance, quality issues, audit.
+
+## Booking return flow
+
+Use a short single-page form with visible validation. After submission show a result summary explaining:
+
+- persisted inspection;
+- resulting vehicle status;
+- created quality issue if any;
+- queued automation event;
+- next booking risk.
+
+## Data Quality Workbench
+
+Use a two-column comparison for duplicate customers with matching signals and a deliberate field-selection merge step. Other issue types need evidence, proposed resolution and an audit preview.
+
+## Knowledge
+
+Chat-like question box is acceptable, but the answer must prioritize source cards: title, version, section and excerpt. Show explicit unavailable and insufficient-evidence states.
+
+## Accessibility
+
+- keyboard-operable controls;
+- visible focus;
+- semantic headings and form labels;
+- status not encoded by color alone;
+- responsive at 360 px width;
+- no hover-only actions.
diff --git a/docs/07-data-quality.md b/docs/07-data-quality.md
new file mode 100644
index 0000000..b6af10c
--- /dev/null
+++ b/docs/07-data-quality.md
@@ -0,0 +1,47 @@
+# Data-quality rules
+
+Run rules after seed/import, after relevant commands and through an explicit scan service. Rules must be deterministic and explainable.
+
+## DQ-01 Possible duplicate customer
+
+Signals and example weights:
+
+- exact normalized email: 60;
+- exact normalized phone: 50;
+- exact postal code: 10;
+- strong normalized full-name similarity: up to 30.
+
+Open an issue at score >= 70. Store individual signals; do not expose a mysterious AI-only confidence.
+
+Resolution:
+
+- merge into selected survivor;
+- reject as not duplicate;
+- defer.
+
+Merge rewires booking references, preserves the loser as a tombstone and audits before/after values.
+
+## DQ-02 Missing required field
+
+Required for active customers: first name, last name and at least one of email or phone. Required for active vehicles: registration number, make, model and location.
+
+## DQ-03 Odometer regression
+
+Flag an inspection or maintenance reading below the canonical odometer. Never lower the canonical value automatically.
+
+## DQ-04 Booking overlap
+
+Flag overlapping `reserved` or `active` bookings for one vehicle. Normal write APIs reject new overlaps; the seed/import path may create one controlled legacy conflict.
+
+## DQ-05 Vehicle status conflict
+
+Examples:
+
+- status `available` while an active booking exists;
+- status `rented` without an active booking;
+- status `available` while critical open quality issue exists;
+- status `maintenance` with an active booking.
+
+## Lifecycle
+
+Detection is idempotent by `(rule_type, entity_type, entity_id, evidence fingerprint)` while open. Resolved issues remain historical. Reintroduced evidence creates a new issue linked to the prior issue where useful.
diff --git a/docs/08-return-workflow.md b/docs/08-return-workflow.md
new file mode 100644
index 0000000..525cd06
--- /dev/null
+++ b/docs/08-return-workflow.md
@@ -0,0 +1,42 @@
+# Vehicle-return workflow
+
+## Input
+
+- booking public reference;
+- submitted end odometer;
+- fuel level 0–100;
+- cleanliness flag;
+- damage flag;
+- technical warning flag;
+- notes;
+- idempotency key.
+
+## Transaction
+
+1. Authorize Rental Employee or Operations Manager.
+2. Lock booking and vehicle rows.
+3. Reject cancelled/already-returned booking unless idempotency replay matches.
+4. Validate required fields and submitted reading against booking start reading.
+5. Create a return inspection.
+6. Set booking to returned and store submitted end reading.
+7. If submitted reading >= canonical odometer, update canonical odometer.
+8. Otherwise create `odometer_regression`; keep canonical odometer unchanged.
+9. Derive vehicle state:
+ - damage or technical warning -> `blocked`;
+ - service threshold reached -> `maintenance`;
+ - otherwise -> `cleaning`.
+10. Create quality issues for contradictions.
+11. Create audit events.
+12. Insert `vehicle.returned.v1` outbox event.
+13. Commit once.
+
+## Post-commit n8n behaviour
+
+The event contains enough identifiers to retrieve current state, not an uncontrolled full database snapshot. n8n may create a cleaning/maintenance follow-up through a narrow callback API and return its run ID.
+
+## Failure behaviour
+
+- n8n unavailable: return succeeds; event stays pending.
+- duplicate event delivery: n8n and callback are idempotent by event ID.
+- callback fails: workflow appears failed and is retryable.
+- concurrent return submissions: only one succeeds; same idempotency key replays the original response.
diff --git a/docs/09-ragcore-integration.md b/docs/09-ragcore-integration.md
new file mode 100644
index 0000000..8e9cd40
--- /dev/null
+++ b/docs/09-ragcore-integration.md
@@ -0,0 +1,65 @@
+# RAGcore integration
+
+## Objective
+
+Use the existing central RAGcore project. MobilityOps must not implement embeddings, vector storage, chunking or its own answer-generation pipeline.
+
+## Namespace
+
+- tenant: `northstar-mobility-demo`
+- workspace: `mobilityops`
+- collection: `internal-procedures`
+
+These values are configurable.
+
+## Source documents
+
+The ten Markdown files under `knowledge/procedures/` are authoritative PoC sources. Keep their IDs, versions and effective dates as metadata.
+
+## Required adapter interface
+
+```python
+class KnowledgeProvider(Protocol):
+ async def health(self) -> KnowledgeHealth: ...
+ async def ask(self, question: str, actor: ActorContext) -> GroundedAnswer: ...
+ async def sync_manifest(self, documents: list[KnowledgeDocumentRef]) -> SyncResult: ...
+```
+
+Implement:
+
+- `RAGcoreKnowledgeProvider`;
+- `DemoKnowledgeProvider` using deterministic keyword/BM25-style local source retrieval only.
+
+The demo provider is a resilience/test adapter, not a second RAG platform. It must return extracted source passages and a template summary; it must not pretend to be generative AI.
+
+## Answer contract
+
+```json
+{
+ "answer": "...",
+ "evidence_state": "grounded | insufficient | unavailable",
+ "sources": [
+ {
+ "document_id": "damage-procedure",
+ "title": "Damage handling procedure",
+ "version": "1.3",
+ "section": "2. Immediate actions",
+ "excerpt": "..."
+ }
+ ],
+ "provider": "ragcore",
+ "correlation_id": "..."
+}
+```
+
+## Safety
+
+- send actor scope and tenant/workspace with each request;
+- enforce source allow-list for this PoC;
+- never fall back to general model knowledge silently;
+- no customer PII is indexed in RAGcore;
+- log question metadata and source IDs, not unnecessary full prompts.
+
+## Degraded mode
+
+When RAGcore is unreachable, return `unavailable` and keep all operational functions available. When evidence is weak, return `insufficient` with the best source matches and no fabricated procedure.
diff --git a/docs/10-mcp-hub-integration.md b/docs/10-mcp-hub-integration.md
new file mode 100644
index 0000000..aa284b0
--- /dev/null
+++ b/docs/10-mcp-hub-integration.md
@@ -0,0 +1,56 @@
+# ITWorx MCP Hub integration
+
+## Objective
+
+Publish four read-only MobilityOps capabilities through the existing central ITWorx MCP Hub. MobilityOps does not host MCP transport itself.
+
+## Provider registration
+
+- provider ID: `mobilityops`
+- API base: configurable internal MobilityOps API URL
+- authentication: scoped service token
+- mode: read-only
+- required scope: `mobilityops.read`
+
+## Tools
+
+The machine-readable definitions are in `contracts/mcp-tools.json`.
+
+1. `mobilityops_get_operations_summary`
+2. `mobilityops_list_attention_vehicles`
+3. `mobilityops_get_vehicle_details`
+4. `mobilityops_search_knowledge`
+
+## Routing
+
+Operational tools:
+
+```text
+AI client -> MCP Hub -> MobilityOps provider API
+```
+
+Knowledge tool:
+
+Preferred:
+
+```text
+AI client -> MCP Hub -> RAGcore workspace mobilityops
+```
+
+If the Hub requires a single provider boundary, route through a narrow MobilityOps knowledge façade that calls RAGcore. Do not duplicate retrieval logic.
+
+## Restrictions
+
+Do not expose:
+
+- generic SQL;
+- arbitrary URL fetching;
+- arbitrary shell commands;
+- return registration;
+- customer merge;
+- booking or vehicle mutation;
+- secrets or raw service configuration.
+
+## Audit
+
+The Hub owns central tool-call audit. MobilityOps also records service requests that reach its provider APIs with tool name, correlation ID, client/service identity and result status.
diff --git a/docs/11-n8n-integration.md b/docs/11-n8n-integration.md
new file mode 100644
index 0000000..5aa135c
--- /dev/null
+++ b/docs/11-n8n-integration.md
@@ -0,0 +1,41 @@
+# n8n integration
+
+## Role
+
+n8n orchestrates secondary cross-system work after MobilityOps commits canonical state. It is not the domain engine.
+
+## Required live workflow: return processing
+
+Input: `vehicle.returned.v1` webhook event.
+
+Steps:
+
+1. validate event type and schema;
+2. derive a follow-up category from the already-calculated state;
+3. call the narrow MobilityOps callback endpoint with event ID and follow-up summary;
+4. return a stable workflow result;
+5. on errors, fail visibly so the outbox dispatcher can retry.
+
+The starter export is `n8n/mobilityops-return-processing.json`. Claude may correct its credentials and callback route but must preserve idempotency.
+
+## Optional second workflow: knowledge sync
+
+Input: manual trigger or manifest-changed event.
+
+Steps:
+
+1. read the fixed knowledge manifest;
+2. call RAGcore ingestion/sync API;
+3. record per-document results through MobilityOps integration status API.
+
+This workflow is useful but must not delay the core demo if RAGcore's final API is not ready.
+
+## Outbox dispatcher
+
+- polls pending records;
+- claims with `FOR UPDATE SKIP LOCKED` or equivalent;
+- sends event with timeout;
+- exponential backoff with a small maximum attempt count;
+- supports explicit manual retry;
+- preserves last error and response metadata;
+- does not hold a database transaction open during network I/O.
diff --git a/docs/12-security-and-audit.md b/docs/12-security-and-audit.md
new file mode 100644
index 0000000..a330a44
--- /dev/null
+++ b/docs/12-security-and-audit.md
@@ -0,0 +1,47 @@
+# Security and audit
+
+## Demo authentication
+
+Role buttons may create a session for a seeded demo identity. All API routes still enforce authorization. Demo reset and customer merge require Operations Manager.
+
+## Service authentication
+
+Use separate scoped credentials for:
+
+- n8n callbacks;
+- MCP Hub provider calls;
+- RAGcore calls.
+
+Never reuse browser session secrets.
+
+## Sensitive data
+
+All data are synthetic, but design as though data were sensitive:
+
+- do not log full tokens;
+- avoid logging complete customer payloads;
+- validate and size-limit free-text inputs;
+- escape rendered content;
+- use CSRF protection for cookie sessions or use a secure token model;
+- restrictive CORS;
+- secrets from environment or mounted secret files.
+
+## Audit events
+
+Required actions:
+
+- demo login;
+- return registration;
+- vehicle status change;
+- data-quality issue creation and resolution;
+- customer merge;
+- workflow retry;
+- demo reset;
+- MCP provider request;
+- knowledge question status and source IDs.
+
+Audit is append-only through the application. Provide filters by actor, action, entity and correlation ID.
+
+## Confirmation
+
+No write-capable MCP actions exist in this PoC. Destructive UI actions such as demo reset and customer merge require explicit confirmation.
diff --git a/docs/13-seed-and-demo-scenarios.md b/docs/13-seed-and-demo-scenarios.md
new file mode 100644
index 0000000..0b53005
--- /dev/null
+++ b/docs/13-seed-and-demo-scenarios.md
@@ -0,0 +1,54 @@
+# Seed and demo scenarios
+
+## Dataset
+
+`seed/generate_seed.py` creates a deterministic snapshot for a chosen anchor date and random seed. The committed CSV files are generated with:
+
+```bash
+python seed/generate_seed.py --anchor 2026-08-01 --seed 20260801
+```
+
+Target scale:
+
+- 50 vehicles;
+- 180 customers including three duplicate pairs;
+- 220 historical and 25 current/future bookings;
+- realistic inspections and maintenance records;
+- fixed quality and workflow scenarios.
+
+## Required named scenarios
+
+### S1 — Odometer regression return
+
+Booking `BK-DEMO-RETURN` for vehicle `MO-024` is active. Submit a return reading below the canonical odometer. Expected: inspection saved, canonical odometer unchanged, vehicle blocked or cleaning according to other flags, quality issue created, event queued and audit visible.
+
+### S2 — Duplicate customer merge
+
+Customers `CUS-0012` and `CUS-0178` share normalized contact data and similar names. Expected: issue with explainable signals, merge rewires bookings, loser becomes tombstone, audit preserved.
+
+### S3 — Missing inspection before next booking
+
+Vehicle `MO-031` has a near-future booking and an unresolved operational attention item. Expected: dashboard links to its record.
+
+### S4 — Legacy booking overlap
+
+Vehicle `MO-016` has two imported overlapping reservations. Expected: visible quality issue; normal booking command would reject the same overlap.
+
+### S5 — Failed workflow
+
+One seeded outbox/workflow record is failed with a safe simulated connection error. Expected: dashboard and Automation page show it; Operations Manager can retry.
+
+### S6 — Grounded damage question
+
+Question: “What must I do when a vehicle returns with damage?” Expected: answer cites damage handling and return inspection procedures.
+
+## Demo reset
+
+Reset must:
+
+- require Operations Manager;
+- rebuild the deterministic dataset;
+- re-establish scenario references;
+- clear non-seed audit/workflow state;
+- complete safely and visibly;
+- be covered by a test.
diff --git a/docs/14-testing-and-acceptance.md b/docs/14-testing-and-acceptance.md
new file mode 100644
index 0000000..3cdbc04
--- /dev/null
+++ b/docs/14-testing-and-acceptance.md
@@ -0,0 +1,74 @@
+# Testing and acceptance
+
+## Test layers
+
+### Unit/domain
+
+- vehicle status derivation;
+- return mileage handling;
+- duplicate scoring and merge rules;
+- booking overlap detection;
+- status conflict rules;
+- outbox retry schedule;
+- RAG answer-state mapping.
+
+### Database/integration
+
+- migrations from empty database;
+- constraints and transaction rollback;
+- concurrent return submissions;
+- idempotency replay;
+- customer merge atomicity;
+- outbox claim concurrency;
+- demo reset determinism.
+
+### API contract
+
+Validate OpenAPI and test authentication, role boundaries, filtering, error shape and service-token routes.
+
+### External adapters
+
+Use contract tests with fake HTTP servers for RAGcore, n8n and MCP Hub-facing APIs. Test timeout, unavailable, malformed response and success.
+
+### Playwright
+
+The five-minute demo must be automated:
+
+1. login as Operations Manager;
+2. verify dashboard metrics are loaded;
+3. open active demo booking;
+4. register odometer-regression return;
+5. verify issue and queued workflow;
+6. resolve/merge the duplicate customer scenario;
+7. ask the damage procedure question and inspect citations;
+8. inspect audit entries;
+9. verify responsive navigation at mobile width.
+
+## Clean-checkout acceptance
+
+From a clean checkout:
+
+1. copy `.env.example` to `.env`;
+2. run one documented bootstrap command;
+3. migrations and seed complete automatically or via one explicit command;
+4. web and API health become green;
+5. all automated tests pass;
+6. no secret is required for the demo provider;
+7. RAGcore unavailable state is clear and operational pages still work;
+8. n8n unavailable does not roll back a vehicle return;
+9. MCP provider endpoints are read-only and service-protected;
+10. no visible route contains dead actions or placeholder data.
+
+## Final evidence
+
+Create `artifacts/evidence/final-summary.md` containing:
+
+- commit/tag;
+- exact commands;
+- test counts;
+- screenshots of the seven main pages;
+- RAGcore success and unavailable evidence;
+- n8n success and retry evidence;
+- MCP tool sample calls;
+- known PoC limitations;
+- truthful portfolio wording.
diff --git a/docs/15-build-plan.md b/docs/15-build-plan.md
new file mode 100644
index 0000000..0090225
--- /dev/null
+++ b/docs/15-build-plan.md
@@ -0,0 +1,113 @@
+# Autonomous build plan
+
+Claude must complete all milestones in one continuous run where possible.
+
+## M0 — Reproducible foundation
+
+Read: `README.md`, `docs/03-architecture.md`, `docs/04-domain-model.md`, `docs/14-testing-and-acceptance.md`.
+
+Deliver:
+
+- stable backend/frontend project structure;
+- dependency lockfiles;
+- Compose health and clean-checkout bootstrap;
+- migrations framework;
+- CI-friendly commands;
+- initial lint/test gates.
+
+Validate: clean build, API/web health, baseline tests.
+
+## M1 — Operational core
+
+Read: `docs/02-user-stories.md`, `docs/05-api-contract.md`, `docs/06-ui-ux.md`, `docs/13-seed-and-demo-scenarios.md`.
+
+Deliver:
+
+- demo authentication and authorization;
+- deterministic seed import/reset;
+- dashboard;
+- vehicle and booking list/detail;
+- audit foundation.
+
+Validate: real persisted metrics, role tests, responsive smoke test.
+
+## M2 — Vehicle return vertical slice
+
+Read: `docs/08-return-workflow.md`, `contracts/events.schema.json`.
+
+Deliver:
+
+- complete transactional return command;
+- idempotency and concurrency handling;
+- status derivation;
+- outbox record;
+- result UI and audit.
+
+Validate: success, odometer regression, replay, concurrent submission and rollback tests.
+
+## M3 — Data Quality Workbench
+
+Read: `docs/07-data-quality.md`.
+
+Deliver:
+
+- five rules;
+- issue scan/lifecycle;
+- duplicate comparison and transactional merge;
+- issue UI and dashboard attention integration.
+
+Validate: seeded scenarios, idempotent scans, merge rewiring and audit.
+
+## M4 — n8n automation
+
+Read: `docs/11-n8n-integration.md`, `n8n/README.md`.
+
+Deliver:
+
+- dispatcher with retry/backoff;
+- working return workflow and callback;
+- Automation UI;
+- failed seeded run and manual retry.
+
+Validate: n8n online success, offline pending behaviour, duplicate delivery idempotency.
+
+## M5 — RAGcore knowledge integration
+
+Read: `docs/09-ragcore-integration.md`, `knowledge/manifest.json`.
+
+Deliver:
+
+- provider interface;
+- deterministic demo provider;
+- RAGcore adapter against the actual available contract;
+- Knowledge UI with source cards and honest states;
+- document sync support when feasible.
+
+Validate: grounded demo question, insufficient evidence, timeout/unavailable and source metadata tests.
+
+## M6 — ITWorx MCP Hub publication
+
+Read: `docs/10-mcp-hub-integration.md`, `contracts/mcp-tools.json`.
+
+Deliver:
+
+- protected provider APIs;
+- registration/configuration documentation or automation compatible with the Hub;
+- four validated read-only tools;
+- service audit events.
+
+Validate: tool schemas, authorization denial, sample Hub calls, no write or generic tool exposure.
+
+## M7 — Portfolio polish and final acceptance
+
+Read: `docs/06-ui-ux.md`, `docs/14-testing-and-acceptance.md`, `docs/16-portfolio-case-study.md`.
+
+Deliver:
+
+- remove rough edges and placeholders;
+- complete Playwright demo;
+- screenshots and architecture diagram;
+- concise deployment/runbook;
+- final evidence, version and case-study text.
+
+Validate: all acceptance criteria from a clean checkout.
diff --git a/docs/16-portfolio-case-study.md b/docs/16-portfolio-case-study.md
new file mode 100644
index 0000000..18dee71
--- /dev/null
+++ b/docs/16-portfolio-case-study.md
@@ -0,0 +1,43 @@
+# Portfolio case-study template
+
+## Title
+
+MobilityOps — connected operations for vehicle rental and service teams
+
+## One-sentence summary
+
+A working synthetic-data PoC that unifies vehicle and booking operations, automates vehicle returns, surfaces data-quality problems and exposes grounded internal knowledge through reusable RAG and MCP platforms.
+
+## Problem
+
+Operational information was modelled as fragmented across booking, vehicle and procedure sources, causing manual checks, inconsistent states and slow access to instructions.
+
+## Approach
+
+- MobilityOps owns canonical operational data and business rules.
+- A transactional return workflow persists inspections, derives status and queues post-commit orchestration.
+- Explainable rules detect duplicates, mileage regressions, overlaps, missing fields and status conflicts.
+- RAGcore provides source-grounded procedure answers.
+- ITWorx MCP Hub exposes narrowly scoped read-only AI tools.
+- n8n orchestrates secondary processing without becoming the business-rule engine.
+
+## Demonstrable results
+
+Use only facts actually measured in the final build, such as:
+
+- 50 deterministic synthetic vehicles and 245 bookings;
+- five automated data-quality rule types;
+- one end-to-end return workflow;
+- ten versioned source procedures;
+- four read-only MCP tools;
+- tested degraded modes for RAGcore and n8n.
+
+Do not claim real customer adoption, production use or percentage time savings.
+
+## Stack
+
+React, TypeScript, FastAPI, PostgreSQL, n8n, RAGcore, ITWorx MCP Hub, Docker Compose and Playwright.
+
+## Demo disclosure
+
+All data and the represented company are fictitious. The software behaviour, integration contracts, audit trail and validation are implemented as a real proof of concept.
diff --git a/docs/17-runbook.md b/docs/17-runbook.md
new file mode 100644
index 0000000..2785a7d
--- /dev/null
+++ b/docs/17-runbook.md
@@ -0,0 +1,28 @@
+# PoC runbook
+
+## Bootstrap
+
+```bash
+cp .env.example .env
+docker compose up --build -d
+```
+
+Claude must replace this scaffold runbook with exact migration, seed, test and integration commands after implementation.
+
+## Required operational checks
+
+- API and web health;
+- database migration level;
+- pending/failed outbox count;
+- RAGcore provider state;
+- n8n connectivity;
+- MCP provider endpoint authorization;
+- deterministic demo reset.
+
+## Recovery expectations
+
+- database restart: application reconnects;
+- n8n outage: events remain pending and retryable;
+- RAGcore outage: knowledge shows unavailable, operations continue;
+- MCP Hub outage: web application unaffected;
+- failed demo experiment: Operations Manager reset restores the seed.
diff --git a/docs/deferred.md b/docs/deferred.md
new file mode 100644
index 0000000..6a606f3
--- /dev/null
+++ b/docs/deferred.md
@@ -0,0 +1,14 @@
+# Deferred ideas
+
+Not part of the PoC:
+
+- document OCR and intake;
+- pricing recommendations;
+- sales workflows;
+- maintenance work orders and parts;
+- calendar view;
+- notifications and email delivery;
+- write-capable MCP tools with approval;
+- true multi-tenancy;
+- production SSO;
+- advanced analytics.
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
new file mode 100644
index 0000000..f353753
--- /dev/null
+++ b/frontend/Dockerfile
@@ -0,0 +1,10 @@
+FROM node:22-alpine AS build
+WORKDIR /app
+COPY package.json tsconfig.json vite.config.ts index.html ./
+COPY src ./src
+RUN npm install && npm run build
+
+FROM nginx:1.27-alpine
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+COPY --from=build /app/dist /usr/share/nginx/html
+EXPOSE 80
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..0456058
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ MobilityOps
+
+
+
+
+
+
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
new file mode 100644
index 0000000..c753a38
--- /dev/null
+++ b/frontend/nginx.conf
@@ -0,0 +1,22 @@
+server {
+ listen 80;
+ server_name _;
+
+ root /usr/share/nginx/html;
+ index index.html;
+
+ location /api/ {
+ proxy_pass http://api:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location /health {
+ proxy_pass http://api:8000/health;
+ }
+
+ location / {
+ try_files $uri /index.html;
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..c79aa78
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "mobilityops-web",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 0.0.0.0",
+ "build": "tsc -b && vite build",
+ "preview": "vite preview --host 0.0.0.0"
+ },
+ "dependencies": {
+ "@vitejs/plugin-react": "latest",
+ "vite": "latest",
+ "typescript": "latest",
+ "react": "latest",
+ "react-dom": "latest"
+ },
+ "devDependencies": {
+ "@types/react": "latest",
+ "@types/react-dom": "latest"
+ }
+}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000..dfced3f
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,49 @@
+import { useEffect, useState } from "react";
+
+const API = import.meta.env.VITE_API_BASE_URL ?? "";
+
+type Status = {
+ service: string;
+ environment: string;
+ demo_mode: boolean;
+ knowledge_provider: string;
+ scaffold: boolean;
+};
+
+export function App() {
+ const [status, setStatus] = useState(null);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ fetch(`${API}/api/v1/system/status`)
+ .then((response) => {
+ if (!response.ok) throw new Error(`API returned ${response.status}`);
+ return response.json();
+ })
+ .then(setStatus)
+ .catch((reason: unknown) => setError(reason instanceof Error ? reason.message : "Unknown error"));
+ }, []);
+
+ return (
+
+
+ Synthetic proof of concept
+ MobilityOps
+ Connected operations for vehicle rental and service teams.
+
+
+ Scaffold status
+ {error && API unavailable: {error}
}
+ {!error && !status && Connecting to the MobilityOps API…
}
+ {status && (
+
+ - Service
- {status.service}
+ - Environment
- {status.environment}
+ - Knowledge provider
- {status.knowledge_provider}
+
+ )}
+ This is the bootable project scaffold. Claude must replace it with the complete scoped application described in the build pack.
+
+
+ );
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 0000000..e7e51d8
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,10 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { App } from "./App";
+import "./styles.css";
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
new file mode 100644
index 0000000..570dbc4
--- /dev/null
+++ b/frontend/src/styles.css
@@ -0,0 +1,19 @@
+:root {
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
+ color: #172033;
+ background: #f3f6fa;
+ font-synthesis: none;
+}
+* { box-sizing: border-box; }
+body { margin: 0; min-width: 320px; }
+.shell { width: min(920px, calc(100% - 32px)); margin: 0 auto; padding: 64px 0; }
+.hero { margin-bottom: 28px; }
+.eyebrow { color: #315d73; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; font-size: .8rem; }
+h1 { margin: 6px 0; font-size: clamp(2.4rem, 7vw, 4.8rem); line-height: 1; }
+.hero > p:last-child { color: #526173; font-size: 1.2rem; }
+.panel { background: white; border: 1px solid #dce3eb; border-radius: 18px; padding: 24px; box-shadow: 0 10px 30px rgba(24,40,64,.06); }
+dl { display: grid; gap: 12px; }
+dl div { display: flex; justify-content: space-between; gap: 24px; border-bottom: 1px solid #edf1f5; padding-bottom: 10px; }
+dt { color: #607084; } dd { margin: 0; font-weight: 700; }
+.note { margin-top: 24px; color: #607084; }
+.error { color: #9a2530; }
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..dfc3fbe
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "allowJs": false,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx"
+ },
+ "include": ["src"],
+ "references": []
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..081c8d9
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+});
diff --git a/knowledge/manifest.json b/knowledge/manifest.json
new file mode 100644
index 0000000..09ac031
--- /dev/null
+++ b/knowledge/manifest.json
@@ -0,0 +1,107 @@
+{
+ "tenant": "northstar-mobility-demo",
+ "workspace": "mobilityops",
+ "collection": "internal-procedures",
+ "documents": [
+ {
+ "path": "knowledge/procedures/01-vehicle-checkout.md",
+ "sha256": "31f9dd030e06f80b4320030df1749d170e6196aed38cb3021a87ca6fb8be3318",
+ "document_id": "vehicle-checkout-procedure",
+ "title": "Vehicle checkout procedure",
+ "version": "1.2",
+ "effective_date": "2026-06-01",
+ "department": "Rental Operations",
+ "status": "active"
+ },
+ {
+ "path": "knowledge/procedures/02-vehicle-return.md",
+ "sha256": "dc6bb97065a420ed0f0d828721135655ebb5510a394c3be84fe7fd0a846daf17",
+ "document_id": "vehicle-return-procedure",
+ "title": "Vehicle return procedure",
+ "version": "2.0",
+ "effective_date": "2026-07-01",
+ "department": "Rental Operations",
+ "status": "active"
+ },
+ {
+ "path": "knowledge/procedures/03-damage-handling.md",
+ "sha256": "ebba2c8146bff345139a4899d691763cdba40331ead10f06058b11d63ccbaf20",
+ "document_id": "damage-procedure",
+ "title": "Damage handling procedure",
+ "version": "1.3",
+ "effective_date": "2026-06-15",
+ "department": "Operations",
+ "status": "active"
+ },
+ {
+ "path": "knowledge/procedures/04-odometer-anomalies.md",
+ "sha256": "93554311119b9ce16f4a16d39ccb97807032c1f34c778117e5644a8c4be52279",
+ "document_id": "odometer-anomaly-guideline",
+ "title": "Odometer anomaly guideline",
+ "version": "1.1",
+ "effective_date": "2026-05-20",
+ "department": "Operations",
+ "status": "active"
+ },
+ {
+ "path": "knowledge/procedures/05-cleaning-checklist.md",
+ "sha256": "d65f0aafdb684db50a3d26aeb28fbffa8aec1851fc4f4cb78e9c9e01446884da",
+ "document_id": "cleaning-checklist",
+ "title": "Vehicle cleaning checklist",
+ "version": "1.4",
+ "effective_date": "2026-06-10",
+ "department": "Fleet Preparation",
+ "status": "active"
+ },
+ {
+ "path": "knowledge/procedures/06-maintenance-escalation.md",
+ "sha256": "f2bdaf59c2c5f2a3fa82b1c038cadf116ad89af2b1fd27b0d48f41b92eecf0fe",
+ "document_id": "maintenance-escalation",
+ "title": "Maintenance escalation procedure",
+ "version": "1.1",
+ "effective_date": "2026-04-01",
+ "department": "Workshop",
+ "status": "active"
+ },
+ {
+ "path": "knowledge/procedures/07-customer-documents.md",
+ "sha256": "b979213e49885d4ffc3224e646cd2e413f0c3affc2bc8e6269ceb129f418efdb",
+ "document_id": "customer-document-requirements",
+ "title": "Required customer documents",
+ "version": "1.0",
+ "effective_date": "2026-07-01",
+ "department": "Rental Operations",
+ "status": "active"
+ },
+ {
+ "path": "knowledge/procedures/08-privacy.md",
+ "sha256": "39981d9871f0bf6c4d495809ca0026ad16d646e5b68bcbf7e6c08315a04fd1e6",
+ "document_id": "privacy-guideline",
+ "title": "Operational privacy guideline",
+ "version": "1.0",
+ "effective_date": "2026-03-01",
+ "department": "Management",
+ "status": "active"
+ },
+ {
+ "path": "knowledge/procedures/09-booking-conflicts.md",
+ "sha256": "d649ed697d9825cddb6ba33aed7d3bd125737d6543e5456d5853e277f73b0090",
+ "document_id": "booking-conflict-procedure",
+ "title": "Booking conflict procedure",
+ "version": "1.2",
+ "effective_date": "2026-06-20",
+ "department": "Operations",
+ "status": "active"
+ },
+ {
+ "path": "knowledge/procedures/10-roles-and-escalation.md",
+ "sha256": "9f62e555e89737d0096b972ca1ee15fc32e53368f50328a3dee5f64671015c4f",
+ "document_id": "roles-and-escalation",
+ "title": "Roles and escalation matrix",
+ "version": "1.0",
+ "effective_date": "2026-06-01",
+ "department": "Management",
+ "status": "active"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/knowledge/procedures/01-vehicle-checkout.md b/knowledge/procedures/01-vehicle-checkout.md
new file mode 100644
index 0000000..f338027
--- /dev/null
+++ b/knowledge/procedures/01-vehicle-checkout.md
@@ -0,0 +1,21 @@
+---
+document_id: vehicle-checkout-procedure
+title: Vehicle checkout procedure
+version: "1.2"
+effective_date: 2026-06-01
+department: Rental Operations
+status: active
+---
+# Vehicle checkout procedure
+
+## 1. Required checks
+
+Before a vehicle leaves the site, the rental employee must confirm the active booking, customer identity, driving licence and required signed documents. The recorded starting odometer must not be lower than the vehicle's current canonical odometer.
+
+## 2. Vehicle condition
+
+Complete the checkout inspection, record fuel level and visible pre-existing damage, and confirm that no blocking maintenance or data-quality issue is open.
+
+## 3. Release
+
+Only a vehicle with an approved checkout inspection may be set to rented. When a critical discrepancy is found, keep the vehicle blocked and escalate to Operations.
diff --git a/knowledge/procedures/02-vehicle-return.md b/knowledge/procedures/02-vehicle-return.md
new file mode 100644
index 0000000..199784a
--- /dev/null
+++ b/knowledge/procedures/02-vehicle-return.md
@@ -0,0 +1,25 @@
+---
+document_id: vehicle-return-procedure
+title: Vehicle return procedure
+version: "2.0"
+effective_date: 2026-07-01
+department: Rental Operations
+status: active
+---
+# Vehicle return procedure
+
+## 1. Register the return
+
+Open the active booking and record the ending odometer, fuel level, cleanliness, visible damage, technical warnings and relevant notes.
+
+## 2. Validate the reading
+
+A submitted odometer below the previous canonical reading must never replace the canonical value. Register the inspection, create a data-quality issue and request human review.
+
+## 3. Determine next state
+
+Damage or a technical warning requires a blocked state. A reached service threshold requires maintenance. Otherwise the vehicle moves to cleaning before it can become available.
+
+## 4. Follow-up
+
+The return automation may create a cleaning or maintenance follow-up, but MobilityOps remains the source of truth for the vehicle state.
diff --git a/knowledge/procedures/03-damage-handling.md b/knowledge/procedures/03-damage-handling.md
new file mode 100644
index 0000000..3713b42
--- /dev/null
+++ b/knowledge/procedures/03-damage-handling.md
@@ -0,0 +1,21 @@
+---
+document_id: damage-procedure
+title: Damage handling procedure
+version: "1.3"
+effective_date: 2026-06-15
+department: Operations
+status: active
+---
+# Damage handling procedure
+
+## 1. Immediate actions
+
+When a vehicle returns with visible or reported damage, mark damage in the return inspection, add a concise factual description and keep the vehicle blocked. Do not promise the customer a repair cost or liability decision.
+
+## 2. Evidence
+
+Record at least four photographs in the source inspection system: full affected side, close view, wider context and vehicle registration. MobilityOps may store only the evidence reference in this PoC.
+
+## 3. Escalation
+
+Create an Operations review before any new rental. A vehicle with unresolved damage may not be marked available even when a future booking exists.
diff --git a/knowledge/procedures/04-odometer-anomalies.md b/knowledge/procedures/04-odometer-anomalies.md
new file mode 100644
index 0000000..65da8f0
--- /dev/null
+++ b/knowledge/procedures/04-odometer-anomalies.md
@@ -0,0 +1,21 @@
+---
+document_id: odometer-anomaly-guideline
+title: Odometer anomaly guideline
+version: "1.1"
+effective_date: 2026-05-20
+department: Operations
+status: active
+---
+# Odometer anomaly guideline
+
+## 1. Lower reading
+
+If a new reading is lower than the canonical odometer, preserve the submitted reading as evidence but do not lower the canonical value. Check for digit transposition, incorrect vehicle selection or a source-system import error.
+
+## 2. Resolution
+
+An Operations Manager may resolve the issue after checking the original inspection. Correct only the erroneous inspection or source mapping; do not rewrite valid historical readings.
+
+## 3. Escalation threshold
+
+Any unexplained reduction or a single increase above 2,500 kilometres during one rental requires review.
diff --git a/knowledge/procedures/05-cleaning-checklist.md b/knowledge/procedures/05-cleaning-checklist.md
new file mode 100644
index 0000000..733fac7
--- /dev/null
+++ b/knowledge/procedures/05-cleaning-checklist.md
@@ -0,0 +1,21 @@
+---
+document_id: cleaning-checklist
+title: Vehicle cleaning checklist
+version: "1.4"
+effective_date: 2026-06-10
+department: Fleet Preparation
+status: active
+---
+# Vehicle cleaning checklist
+
+## 1. Interior
+
+Remove waste, inspect upholstery, clean kitchen and sanitary surfaces, and confirm that the inventory checklist is complete.
+
+## 2. Exterior
+
+Inspect windows, lights, tyres and bodywork while cleaning. Report newly observed damage rather than silently completing the task.
+
+## 3. Completion
+
+Cleaning completion alone does not make a blocked or maintenance vehicle available. MobilityOps derives availability from all active restrictions.
diff --git a/knowledge/procedures/06-maintenance-escalation.md b/knowledge/procedures/06-maintenance-escalation.md
new file mode 100644
index 0000000..85e7421
--- /dev/null
+++ b/knowledge/procedures/06-maintenance-escalation.md
@@ -0,0 +1,21 @@
+---
+document_id: maintenance-escalation
+title: Maintenance escalation procedure
+version: "1.1"
+effective_date: 2026-04-01
+department: Workshop
+status: active
+---
+# Maintenance escalation procedure
+
+## 1. Triggers
+
+Escalate when a technical warning is reported, the service threshold is reached, a safety-related defect is observed or an existing maintenance block remains unresolved.
+
+## 2. State
+
+Set or retain the maintenance state. A future booking does not override a maintenance restriction.
+
+## 3. Release
+
+Only an authorised maintenance completion record and the absence of other blocking issues permit the vehicle to proceed to cleaning or availability.
diff --git a/knowledge/procedures/07-customer-documents.md b/knowledge/procedures/07-customer-documents.md
new file mode 100644
index 0000000..8653f78
--- /dev/null
+++ b/knowledge/procedures/07-customer-documents.md
@@ -0,0 +1,21 @@
+---
+document_id: customer-document-requirements
+title: Required customer documents
+version: "1.0"
+effective_date: 2026-07-01
+department: Rental Operations
+status: active
+---
+# Required customer documents
+
+## 1. Checkout requirements
+
+The customer identity and valid driving licence must be verified in the source process before checkout. Required contractual documents must be signed.
+
+## 2. Data minimisation
+
+MobilityOps records only completion status and source references for this PoC. It does not store identity-document images.
+
+## 3. Missing information
+
+A booking with incomplete required documents is not ready for checkout and must appear as an attention item.
diff --git a/knowledge/procedures/08-privacy.md b/knowledge/procedures/08-privacy.md
new file mode 100644
index 0000000..b659c1c
--- /dev/null
+++ b/knowledge/procedures/08-privacy.md
@@ -0,0 +1,21 @@
+---
+document_id: privacy-guideline
+title: Operational privacy guideline
+version: "1.0"
+effective_date: 2026-03-01
+department: Management
+status: active
+---
+# Operational privacy guideline
+
+## 1. Minimum access
+
+Employees may access only the customer and booking information required for their role. External AI tools receive no unrestricted customer dataset.
+
+## 2. Knowledge platform
+
+Do not ingest customer personal data, booking notes or identity documents into the procedure knowledge collection. The collection is limited to approved internal guidance.
+
+## 3. Logging
+
+Audit action metadata and source references, but avoid copying unnecessary personal data into technical logs.
diff --git a/knowledge/procedures/09-booking-conflicts.md b/knowledge/procedures/09-booking-conflicts.md
new file mode 100644
index 0000000..7b5b020
--- /dev/null
+++ b/knowledge/procedures/09-booking-conflicts.md
@@ -0,0 +1,21 @@
+---
+document_id: booking-conflict-procedure
+title: Booking conflict procedure
+version: "1.2"
+effective_date: 2026-06-20
+department: Operations
+status: active
+---
+# Booking conflict procedure
+
+## 1. Detection
+
+Two reserved or active bookings for the same vehicle may not overlap. Imported legacy conflicts must be flagged as data-quality issues.
+
+## 2. Resolution
+
+Operations reviews both bookings, confirms source-system ownership and reassigns or cancels one booking in the authoritative booking system. MobilityOps records the reviewed outcome.
+
+## 3. Safety
+
+Do not automatically cancel a customer booking based only on an inferred conflict.
diff --git a/knowledge/procedures/10-roles-and-escalation.md b/knowledge/procedures/10-roles-and-escalation.md
new file mode 100644
index 0000000..82cbdd1
--- /dev/null
+++ b/knowledge/procedures/10-roles-and-escalation.md
@@ -0,0 +1,21 @@
+---
+document_id: roles-and-escalation
+title: Roles and escalation matrix
+version: "1.0"
+effective_date: 2026-06-01
+department: Management
+status: active
+---
+# Roles and escalation matrix
+
+## Rental Employee
+
+May view assigned operational records, complete checkout and return inspections, and consult procedures. May not merge customer records or reset the demo environment.
+
+## Operations Manager
+
+May review all attention and data-quality issues, merge confirmed duplicate customers, retry workflow delivery and reset the synthetic demo.
+
+## Service integrations
+
+n8n, RAGcore and ITWorx MCP Hub use separate scoped identities. Service identities do not inherit an interactive user role.
diff --git a/n8n/README.md b/n8n/README.md
new file mode 100644
index 0000000..f5638b0
--- /dev/null
+++ b/n8n/README.md
@@ -0,0 +1,13 @@
+# n8n workflows
+
+`mobilityops-return-processing.json` is a starter import for the required live workflow.
+
+Before final validation Claude must:
+
+1. import or provision the workflow;
+2. configure a service credential for the MobilityOps callback;
+3. activate the webhook;
+4. ensure event ID is used as the idempotency key;
+5. validate online success and offline retry behaviour.
+
+The callback URL in the starter file is intentionally environment-driven and may require an n8n expression or credential adjustment.
diff --git a/n8n/mobilityops-return-processing.json b/n8n/mobilityops-return-processing.json
new file mode 100644
index 0000000..22340f1
--- /dev/null
+++ b/n8n/mobilityops-return-processing.json
@@ -0,0 +1,122 @@
+{
+ "name": "MobilityOps - Vehicle Return Processing",
+ "nodes": [
+ {
+ "parameters": {
+ "httpMethod": "POST",
+ "path": "mobilityops-return",
+ "responseMode": "responseNode",
+ "options": {}
+ },
+ "id": "webhook-node",
+ "name": "Return webhook",
+ "type": "n8n-nodes-base.webhook",
+ "typeVersion": 2,
+ "position": [
+ 240,
+ 300
+ ],
+ "webhookId": "mobilityops-return"
+ },
+ {
+ "parameters": {
+ "jsCode": "const e = $json.body ?? $json;\nif (e.event_type !== 'vehicle.returned.v1') throw new Error('Unsupported event type');\nconst reasons = e.data?.attention_reasons ?? [];\nreturn [{json: {event_id: e.event_id, event_type: e.event_type, aggregate_ref: e.aggregate.public_ref, follow_up: reasons.length ? 'attention_required' : 'cleaning', summary: reasons.join('; ') || 'Create cleaning follow-up'}}];"
+ },
+ "id": "validate-node",
+ "name": "Validate and derive follow-up",
+ "type": "n8n-nodes-base.code",
+ "typeVersion": 2,
+ "position": [
+ 500,
+ 300
+ ]
+ },
+ {
+ "parameters": {
+ "method": "POST",
+ "url": "={{$env.MOBILITYOPS_CALLBACK_URL || 'http://api:8000/api/v1/integrations/n8n/return-callback'}}",
+ "sendHeaders": true,
+ "headerParameters": {
+ "parameters": [
+ {
+ "name": "Idempotency-Key",
+ "value": "={{$json.event_id}}"
+ }
+ ]
+ },
+ "sendBody": true,
+ "contentType": "raw",
+ "rawContentType": "application/json",
+ "body": "={{JSON.stringify($json)}}",
+ "options": {}
+ },
+ "id": "callback-node",
+ "name": "Record follow-up",
+ "type": "n8n-nodes-base.httpRequest",
+ "typeVersion": 4.2,
+ "position": [
+ 760,
+ 300
+ ]
+ },
+ {
+ "parameters": {
+ "respondWith": "json",
+ "responseBody": "={{ { ok: true, event_id: $('Validate and derive follow-up').item.json.event_id, result: $json } }}",
+ "options": {}
+ },
+ "id": "response-node",
+ "name": "Return result",
+ "type": "n8n-nodes-base.respondToWebhook",
+ "typeVersion": 1.4,
+ "position": [
+ 1020,
+ 300
+ ]
+ }
+ ],
+ "connections": {
+ "Return webhook": {
+ "main": [
+ [
+ {
+ "node": "Validate and derive follow-up",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Validate and derive follow-up": {
+ "main": [
+ [
+ {
+ "node": "Record follow-up",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ },
+ "Record follow-up": {
+ "main": [
+ [
+ {
+ "node": "Return result",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "settings": {
+ "executionOrder": "v1"
+ },
+ "active": false,
+ "versionId": "11111111-1111-4111-8111-111111111111",
+ "meta": {
+ "templateCredsSetupCompleted": false
+ },
+ "tags": []
+}
\ No newline at end of file
diff --git a/seed/README.md b/seed/README.md
new file mode 100644
index 0000000..c40cdac
--- /dev/null
+++ b/seed/README.md
@@ -0,0 +1,11 @@
+# Synthetic seed data
+
+The committed CSV snapshot is fully synthetic and generated by `generate_seed.py`.
+
+Regenerate deterministically:
+
+```bash
+python seed/generate_seed.py --anchor 2026-08-01 --seed 20260801
+```
+
+The application importer must preserve the named demo references in `docs/13-seed-and-demo-scenarios.md`. Normal write APIs must enforce invariants even though the controlled seed/import path contains deliberate legacy data-quality conflicts.
diff --git a/seed/bookings.csv b/seed/bookings.csv
new file mode 100644
index 0000000..d51edf2
--- /dev/null
+++ b/seed/bookings.csv
@@ -0,0 +1,247 @@
+public_ref,customer_ref,vehicle_ref,starts_at,ends_at,status,start_odometer_km,end_odometer_km,requirements_complete
+BK-H-0001,CUS-0012,MO-008,2026-07-26T08:00:00Z,2026-07-29T08:00:00Z,returned,20932,21133,true
+BK-H-0002,CUS-0023,MO-015,2026-07-24T07:00:00Z,2026-07-28T07:00:00Z,returned,26659,26861,true
+BK-H-0003,CUS-0034,MO-022,2026-07-22T06:00:00Z,2026-07-27T06:00:00Z,returned,31407,31610,true
+BK-H-0004,CUS-0045,MO-029,2026-07-20T05:00:00Z,2026-07-26T05:00:00Z,returned,36708,36912,true
+BK-H-0005,CUS-0056,MO-036,2026-07-18T04:00:00Z,2026-07-25T04:00:00Z,returned,41655,41860,true
+BK-H-0006,CUS-0067,MO-043,2026-07-16T03:00:00Z,2026-07-24T03:00:00Z,returned,46513,46719,true
+BK-H-0007,CUS-0078,MO-050,2026-07-14T02:00:00Z,2026-07-23T02:00:00Z,returned,51685,51892,true
+BK-H-0008,CUS-0089,MO-007,2026-07-12T09:00:00Z,2026-07-22T09:00:00Z,returned,20681,20889,true
+BK-H-0009,CUS-0100,MO-014,2026-07-10T08:00:00Z,2026-07-21T08:00:00Z,returned,25419,25628,true
+BK-H-0010,CUS-0111,MO-021,2026-07-18T07:00:00Z,2026-07-20T07:00:00Z,returned,31088,31298,true
+BK-H-0011,CUS-0122,MO-028,2026-07-16T06:00:00Z,2026-07-19T06:00:00Z,returned,35948,36159,true
+BK-H-0012,CUS-0133,MO-035,2026-07-14T05:00:00Z,2026-07-18T05:00:00Z,returned,41022,41234,true
+BK-H-0013,CUS-0144,MO-042,2026-07-12T04:00:00Z,2026-07-17T04:00:00Z,returned,46535,46748,true
+BK-H-0014,CUS-0155,MO-049,2026-07-10T03:00:00Z,2026-07-16T03:00:00Z,returned,50921,51135,true
+BK-H-0015,CUS-0166,MO-006,2026-07-08T02:00:00Z,2026-07-15T02:00:00Z,returned,20148,20363,true
+BK-H-0016,CUS-0177,MO-013,2026-07-06T09:00:00Z,2026-07-14T09:00:00Z,returned,24734,24950,true
+BK-H-0017,CUS-0008,MO-020,2026-07-04T08:00:00Z,2026-07-13T08:00:00Z,returned,29599,29816,true
+BK-H-0018,CUS-0019,MO-027,2026-07-02T07:00:00Z,2026-07-12T07:00:00Z,returned,35484,35702,true
+BK-H-0019,CUS-0030,MO-034,2026-06-30T06:00:00Z,2026-07-11T06:00:00Z,returned,40038,40257,true
+BK-H-0020,CUS-0041,MO-041,2026-07-08T05:00:00Z,2026-07-10T05:00:00Z,returned,45423,45643,true
+BK-H-0021,CUS-0052,MO-048,2026-07-06T04:00:00Z,2026-07-09T04:00:00Z,returned,50672,50893,true
+BK-H-0022,CUS-0063,MO-005,2026-07-04T03:00:00Z,2026-07-08T03:00:00Z,returned,19191,19413,true
+BK-H-0023,CUS-0074,MO-012,2026-07-02T02:00:00Z,2026-07-07T02:00:00Z,returned,23836,24059,true
+BK-H-0024,CUS-0085,MO-019,2026-06-30T09:00:00Z,2026-07-06T09:00:00Z,returned,29222,29446,true
+BK-H-0025,CUS-0096,MO-026,2026-06-28T08:00:00Z,2026-07-05T08:00:00Z,returned,34248,34473,true
+BK-H-0026,CUS-0107,MO-033,2026-06-26T07:00:00Z,2026-07-04T07:00:00Z,returned,39535,39761,true
+BK-H-0027,CUS-0118,MO-040,2026-06-24T06:00:00Z,2026-07-03T06:00:00Z,returned,44882,45109,true
+BK-H-0028,CUS-0129,MO-047,2026-06-22T05:00:00Z,2026-07-02T05:00:00Z,returned,49424,49652,true
+BK-H-0029,CUS-0140,MO-004,2026-06-20T04:00:00Z,2026-07-01T04:00:00Z,returned,18126,18355,true
+BK-H-0030,CUS-0151,MO-011,2026-06-28T03:00:00Z,2026-06-30T03:00:00Z,returned,23130,23360,true
+BK-H-0031,CUS-0162,MO-018,2026-06-26T02:00:00Z,2026-06-29T02:00:00Z,returned,28551,28782,true
+BK-H-0032,CUS-0173,MO-025,2026-06-24T09:00:00Z,2026-06-28T09:00:00Z,returned,33397,33629,true
+BK-H-0033,CUS-0004,MO-032,2026-06-22T08:00:00Z,2026-06-27T08:00:00Z,returned,38758,38991,true
+BK-H-0034,CUS-0015,MO-039,2026-06-20T07:00:00Z,2026-06-26T07:00:00Z,returned,43623,43857,true
+BK-H-0035,CUS-0026,MO-046,2026-06-18T06:00:00Z,2026-06-25T06:00:00Z,returned,48922,49157,true
+BK-H-0036,CUS-0037,MO-003,2026-06-16T05:00:00Z,2026-06-24T05:00:00Z,returned,18022,18258,true
+BK-H-0037,CUS-0048,MO-010,2026-06-14T04:00:00Z,2026-06-23T04:00:00Z,returned,22296,22533,true
+BK-H-0038,CUS-0059,MO-017,2026-06-12T03:00:00Z,2026-06-22T03:00:00Z,returned,27558,27796,true
+BK-H-0039,CUS-0070,MO-024,2026-06-10T02:00:00Z,2026-06-21T02:00:00Z,returned,51781,52020,true
+BK-H-0040,CUS-0081,MO-031,2026-06-18T09:00:00Z,2026-06-20T09:00:00Z,returned,38109,38349,true
+BK-H-0041,CUS-0092,MO-038,2026-06-16T08:00:00Z,2026-06-19T08:00:00Z,returned,43589,43830,true
+BK-H-0042,CUS-0103,MO-045,2026-06-14T07:00:00Z,2026-06-18T07:00:00Z,returned,48218,48460,true
+BK-H-0043,CUS-0114,MO-002,2026-06-12T06:00:00Z,2026-06-17T06:00:00Z,returned,16920,17163,true
+BK-H-0044,CUS-0125,MO-009,2026-06-10T05:00:00Z,2026-06-16T05:00:00Z,returned,22159,22403,true
+BK-H-0045,CUS-0136,MO-016,2026-06-08T04:00:00Z,2026-06-15T04:00:00Z,returned,27452,27697,true
+BK-H-0046,CUS-0147,MO-023,2026-06-06T03:00:00Z,2026-06-14T03:00:00Z,returned,32578,32824,true
+BK-H-0047,CUS-0158,MO-030,2026-06-04T02:00:00Z,2026-06-13T02:00:00Z,returned,37166,37413,true
+BK-H-0048,CUS-0169,MO-037,2026-06-02T09:00:00Z,2026-06-12T09:00:00Z,returned,42606,42854,true
+BK-H-0049,CUS-0180,MO-044,2026-05-31T08:00:00Z,2026-06-11T08:00:00Z,returned,47562,47811,true
+BK-H-0050,CUS-0011,MO-001,2026-06-08T07:00:00Z,2026-06-10T07:00:00Z,returned,16034,16284,true
+BK-H-0051,CUS-0022,MO-008,2026-06-06T06:00:00Z,2026-06-09T06:00:00Z,returned,20882,21133,true
+BK-H-0052,CUS-0033,MO-015,2026-06-04T05:00:00Z,2026-06-08T05:00:00Z,returned,26609,26861,true
+BK-H-0053,CUS-0044,MO-022,2026-06-02T04:00:00Z,2026-06-07T04:00:00Z,returned,31357,31610,true
+BK-H-0054,CUS-0055,MO-029,2026-05-31T03:00:00Z,2026-06-06T03:00:00Z,returned,36658,36912,true
+BK-H-0055,CUS-0066,MO-036,2026-05-29T02:00:00Z,2026-06-05T02:00:00Z,returned,41605,41860,true
+BK-H-0056,CUS-0077,MO-043,2026-05-27T09:00:00Z,2026-06-04T09:00:00Z,returned,46463,46719,true
+BK-H-0057,CUS-0088,MO-050,2026-05-25T08:00:00Z,2026-06-03T08:00:00Z,returned,51635,51892,true
+BK-H-0058,CUS-0099,MO-007,2026-05-23T07:00:00Z,2026-06-02T07:00:00Z,returned,20631,20889,true
+BK-H-0059,CUS-0110,MO-014,2026-05-21T06:00:00Z,2026-06-01T06:00:00Z,returned,25369,25628,true
+BK-H-0060,CUS-0121,MO-021,2026-05-29T05:00:00Z,2026-05-31T05:00:00Z,returned,31038,31298,true
+BK-H-0061,CUS-0132,MO-028,2026-05-27T04:00:00Z,2026-05-30T04:00:00Z,returned,35898,36159,true
+BK-H-0062,CUS-0143,MO-035,2026-05-25T03:00:00Z,2026-05-29T03:00:00Z,returned,40972,41234,true
+BK-H-0063,CUS-0154,MO-042,2026-05-23T02:00:00Z,2026-05-28T02:00:00Z,returned,46485,46748,true
+BK-H-0064,CUS-0165,MO-049,2026-05-21T09:00:00Z,2026-05-27T09:00:00Z,returned,50871,51135,true
+BK-H-0065,CUS-0176,MO-006,2026-05-19T08:00:00Z,2026-05-26T08:00:00Z,returned,20098,20363,true
+BK-H-0066,CUS-0007,MO-013,2026-05-17T07:00:00Z,2026-05-25T07:00:00Z,returned,24684,24950,true
+BK-H-0067,CUS-0018,MO-020,2026-05-15T06:00:00Z,2026-05-24T06:00:00Z,returned,29549,29816,true
+BK-H-0068,CUS-0029,MO-027,2026-05-13T05:00:00Z,2026-05-23T05:00:00Z,returned,35434,35702,true
+BK-H-0069,CUS-0040,MO-034,2026-05-11T04:00:00Z,2026-05-22T04:00:00Z,returned,39988,40257,true
+BK-H-0070,CUS-0051,MO-041,2026-05-19T03:00:00Z,2026-05-21T03:00:00Z,returned,45373,45643,true
+BK-H-0071,CUS-0062,MO-048,2026-05-17T02:00:00Z,2026-05-20T02:00:00Z,returned,50622,50893,true
+BK-H-0072,CUS-0073,MO-005,2026-05-15T09:00:00Z,2026-05-19T09:00:00Z,returned,19141,19413,true
+BK-H-0073,CUS-0084,MO-012,2026-05-13T08:00:00Z,2026-05-18T08:00:00Z,returned,23786,24059,true
+BK-H-0074,CUS-0095,MO-019,2026-05-11T07:00:00Z,2026-05-17T07:00:00Z,returned,29172,29446,true
+BK-H-0075,CUS-0106,MO-026,2026-05-09T06:00:00Z,2026-05-16T06:00:00Z,returned,34198,34473,true
+BK-H-0076,CUS-0117,MO-033,2026-05-07T05:00:00Z,2026-05-15T05:00:00Z,returned,39485,39761,true
+BK-H-0077,CUS-0128,MO-040,2026-05-05T04:00:00Z,2026-05-14T04:00:00Z,returned,44832,45109,true
+BK-H-0078,CUS-0139,MO-047,2026-05-03T03:00:00Z,2026-05-13T03:00:00Z,returned,49374,49652,true
+BK-H-0079,CUS-0150,MO-004,2026-05-01T02:00:00Z,2026-05-12T02:00:00Z,returned,18076,18355,true
+BK-H-0080,CUS-0161,MO-011,2026-05-09T09:00:00Z,2026-05-11T09:00:00Z,returned,23080,23360,true
+BK-H-0081,CUS-0172,MO-018,2026-05-07T08:00:00Z,2026-05-10T08:00:00Z,returned,28501,28782,true
+BK-H-0082,CUS-0003,MO-025,2026-05-05T07:00:00Z,2026-05-09T07:00:00Z,returned,33347,33629,true
+BK-H-0083,CUS-0014,MO-032,2026-05-03T06:00:00Z,2026-05-08T06:00:00Z,returned,38708,38991,true
+BK-H-0084,CUS-0025,MO-039,2026-05-01T05:00:00Z,2026-05-07T05:00:00Z,returned,43573,43857,true
+BK-H-0085,CUS-0036,MO-046,2026-04-29T04:00:00Z,2026-05-06T04:00:00Z,returned,48872,49157,true
+BK-H-0086,CUS-0047,MO-003,2026-04-27T03:00:00Z,2026-05-05T03:00:00Z,returned,17972,18258,true
+BK-H-0087,CUS-0058,MO-010,2026-04-25T02:00:00Z,2026-05-04T02:00:00Z,returned,22246,22533,true
+BK-H-0088,CUS-0069,MO-017,2026-04-23T09:00:00Z,2026-05-03T09:00:00Z,returned,27508,27796,true
+BK-H-0089,CUS-0080,MO-024,2026-04-21T08:00:00Z,2026-05-02T08:00:00Z,returned,51731,52020,true
+BK-H-0090,CUS-0091,MO-031,2026-04-29T07:00:00Z,2026-05-01T07:00:00Z,returned,38059,38349,true
+BK-H-0091,CUS-0102,MO-038,2026-04-27T06:00:00Z,2026-04-30T06:00:00Z,returned,43539,43830,true
+BK-H-0092,CUS-0113,MO-045,2026-04-25T05:00:00Z,2026-04-29T05:00:00Z,returned,48168,48460,true
+BK-H-0093,CUS-0124,MO-002,2026-04-23T04:00:00Z,2026-04-28T04:00:00Z,returned,16870,17163,true
+BK-H-0094,CUS-0135,MO-009,2026-04-21T03:00:00Z,2026-04-27T03:00:00Z,returned,22109,22403,true
+BK-H-0095,CUS-0146,MO-016,2026-04-19T02:00:00Z,2026-04-26T02:00:00Z,returned,27402,27697,true
+BK-H-0096,CUS-0157,MO-023,2026-04-17T09:00:00Z,2026-04-25T09:00:00Z,returned,32528,32824,true
+BK-H-0097,CUS-0168,MO-030,2026-04-15T08:00:00Z,2026-04-24T08:00:00Z,returned,37116,37413,true
+BK-H-0098,CUS-0179,MO-037,2026-04-13T07:00:00Z,2026-04-23T07:00:00Z,returned,42556,42854,true
+BK-H-0099,CUS-0010,MO-044,2026-04-11T06:00:00Z,2026-04-22T06:00:00Z,returned,47512,47811,true
+BK-H-0100,CUS-0021,MO-001,2026-04-19T05:00:00Z,2026-04-21T05:00:00Z,returned,15984,16284,true
+BK-H-0101,CUS-0032,MO-008,2026-04-17T04:00:00Z,2026-04-20T04:00:00Z,returned,20832,21133,true
+BK-H-0102,CUS-0043,MO-015,2026-04-15T03:00:00Z,2026-04-19T03:00:00Z,returned,26559,26861,true
+BK-H-0103,CUS-0054,MO-022,2026-04-13T02:00:00Z,2026-04-18T02:00:00Z,returned,31307,31610,true
+BK-H-0104,CUS-0065,MO-029,2026-04-11T09:00:00Z,2026-04-17T09:00:00Z,returned,36608,36912,true
+BK-H-0105,CUS-0076,MO-036,2026-04-09T08:00:00Z,2026-04-16T08:00:00Z,returned,41555,41860,true
+BK-H-0106,CUS-0087,MO-043,2026-04-07T07:00:00Z,2026-04-15T07:00:00Z,returned,46413,46719,true
+BK-H-0107,CUS-0098,MO-050,2026-04-05T06:00:00Z,2026-04-14T06:00:00Z,returned,51585,51892,true
+BK-H-0108,CUS-0109,MO-007,2026-04-03T05:00:00Z,2026-04-13T05:00:00Z,returned,20581,20889,true
+BK-H-0109,CUS-0120,MO-014,2026-04-01T04:00:00Z,2026-04-12T04:00:00Z,returned,25319,25628,true
+BK-H-0110,CUS-0131,MO-021,2026-04-09T03:00:00Z,2026-04-11T03:00:00Z,returned,30988,31298,true
+BK-H-0111,CUS-0142,MO-028,2026-04-07T02:00:00Z,2026-04-10T02:00:00Z,returned,35848,36159,true
+BK-H-0112,CUS-0153,MO-035,2026-04-05T09:00:00Z,2026-04-09T09:00:00Z,returned,40922,41234,true
+BK-H-0113,CUS-0164,MO-042,2026-04-03T08:00:00Z,2026-04-08T08:00:00Z,returned,46435,46748,true
+BK-H-0114,CUS-0175,MO-049,2026-04-01T07:00:00Z,2026-04-07T07:00:00Z,returned,50821,51135,true
+BK-H-0115,CUS-0006,MO-006,2026-03-30T06:00:00Z,2026-04-06T06:00:00Z,returned,20048,20363,true
+BK-H-0116,CUS-0017,MO-013,2026-03-28T05:00:00Z,2026-04-05T05:00:00Z,returned,24634,24950,true
+BK-H-0117,CUS-0028,MO-020,2026-03-26T04:00:00Z,2026-04-04T04:00:00Z,returned,29499,29816,true
+BK-H-0118,CUS-0039,MO-027,2026-03-24T03:00:00Z,2026-04-03T03:00:00Z,returned,35384,35702,true
+BK-H-0119,CUS-0050,MO-034,2026-03-22T02:00:00Z,2026-04-02T02:00:00Z,returned,39938,40257,true
+BK-H-0120,CUS-0061,MO-041,2026-03-30T09:00:00Z,2026-04-01T09:00:00Z,returned,45323,45643,true
+BK-H-0121,CUS-0072,MO-048,2026-03-28T08:00:00Z,2026-03-31T08:00:00Z,returned,50572,50893,true
+BK-H-0122,CUS-0083,MO-005,2026-03-26T07:00:00Z,2026-03-30T07:00:00Z,returned,19091,19413,true
+BK-H-0123,CUS-0094,MO-012,2026-03-24T06:00:00Z,2026-03-29T06:00:00Z,returned,23736,24059,true
+BK-H-0124,CUS-0105,MO-019,2026-03-22T05:00:00Z,2026-03-28T05:00:00Z,returned,29122,29446,true
+BK-H-0125,CUS-0116,MO-026,2026-03-20T04:00:00Z,2026-03-27T04:00:00Z,returned,34148,34473,true
+BK-H-0126,CUS-0127,MO-033,2026-03-18T03:00:00Z,2026-03-26T03:00:00Z,returned,39435,39761,true
+BK-H-0127,CUS-0138,MO-040,2026-03-16T02:00:00Z,2026-03-25T02:00:00Z,returned,44782,45109,true
+BK-H-0128,CUS-0149,MO-047,2026-03-14T09:00:00Z,2026-03-24T09:00:00Z,returned,49324,49652,true
+BK-H-0129,CUS-0160,MO-004,2026-03-12T08:00:00Z,2026-03-23T08:00:00Z,returned,18026,18355,true
+BK-H-0130,CUS-0171,MO-011,2026-03-20T07:00:00Z,2026-03-22T07:00:00Z,returned,23030,23360,true
+BK-H-0131,CUS-0002,MO-018,2026-03-18T06:00:00Z,2026-03-21T06:00:00Z,returned,28451,28782,true
+BK-H-0132,CUS-0013,MO-025,2026-03-16T05:00:00Z,2026-03-20T05:00:00Z,returned,33297,33629,true
+BK-H-0133,CUS-0024,MO-032,2026-03-14T04:00:00Z,2026-03-19T04:00:00Z,returned,38658,38991,true
+BK-H-0134,CUS-0035,MO-039,2026-03-12T03:00:00Z,2026-03-18T03:00:00Z,returned,43523,43857,true
+BK-H-0135,CUS-0046,MO-046,2026-03-10T02:00:00Z,2026-03-17T02:00:00Z,returned,48822,49157,true
+BK-H-0136,CUS-0057,MO-003,2026-03-08T09:00:00Z,2026-03-16T09:00:00Z,returned,17922,18258,true
+BK-H-0137,CUS-0068,MO-010,2026-03-06T08:00:00Z,2026-03-15T08:00:00Z,returned,22196,22533,true
+BK-H-0138,CUS-0079,MO-017,2026-03-04T07:00:00Z,2026-03-14T07:00:00Z,returned,27458,27796,true
+BK-H-0139,CUS-0090,MO-024,2026-03-02T06:00:00Z,2026-03-13T06:00:00Z,returned,51681,52020,true
+BK-H-0140,CUS-0101,MO-031,2026-03-10T05:00:00Z,2026-03-12T05:00:00Z,returned,38009,38349,true
+BK-H-0141,CUS-0112,MO-038,2026-03-08T04:00:00Z,2026-03-11T04:00:00Z,returned,43489,43830,true
+BK-H-0142,CUS-0123,MO-045,2026-03-06T03:00:00Z,2026-03-10T03:00:00Z,returned,48118,48460,true
+BK-H-0143,CUS-0134,MO-002,2026-03-04T02:00:00Z,2026-03-09T02:00:00Z,returned,16820,17163,true
+BK-H-0144,CUS-0145,MO-009,2026-03-02T09:00:00Z,2026-03-08T09:00:00Z,returned,22059,22403,true
+BK-H-0145,CUS-0156,MO-016,2026-02-28T08:00:00Z,2026-03-07T08:00:00Z,returned,27352,27697,true
+BK-H-0146,CUS-0167,MO-023,2026-02-26T07:00:00Z,2026-03-06T07:00:00Z,returned,32478,32824,true
+BK-H-0147,CUS-0178,MO-030,2026-02-24T06:00:00Z,2026-03-05T06:00:00Z,returned,37066,37413,true
+BK-H-0148,CUS-0009,MO-037,2026-02-22T05:00:00Z,2026-03-04T05:00:00Z,returned,42506,42854,true
+BK-H-0149,CUS-0020,MO-044,2026-02-20T04:00:00Z,2026-03-03T04:00:00Z,returned,47462,47811,true
+BK-H-0150,CUS-0031,MO-001,2026-02-28T03:00:00Z,2026-03-02T03:00:00Z,returned,15934,16284,true
+BK-H-0151,CUS-0042,MO-008,2026-02-26T02:00:00Z,2026-03-01T02:00:00Z,returned,20782,21133,true
+BK-H-0152,CUS-0053,MO-015,2026-02-24T09:00:00Z,2026-02-28T09:00:00Z,returned,26509,26861,true
+BK-H-0153,CUS-0064,MO-022,2026-02-22T08:00:00Z,2026-02-27T08:00:00Z,returned,31257,31610,true
+BK-H-0154,CUS-0075,MO-029,2026-02-20T07:00:00Z,2026-02-26T07:00:00Z,returned,36558,36912,true
+BK-H-0155,CUS-0086,MO-036,2026-02-18T06:00:00Z,2026-02-25T06:00:00Z,returned,41505,41860,true
+BK-H-0156,CUS-0097,MO-043,2026-02-16T05:00:00Z,2026-02-24T05:00:00Z,returned,46363,46719,true
+BK-H-0157,CUS-0108,MO-050,2026-02-14T04:00:00Z,2026-02-23T04:00:00Z,returned,51535,51892,true
+BK-H-0158,CUS-0119,MO-007,2026-02-12T03:00:00Z,2026-02-22T03:00:00Z,returned,20531,20889,true
+BK-H-0159,CUS-0130,MO-014,2026-02-10T02:00:00Z,2026-02-21T02:00:00Z,returned,25269,25628,true
+BK-H-0160,CUS-0141,MO-021,2026-02-18T09:00:00Z,2026-02-20T09:00:00Z,returned,30938,31298,true
+BK-H-0161,CUS-0152,MO-028,2026-02-16T08:00:00Z,2026-02-19T08:00:00Z,returned,35798,36159,true
+BK-H-0162,CUS-0163,MO-035,2026-02-14T07:00:00Z,2026-02-18T07:00:00Z,returned,40872,41234,true
+BK-H-0163,CUS-0174,MO-042,2026-02-12T06:00:00Z,2026-02-17T06:00:00Z,returned,46385,46748,true
+BK-H-0164,CUS-0005,MO-049,2026-02-10T05:00:00Z,2026-02-16T05:00:00Z,returned,50771,51135,true
+BK-H-0165,CUS-0016,MO-006,2026-02-08T04:00:00Z,2026-02-15T04:00:00Z,returned,19998,20363,true
+BK-H-0166,CUS-0027,MO-013,2026-02-06T03:00:00Z,2026-02-14T03:00:00Z,returned,24584,24950,true
+BK-H-0167,CUS-0038,MO-020,2026-02-04T02:00:00Z,2026-02-13T02:00:00Z,returned,29449,29816,true
+BK-H-0168,CUS-0049,MO-027,2026-02-02T09:00:00Z,2026-02-12T09:00:00Z,returned,35334,35702,true
+BK-H-0169,CUS-0060,MO-034,2026-01-31T08:00:00Z,2026-02-11T08:00:00Z,returned,39888,40257,true
+BK-H-0170,CUS-0071,MO-041,2026-02-08T07:00:00Z,2026-02-10T07:00:00Z,returned,45273,45643,true
+BK-H-0171,CUS-0082,MO-048,2026-02-06T06:00:00Z,2026-02-09T06:00:00Z,returned,50522,50893,true
+BK-H-0172,CUS-0093,MO-005,2026-02-04T05:00:00Z,2026-02-08T05:00:00Z,returned,19041,19413,true
+BK-H-0173,CUS-0104,MO-012,2026-02-02T04:00:00Z,2026-02-07T04:00:00Z,returned,23686,24059,true
+BK-H-0174,CUS-0115,MO-019,2026-01-31T03:00:00Z,2026-02-06T03:00:00Z,returned,29072,29446,true
+BK-H-0175,CUS-0126,MO-026,2026-01-29T02:00:00Z,2026-02-05T02:00:00Z,returned,34098,34473,true
+BK-H-0176,CUS-0137,MO-033,2026-01-27T09:00:00Z,2026-02-04T09:00:00Z,returned,39385,39761,true
+BK-H-0177,CUS-0148,MO-040,2026-01-25T08:00:00Z,2026-02-03T08:00:00Z,returned,44732,45109,true
+BK-H-0178,CUS-0159,MO-047,2026-01-23T07:00:00Z,2026-02-02T07:00:00Z,returned,49274,49652,true
+BK-H-0179,CUS-0170,MO-004,2026-01-21T06:00:00Z,2026-02-01T06:00:00Z,returned,17976,18355,true
+BK-H-0180,CUS-0001,MO-011,2026-01-29T05:00:00Z,2026-01-31T05:00:00Z,returned,22980,23360,true
+BK-H-0181,CUS-0012,MO-018,2026-01-27T04:00:00Z,2026-01-30T04:00:00Z,returned,28401,28782,true
+BK-H-0182,CUS-0023,MO-025,2026-01-25T03:00:00Z,2026-01-29T03:00:00Z,returned,33247,33629,true
+BK-H-0183,CUS-0034,MO-032,2026-01-23T02:00:00Z,2026-01-28T02:00:00Z,returned,38608,38991,true
+BK-H-0184,CUS-0045,MO-039,2026-01-21T09:00:00Z,2026-01-27T09:00:00Z,returned,43473,43857,true
+BK-H-0185,CUS-0056,MO-046,2026-01-19T08:00:00Z,2026-01-26T08:00:00Z,returned,48772,49157,true
+BK-H-0186,CUS-0067,MO-003,2026-01-17T07:00:00Z,2026-01-25T07:00:00Z,returned,17872,18258,true
+BK-H-0187,CUS-0078,MO-010,2026-01-15T06:00:00Z,2026-01-24T06:00:00Z,returned,22146,22533,true
+BK-H-0188,CUS-0089,MO-017,2026-01-13T05:00:00Z,2026-01-23T05:00:00Z,returned,27408,27796,true
+BK-H-0189,CUS-0100,MO-024,2026-01-11T04:00:00Z,2026-01-22T04:00:00Z,returned,51631,52020,true
+BK-H-0190,CUS-0111,MO-031,2026-01-19T03:00:00Z,2026-01-21T03:00:00Z,returned,37959,38349,true
+BK-H-0191,CUS-0122,MO-038,2026-01-17T02:00:00Z,2026-01-20T02:00:00Z,returned,43439,43830,true
+BK-H-0192,CUS-0133,MO-045,2026-01-15T09:00:00Z,2026-01-19T09:00:00Z,returned,48068,48460,true
+BK-H-0193,CUS-0144,MO-002,2026-01-13T08:00:00Z,2026-01-18T08:00:00Z,returned,16770,17163,true
+BK-H-0194,CUS-0155,MO-009,2026-01-11T07:00:00Z,2026-01-17T07:00:00Z,returned,22009,22403,true
+BK-H-0195,CUS-0166,MO-016,2026-01-09T06:00:00Z,2026-01-16T06:00:00Z,returned,27302,27697,true
+BK-H-0196,CUS-0177,MO-023,2026-01-07T05:00:00Z,2026-01-15T05:00:00Z,returned,32428,32824,true
+BK-H-0197,CUS-0008,MO-030,2026-01-05T04:00:00Z,2026-01-14T04:00:00Z,returned,37016,37413,true
+BK-H-0198,CUS-0019,MO-037,2026-01-03T03:00:00Z,2026-01-13T03:00:00Z,returned,42456,42854,true
+BK-H-0199,CUS-0030,MO-044,2026-01-01T02:00:00Z,2026-01-12T02:00:00Z,returned,47412,47811,true
+BK-H-0200,CUS-0041,MO-001,2026-01-09T09:00:00Z,2026-01-11T09:00:00Z,returned,15884,16284,true
+BK-H-0201,CUS-0052,MO-008,2026-01-07T08:00:00Z,2026-01-10T08:00:00Z,returned,20732,21133,true
+BK-H-0202,CUS-0063,MO-015,2026-01-05T07:00:00Z,2026-01-09T07:00:00Z,returned,26459,26861,true
+BK-H-0203,CUS-0074,MO-022,2026-01-03T06:00:00Z,2026-01-08T06:00:00Z,returned,31207,31610,true
+BK-H-0204,CUS-0085,MO-029,2026-01-01T05:00:00Z,2026-01-07T05:00:00Z,returned,36508,36912,true
+BK-H-0205,CUS-0096,MO-036,2025-12-30T04:00:00Z,2026-01-06T04:00:00Z,returned,41455,41860,true
+BK-H-0206,CUS-0107,MO-043,2025-12-28T03:00:00Z,2026-01-05T03:00:00Z,returned,46313,46719,true
+BK-H-0207,CUS-0118,MO-050,2025-12-26T02:00:00Z,2026-01-04T02:00:00Z,returned,51485,51892,true
+BK-H-0208,CUS-0129,MO-007,2025-12-24T09:00:00Z,2026-01-03T09:00:00Z,returned,20481,20889,true
+BK-H-0209,CUS-0140,MO-014,2025-12-22T08:00:00Z,2026-01-02T08:00:00Z,returned,25219,25628,true
+BK-H-0210,CUS-0151,MO-021,2025-12-30T07:00:00Z,2026-01-01T07:00:00Z,returned,30888,31298,true
+BK-H-0211,CUS-0162,MO-028,2025-12-28T06:00:00Z,2025-12-31T06:00:00Z,returned,35748,36159,true
+BK-H-0212,CUS-0173,MO-035,2025-12-26T05:00:00Z,2025-12-30T05:00:00Z,returned,40822,41234,true
+BK-H-0213,CUS-0004,MO-042,2025-12-24T04:00:00Z,2025-12-29T04:00:00Z,returned,46335,46748,true
+BK-H-0214,CUS-0015,MO-049,2025-12-22T03:00:00Z,2025-12-28T03:00:00Z,returned,50721,51135,true
+BK-H-0215,CUS-0026,MO-006,2025-12-20T02:00:00Z,2025-12-27T02:00:00Z,returned,19948,20363,true
+BK-H-0216,CUS-0037,MO-013,2025-12-18T09:00:00Z,2025-12-26T09:00:00Z,returned,24534,24950,true
+BK-H-0217,CUS-0048,MO-020,2025-12-16T08:00:00Z,2025-12-25T08:00:00Z,returned,29399,29816,true
+BK-H-0218,CUS-0059,MO-027,2025-12-14T07:00:00Z,2025-12-24T07:00:00Z,returned,35284,35702,true
+BK-H-0219,CUS-0070,MO-034,2025-12-12T06:00:00Z,2025-12-23T06:00:00Z,returned,39838,40257,true
+BK-H-0220,CUS-0081,MO-041,2025-12-20T05:00:00Z,2025-12-22T05:00:00Z,returned,45223,45643,true
+BK-DEMO-RETURN,CUS-0042,MO-024,2026-07-28T09:00:00Z,2026-08-01T09:00:00Z,active,53610,,true
+BK-F-001,CUS-0014,MO-010,2026-08-03T10:00:00Z,2026-08-07T10:00:00Z,reserved,,,true
+BK-F-002,CUS-0027,MO-019,2026-08-04T11:00:00Z,2026-08-09T11:00:00Z,reserved,,,true
+BK-F-003,CUS-0040,MO-028,2026-08-05T12:00:00Z,2026-08-11T12:00:00Z,reserved,,,false
+BK-F-004,CUS-0053,MO-037,2026-08-06T13:00:00Z,2026-08-13T13:00:00Z,reserved,,,true
+BK-F-005,CUS-0066,MO-046,2026-08-07T09:00:00Z,2026-08-15T09:00:00Z,reserved,,,true
+BK-F-006,CUS-0079,MO-005,2026-08-08T10:00:00Z,2026-08-17T10:00:00Z,reserved,,,true
+BK-F-007,CUS-0092,MO-014,2026-08-09T11:00:00Z,2026-08-19T11:00:00Z,reserved,,,true
+BK-F-008,CUS-0105,MO-023,2026-08-10T12:00:00Z,2026-08-13T12:00:00Z,reserved,,,true
+BK-F-009,CUS-0118,MO-032,2026-08-11T13:00:00Z,2026-08-15T13:00:00Z,reserved,,,true
+BK-F-010,CUS-0131,MO-041,2026-08-12T09:00:00Z,2026-08-17T09:00:00Z,reserved,,,true
+BK-F-011,CUS-0144,MO-050,2026-08-13T10:00:00Z,2026-08-19T10:00:00Z,reserved,,,false
+BK-F-012,CUS-0157,MO-009,2026-08-14T11:00:00Z,2026-08-21T11:00:00Z,reserved,,,true
+BK-F-013,CUS-0170,MO-018,2026-08-15T12:00:00Z,2026-08-23T12:00:00Z,reserved,,,true
+BK-F-014,CUS-0003,MO-027,2026-08-16T13:00:00Z,2026-08-25T13:00:00Z,reserved,,,true
+BK-F-015,CUS-0016,MO-036,2026-08-17T09:00:00Z,2026-08-27T09:00:00Z,reserved,,,true
+BK-F-016,CUS-0029,MO-045,2026-08-18T10:00:00Z,2026-08-21T10:00:00Z,reserved,,,true
+BK-F-017,CUS-0042,MO-004,2026-08-19T11:00:00Z,2026-08-23T11:00:00Z,reserved,,,true
+BK-F-018,CUS-0055,MO-013,2026-08-20T12:00:00Z,2026-08-25T12:00:00Z,reserved,,,true
+BK-F-019,CUS-0068,MO-022,2026-08-21T13:00:00Z,2026-08-27T13:00:00Z,reserved,,,true
+BK-F-020,CUS-0081,MO-031,2026-08-22T09:00:00Z,2026-08-29T09:00:00Z,reserved,,,true
+BK-F-021,CUS-0094,MO-040,2026-08-23T10:00:00Z,2026-08-31T10:00:00Z,reserved,,,true
+BK-F-022,CUS-0107,MO-049,2026-08-24T11:00:00Z,2026-09-02T11:00:00Z,reserved,,,true
+BK-DEMO-NEXT,CUS-0088,MO-031,2026-08-02T10:00:00Z,2026-08-07T09:00:00Z,reserved,,,true
+BK-DEMO-OVERLAP-A,CUS-0101,MO-016,2026-08-03T09:00:00Z,2026-08-08T09:00:00Z,reserved,,,true
+BK-DEMO-OVERLAP-B,CUS-0102,MO-016,2026-08-05T09:00:00Z,2026-08-10T09:00:00Z,reserved,,,true
diff --git a/seed/customers.csv b/seed/customers.csv
new file mode 100644
index 0000000..5eca117
--- /dev/null
+++ b/seed/customers.csv
@@ -0,0 +1,181 @@
+public_ref,first_name,last_name,email,phone,postal_code,city,merged_into
+CUS-0001,Noor,Vermeulen,noor.vermeulen.1@example.test,+32 471 100001,2300,Turnhout,
+CUS-0002,Bram,De Smet,bram.desmet.2@example.test,+32 472 100002,2200,Herentals,
+CUS-0003,Pieter,Willems,pieter.willems.3@example.test,+32 473 100003,2400,Mol,
+CUS-0004,Eva,Goossens,eva.goossens.4@example.test,+32 474 100004,2260,Westerlo,
+CUS-0005,Sofie,Janssens,sofie.janssens.5@example.test,+32 475 100005,3980,Tessenderlo-Ham,
+CUS-0006,Noor,Jacobs,noor.jacobs.6@example.test,+32 476 100006,2440,Geel,
+CUS-0007,Bram,Vandamme,bram.vandamme.7@example.test,+32 477 100007,2300,Turnhout,
+CUS-0008,Pieter,Claes,pieter.claes.8@example.test,+32 478 100008,2200,Herentals,
+CUS-0009,Eva,Wouters,eva.wouters.9@example.test,+32 479 100009,2400,Mol,
+CUS-0010,Sofie,Maes,sofie.maes.10@example.test,+32 480 100010,2260,Westerlo,
+CUS-0011,Noor,Mertens,noor.mertens.11@example.test,+32 481 100011,3980,Tessenderlo-Ham,
+CUS-0012,Bram,Peeters,bram.peeters.12@example.test,+32 482 100012,2440,Geel,
+CUS-0013,Pieter,Vermeulen,pieter.vermeulen.13@example.test,+32 483 100013,2300,Turnhout,
+CUS-0014,Eva,De Smet,eva.desmet.14@example.test,+32 484 100014,2200,Herentals,
+CUS-0015,Sofie,Willems,sofie.willems.15@example.test,+32 485 100015,2400,Mol,
+CUS-0016,Noor,Goossens,noor.goossens.16@example.test,+32 486 100016,2260,Westerlo,
+CUS-0017,Bram,Janssens,bram.janssens.17@example.test,+32 487 100017,3980,Tessenderlo-Ham,
+CUS-0018,Pieter,Jacobs,pieter.jacobs.18@example.test,+32 488 100018,2440,Geel,
+CUS-0019,Eva,Vandamme,eva.vandamme.19@example.test,+32 489 100019,2300,Turnhout,
+CUS-0020,Sofie,Claes,sofie.claes.20@example.test,+32 470 100020,2200,Herentals,
+CUS-0021,Noor,Wouters,noor.wouters.21@example.test,+32 471 100021,2400,Mol,
+CUS-0022,Bram,Maes,bram.maes.22@example.test,+32 472 100022,2260,Westerlo,
+CUS-0023,Pieter,Mertens,pieter.mertens.23@example.test,+32 473 100023,3980,Tessenderlo-Ham,
+CUS-0024,Eva,Peeters,eva.peeters.24@example.test,+32 474 100024,2440,Geel,
+CUS-0025,Sofie,Vermeulen,sofie.vermeulen.25@example.test,+32 475 100025,2300,Turnhout,
+CUS-0026,Noor,De Smet,noor.desmet.26@example.test,+32 476 100026,2200,Herentals,
+CUS-0027,Bram,Willems,bram.willems.27@example.test,+32 477 100027,2400,Mol,
+CUS-0028,Pieter,Goossens,pieter.goossens.28@example.test,+32 478 100028,2260,Westerlo,
+CUS-0029,Eva,Janssens,eva.janssens.29@example.test,+32 479 100029,3980,Tessenderlo-Ham,
+CUS-0030,Sofie,Jacobs,sofie.jacobs.30@example.test,+32 480 100030,2440,Geel,
+CUS-0031,Noor,Vandamme,noor.vandamme.31@example.test,+32 481 100031,2300,Turnhout,
+CUS-0032,Bram,Claes,bram.claes.32@example.test,+32 482 100032,2200,Herentals,
+CUS-0033,Pieter,Wouters,pieter.wouters.33@example.test,+32 483 100033,2400,Mol,
+CUS-0034,Eva,Maes,eva.maes.34@example.test,+32 484 100034,2260,Westerlo,
+CUS-0035,Sofie,Mertens,sofie.mertens.35@example.test,+32 485 100035,3980,Tessenderlo-Ham,
+CUS-0036,Noor,Peeters,noor.peeters.36@example.test,+32 486 100036,2440,Geel,
+CUS-0037,Bram,Vermeulen,bram.vermeulen.37@example.test,+32 487 100037,2300,Turnhout,
+CUS-0038,Pieter,De Smet,pieter.desmet.38@example.test,+32 488 100038,2200,Herentals,
+CUS-0039,Eva,Willems,eva.willems.39@example.test,+32 489 100039,2400,Mol,
+CUS-0040,Sofie,Goossens,sofie.goossens.40@example.test,+32 470 100040,2260,Westerlo,
+CUS-0041,Noor,Janssens,noor.janssens.41@example.test,+32 471 100041,3980,Tessenderlo-Ham,
+CUS-0042,Bram,Jacobs,bram.jacobs.42@example.test,+32 472 100042,2440,Geel,
+CUS-0043,Pieter,Vandamme,pieter.vandamme.43@example.test,+32 473 100043,2300,Turnhout,
+CUS-0044,Eva,Claes,eva.claes.44@example.test,+32 474 100044,2200,Herentals,
+CUS-0045,Sofie,Wouters,sofie.wouters.45@example.test,+32 475 100045,2400,Mol,
+CUS-0046,Noor,Maes,noor.maes.46@example.test,+32 476 100046,2260,Westerlo,
+CUS-0047,Bram,Mertens,bram.mertens.47@example.test,+32 477 100047,3980,Tessenderlo-Ham,
+CUS-0048,Pieter,Peeters,pieter.peeters.48@example.test,+32 478 100048,2440,Geel,
+CUS-0049,Eva,Vermeulen,eva.vermeulen.49@example.test,+32 479 100049,2300,Turnhout,
+CUS-0050,Sofie,De Smet,sofie.desmet.50@example.test,+32 480 100050,2200,Herentals,
+CUS-0051,Noor,Willems,noor.willems.51@example.test,+32 481 100051,2400,Mol,
+CUS-0052,Bram,Goossens,bram.goossens.52@example.test,+32 482 100052,2260,Westerlo,
+CUS-0053,Pieter,Janssens,pieter.janssens.53@example.test,+32 483 100053,3980,Tessenderlo-Ham,
+CUS-0054,Eva,Jacobs,eva.jacobs.54@example.test,+32 484 100054,2440,Geel,
+CUS-0055,Sofie,Vandamme,sofie.vandamme.55@example.test,+32 485 100055,2300,Turnhout,
+CUS-0056,Noor,Claes,noor.claes.56@example.test,+32 486 100056,2200,Herentals,
+CUS-0057,Bram,Wouters,bram.wouters.57@example.test,+32 487 100057,2400,Mol,
+CUS-0058,Pieter,Maes,pieter.maes.58@example.test,+32 488 100058,2260,Westerlo,
+CUS-0059,Eva,Mertens,eva.mertens.59@example.test,+32 489 100059,3980,Tessenderlo-Ham,
+CUS-0060,Sofie,Peeters,sofie.peeters.60@example.test,+32 470 100060,2440,Geel,
+CUS-0061,Noor,Vermeulen,noor.vermeulen.61@example.test,+32 471 100061,2300,Turnhout,
+CUS-0062,Bram,De Smet,bram.desmet.62@example.test,+32 472 100062,2200,Herentals,
+CUS-0063,Pieter,Willems,pieter.willems.63@example.test,+32 473 100063,2400,Mol,
+CUS-0064,Eva,Goossens,eva.goossens.64@example.test,+32 474 100064,2260,Westerlo,
+CUS-0065,Sofie,Janssens,sofie.janssens.65@example.test,+32 475 100065,3980,Tessenderlo-Ham,
+CUS-0066,Noor,Jacobs,noor.jacobs.66@example.test,+32 476 100066,2440,Geel,
+CUS-0067,Bram,Vandamme,bram.vandamme.67@example.test,+32 477 100067,2300,Turnhout,
+CUS-0068,Pieter,Claes,pieter.claes.68@example.test,+32 478 100068,2200,Herentals,
+CUS-0069,Eva,Wouters,eva.wouters.69@example.test,+32 479 100069,2400,Mol,
+CUS-0070,Sofie,Maes,sofie.maes.70@example.test,+32 480 100070,2260,Westerlo,
+CUS-0071,Noor,Mertens,noor.mertens.71@example.test,+32 481 100071,3980,Tessenderlo-Ham,
+CUS-0072,Bram,Peeters,bram.peeters.72@example.test,+32 482 100072,2440,Geel,
+CUS-0073,Pieter,Vermeulen,pieter.vermeulen.73@example.test,+32 483 100073,2300,Turnhout,
+CUS-0074,Eva,De Smet,eva.desmet.74@example.test,+32 484 100074,2200,Herentals,
+CUS-0075,Sofie,Willems,sofie.willems.75@example.test,+32 485 100075,2400,Mol,
+CUS-0076,Noor,Goossens,noor.goossens.76@example.test,+32 486 100076,2260,Westerlo,
+CUS-0077,Bram,Janssens,bram.janssens.77@example.test,+32 487 100077,3980,Tessenderlo-Ham,
+CUS-0078,Pieter,Jacobs,pieter.jacobs.78@example.test,+32 488 100078,2440,Geel,
+CUS-0079,Eva,Vandamme,eva.vandamme.79@example.test,+32 489 100079,2300,Turnhout,
+CUS-0080,Sofie,Claes,sofie.claes.80@example.test,+32 470 100080,2200,Herentals,
+CUS-0081,Noor,Wouters,noor.wouters.81@example.test,+32 471 100081,2400,Mol,
+CUS-0082,Bram,Maes,bram.maes.82@example.test,+32 472 100082,2260,Westerlo,
+CUS-0083,Pieter,Mertens,pieter.mertens.83@example.test,+32 473 100083,3980,Tessenderlo-Ham,
+CUS-0084,Eva,Peeters,eva.peeters.84@example.test,+32 474 100084,2440,Geel,
+CUS-0085,Sofie,Vermeulen,sofie.vermeulen.85@example.test,+32 475 100085,2300,Turnhout,
+CUS-0086,Noor,De Smet,noor.desmet.86@example.test,+32 476 100086,2200,Herentals,
+CUS-0087,Bram,Willems,bram.willems.87@example.test,+32 477 100087,2400,Mol,
+CUS-0088,Pieter,Goossens,pieter.goossens.88@example.test,+32 478 100088,2260,Westerlo,
+CUS-0089,Eva,Janssens,eva.janssens.89@example.test,+32 479 100089,3980,Tessenderlo-Ham,
+CUS-0090,Sofie,Jacobs,sofie.jacobs.90@example.test,+32 480 100090,2440,Geel,
+CUS-0091,Noor,Vandamme,noor.vandamme.91@example.test,+32 481 100091,2300,Turnhout,
+CUS-0092,Bram,Claes,bram.claes.92@example.test,+32 482 100092,2200,Herentals,
+CUS-0093,Pieter,Wouters,pieter.wouters.93@example.test,+32 483 100093,2400,Mol,
+CUS-0094,Eva,Maes,eva.maes.94@example.test,+32 484 100094,2260,Westerlo,
+CUS-0095,Sofie,Mertens,sofie.mertens.95@example.test,+32 485 100095,3980,Tessenderlo-Ham,
+CUS-0096,Noor,Peeters,noor.peeters.96@example.test,+32 486 100096,2440,Geel,
+CUS-0097,Bram,Vermeulen,bram.vermeulen.97@example.test,+32 487 100097,2300,Turnhout,
+CUS-0098,Pieter,De Smet,pieter.desmet.98@example.test,+32 488 100098,2200,Herentals,
+CUS-0099,Eva,Willems,eva.willems.99@example.test,+32 489 100099,2400,Mol,
+CUS-0100,Sofie,Goossens,sofie.goossens.100@example.test,+32 470 100100,2260,Westerlo,
+CUS-0101,Noor,Janssens,noor.janssens.101@example.test,+32 471 100101,3980,Tessenderlo-Ham,
+CUS-0102,Bram,Jacobs,bram.jacobs.102@example.test,+32 472 100102,2440,Geel,
+CUS-0103,Pieter,Vandamme,pieter.vandamme.103@example.test,+32 473 100103,2300,Turnhout,
+CUS-0104,Eva,Claes,eva.claes.104@example.test,+32 474 100104,2200,Herentals,
+CUS-0105,Sofie,Wouters,sofie.wouters.105@example.test,+32 475 100105,2400,Mol,
+CUS-0106,Noor,Maes,noor.maes.106@example.test,+32 476 100106,2260,Westerlo,
+CUS-0107,Bram,Mertens,bram.mertens.107@example.test,+32 477 100107,3980,Tessenderlo-Ham,
+CUS-0108,Pieter,Peeters,pieter.peeters.108@example.test,+32 478 100108,2440,Geel,
+CUS-0109,Eva,Vermeulen,eva.vermeulen.109@example.test,+32 479 100109,2300,Turnhout,
+CUS-0110,Sofie,De Smet,sofie.desmet.110@example.test,+32 480 100110,2200,Herentals,
+CUS-0111,Noor,Willems,noor.willems.111@example.test,+32 481 100111,2400,Mol,
+CUS-0112,Bram,Goossens,bram.goossens.112@example.test,+32 482 100112,2260,Westerlo,
+CUS-0113,Pieter,Janssens,pieter.janssens.113@example.test,+32 483 100113,3980,Tessenderlo-Ham,
+CUS-0114,Eva,Jacobs,eva.jacobs.114@example.test,+32 484 100114,2440,Geel,
+CUS-0115,Sofie,Vandamme,sofie.vandamme.115@example.test,+32 485 100115,2300,Turnhout,
+CUS-0116,Noor,Claes,noor.claes.116@example.test,+32 486 100116,2200,Herentals,
+CUS-0117,Bram,Wouters,bram.wouters.117@example.test,+32 487 100117,2400,Mol,
+CUS-0118,Pieter,Maes,pieter.maes.118@example.test,+32 488 100118,2260,Westerlo,
+CUS-0119,Eva,Mertens,eva.mertens.119@example.test,+32 489 100119,3980,Tessenderlo-Ham,
+CUS-0120,Sofie,Peeters,sofie.peeters.120@example.test,+32 470 100120,2440,Geel,
+CUS-0121,Noor,Vermeulen,noor.vermeulen.121@example.test,+32 471 100121,2300,Turnhout,
+CUS-0122,Bram,De Smet,bram.desmet.122@example.test,+32 472 100122,2200,Herentals,
+CUS-0123,Pieter,Willems,pieter.willems.123@example.test,+32 473 100123,2400,Mol,
+CUS-0124,Eva,Goossens,eva.goossens.124@example.test,+32 474 100124,2260,Westerlo,
+CUS-0125,Sofie,Janssens,sofie.janssens.125@example.test,+32 475 100125,3980,Tessenderlo-Ham,
+CUS-0126,Noor,Jacobs,noor.jacobs.126@example.test,+32 476 100126,2440,Geel,
+CUS-0127,Bram,Vandamme,bram.vandamme.127@example.test,+32 477 100127,2300,Turnhout,
+CUS-0128,Pieter,Claes,pieter.claes.128@example.test,+32 478 100128,2200,Herentals,
+CUS-0129,Eva,Wouters,eva.wouters.129@example.test,+32 479 100129,2400,Mol,
+CUS-0130,Sofie,Maes,sofie.maes.130@example.test,+32 480 100130,2260,Westerlo,
+CUS-0131,Noor,Mertens,noor.mertens.131@example.test,+32 481 100131,3980,Tessenderlo-Ham,
+CUS-0132,Bram,Peeters,bram.peeters.132@example.test,+32 482 100132,2440,Geel,
+CUS-0133,Pieter,Vermeulen,pieter.vermeulen.133@example.test,+32 483 100133,2300,Turnhout,
+CUS-0134,Eva,De Smet,eva.desmet.134@example.test,+32 484 100134,2200,Herentals,
+CUS-0135,Sofie,Willems,sofie.willems.135@example.test,+32 485 100135,2400,Mol,
+CUS-0136,Noor,Goossens,noor.goossens.136@example.test,+32 486 100136,2260,Westerlo,
+CUS-0137,Bram,Janssens,bram.janssens.137@example.test,+32 487 100137,3980,Tessenderlo-Ham,
+CUS-0138,Pieter,Jacobs,pieter.jacobs.138@example.test,+32 488 100138,2440,Geel,
+CUS-0139,Eva,Vandamme,eva.vandamme.139@example.test,+32 489 100139,2300,Turnhout,
+CUS-0140,Sofie,Claes,sofie.claes.140@example.test,+32 470 100140,2200,Herentals,
+CUS-0141,Noor,Wouters,noor.wouters.141@example.test,+32 471 100141,2400,Mol,
+CUS-0142,Bram,Maes,bram.maes.142@example.test,+32 472 100142,2260,Westerlo,
+CUS-0143,Pieter,Mertens,pieter.mertens.143@example.test,+32 473 100143,3980,Tessenderlo-Ham,
+CUS-0144,Eva,Peeters,eva.peeters.144@example.test,+32 474 100144,2440,Geel,
+CUS-0145,Sofie,Vermeulen,sofie.vermeulen.145@example.test,+32 475 100145,2300,Turnhout,
+CUS-0146,Noor,De Smet,noor.desmet.146@example.test,+32 476 100146,2200,Herentals,
+CUS-0147,Bram,Willems,bram.willems.147@example.test,+32 477 100147,2400,Mol,
+CUS-0148,Pieter,Goossens,pieter.goossens.148@example.test,+32 478 100148,2260,Westerlo,
+CUS-0149,Eva,Janssens,eva.janssens.149@example.test,+32 479 100149,3980,Tessenderlo-Ham,
+CUS-0150,Eva,Wouters,eva.wouters.69@example.test,+32 480 100150,2400,Mol,
+CUS-0151,Noor,Vandamme,noor.vandamme.151@example.test,+32 481 100151,2300,Turnhout,
+CUS-0152,Bram,Claes,bram.claes.152@example.test,+32 482 100152,2200,Herentals,
+CUS-0153,Pieter,Wouters,pieter.wouters.153@example.test,+32 483 100153,2400,Mol,
+CUS-0154,Eva,Maes,eva.maes.154@example.test,+32 484 100154,2260,Westerlo,
+CUS-0155,Sofie,Mertens,sofie.mertens.155@example.test,+32 485 100155,3980,Tessenderlo-Ham,
+CUS-0156,Noor,Peeters,noor.peeters.156@example.test,+32 486 100156,2440,Geel,
+CUS-0157,Bram,Vermeulen,bram.vermeulen.157@example.test,+32 487 100157,2300,Turnhout,
+CUS-0158,Pieter,De Smet,pieter.desmet.158@example.test,+32 488 100158,2200,Herentals,
+CUS-0159,Eva,Willems,eva.willems.159@example.test,+32 489 100159,2400,Mol,
+CUS-0160,Sofie,Goossens,sofie.goossens.160@example.test,+32 470 100160,2260,Westerlo,
+CUS-0161,Noor,Janssens,noor.janssens.161@example.test,+32 471 100161,3980,Tessenderlo-Ham,
+CUS-0162,Bram,Jacobs,bram.jacobs.162@example.test,+32 472 100162,2440,Geel,
+CUS-0163,Pieter,Vandamme,pieter.vandamme.163@example.test,+32 473 100163,2300,Turnhout,
+CUS-0164,Eva,Claes,eva.claes.164@example.test,+32 474 100164,2200,Herentals,
+CUS-0165,Bram,Claes,bram.claes.32@example.test,+32 475 100165,2200,Herentals,
+CUS-0166,Noor,Maes,noor.maes.166@example.test,+32 476 100166,2260,Westerlo,
+CUS-0167,Bram,Mertens,bram.mertens.167@example.test,+32 477 100167,3980,Tessenderlo-Ham,
+CUS-0168,Pieter,Peeters,pieter.peeters.168@example.test,+32 478 100168,2440,Geel,
+CUS-0169,Eva,Vermeulen,eva.vermeulen.169@example.test,+32 479 100169,2300,Turnhout,
+CUS-0170,Sofie,De Smet,sofie.desmet.170@example.test,+32 480 100170,2200,Herentals,
+CUS-0171,Noor,Willems,noor.willems.171@example.test,+32 481 100171,2400,Mol,
+CUS-0172,Bram,Goossens,bram.goossens.172@example.test,+32 482 100172,2260,Westerlo,
+CUS-0173,Pieter,Janssens,pieter.janssens.173@example.test,+32 483 100173,3980,Tessenderlo-Ham,
+CUS-0174,Eva,Jacobs,eva.jacobs.174@example.test,+32 484 100174,2440,Geel,
+CUS-0175,Sofie,Vandamme,sofie.vandamme.175@example.test,+32 485 100175,2300,Turnhout,
+CUS-0176,Noor,Claes,noor.claes.176@example.test,+32 486 100176,2200,Herentals,
+CUS-0177,Bram,Wouters,bram.wouters.177@example.test,+32 487 100177,2400,Mol,
+CUS-0178,B.,Peeters,bram.peeters.12@example.test,+32 482 100012,2440,Geel,
+CUS-0179,Eva,Mertens,eva.mertens.179@example.test,+32 489 100179,3980,Tessenderlo-Ham,
+CUS-0180,Sofie,Peeters,sofie.peeters.180@example.test,+32 470 100180,2440,Geel,
diff --git a/seed/data_quality_issues.csv b/seed/data_quality_issues.csv
new file mode 100644
index 0000000..4ec5a7b
--- /dev/null
+++ b/seed/data_quality_issues.csv
@@ -0,0 +1,16 @@
+public_ref,rule_type,entity_ref,related_ref,severity,status,evidence
+DQ-DEMO-DUPLICATE,possible_duplicate_customer,CUS-0012,CUS-0178,high,open,exact email; exact phone; exact postal code; similar name
+DQ-DEMO-OVERLAP,booking_overlap,MO-016,BK-DEMO-OVERLAP-A|BK-DEMO-OVERLAP-B,high,open,controlled legacy import overlap
+DQ-DEMO-STATUS,vehicle_status_conflict,MO-016,,high,open,vehicle marked available while reserved bookings conflict
+DQ-DEMO-ATTENTION,missing_required_field,MO-031,BK-DEMO-NEXT,high,open,near-future booking; required operational inspection missing
+DQ-0005,vehicle_status_conflict,MO-036,,high,open,Synthetic deterministic seed issue
+DQ-0006,missing_required_field,MO-043,,low,open,Synthetic deterministic seed issue
+DQ-0007,odometer_regression,MO-050,,medium,open,Synthetic deterministic seed issue
+DQ-0008,vehicle_status_conflict,MO-007,,high,open,Synthetic deterministic seed issue
+DQ-0009,missing_required_field,MO-014,,low,open,Synthetic deterministic seed issue
+DQ-0010,odometer_regression,MO-021,,medium,open,Synthetic deterministic seed issue
+DQ-0011,vehicle_status_conflict,MO-028,,high,open,Synthetic deterministic seed issue
+DQ-0012,missing_required_field,MO-035,,low,resolved,Synthetic deterministic seed issue
+DQ-0013,odometer_regression,MO-042,,medium,resolved,Synthetic deterministic seed issue
+DQ-0014,vehicle_status_conflict,MO-049,,high,resolved,Synthetic deterministic seed issue
+DQ-0015,missing_required_field,MO-006,,low,resolved,Synthetic deterministic seed issue
diff --git a/seed/generate_seed.py b/seed/generate_seed.py
new file mode 100644
index 0000000..2f0e003
--- /dev/null
+++ b/seed/generate_seed.py
@@ -0,0 +1,250 @@
+from __future__ import annotations
+
+import argparse
+import csv
+import random
+from datetime import date, datetime, time, timedelta, timezone
+from pathlib import Path
+
+FIRST_NAMES = ["Sofie", "Lotte", "Emma", "Noor", "Julie", "Thomas", "Bram", "Wout", "Niels", "Pieter", "Anke", "Sara", "Eva", "Tom", "Jeroen"]
+LAST_NAMES = ["Peeters", "Janssens", "Maes", "Willems", "Claes", "Vermeulen", "Jacobs", "Mertens", "Goossens", "Wouters", "De Smet", "Vandamme"]
+CITIES = [("2440", "Geel"), ("2300", "Turnhout"), ("2200", "Herentals"), ("2400", "Mol"), ("2260", "Westerlo"), ("3980", "Tessenderlo-Ham")]
+MAKES_MODELS = [("Adria", "Matrix"), ("Dethleffs", "Trend"), ("Carado", "T-Series"), ("Hymer", "Exsis"), ("Bürstner", "Lyseo"), ("Sunlight", "Cliff")]
+
+
+def write_csv(path: Path, rows: list[dict]) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ with path.open("w", newline="", encoding="utf-8") as handle:
+ writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
+ writer.writeheader()
+ writer.writerows(rows)
+
+
+def iso(dt: datetime) -> str:
+ return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
+
+
+def build(anchor: date, seed: int, out: Path) -> None:
+ rnd = random.Random(seed)
+ customers = []
+ used_emails = set()
+ for i in range(1, 181):
+ first = FIRST_NAMES[(i * 3) % len(FIRST_NAMES)]
+ last = LAST_NAMES[(i * 5) % len(LAST_NAMES)]
+ postal, city = CITIES[i % len(CITIES)]
+ email = f"{first}.{last}.{i}@example.test".lower().replace(" ", "")
+ used_emails.add(email)
+ customers.append({
+ "public_ref": f"CUS-{i:04d}",
+ "first_name": first,
+ "last_name": last,
+ "email": email,
+ "phone": f"+32 4{70 + (i % 20):02d} {100000 + i:06d}",
+ "postal_code": postal,
+ "city": city,
+ "merged_into": "",
+ })
+
+ # Fixed duplicate: CUS-0178 mirrors CUS-0012 with a shortened first name.
+ src = customers[11]
+ dup = customers[177]
+ dup.update({
+ "first_name": src["first_name"][0] + ".",
+ "last_name": src["last_name"],
+ "email": src["email"],
+ "phone": src["phone"],
+ "postal_code": src["postal_code"],
+ "city": src["city"],
+ })
+ # Two additional duplicate pairs.
+ for a, b in [(31, 164), (68, 149)]:
+ customers[b].update({
+ "first_name": customers[a]["first_name"],
+ "last_name": customers[a]["last_name"],
+ "email": customers[a]["email"],
+ "postal_code": customers[a]["postal_code"],
+ "city": customers[a]["city"],
+ })
+
+ vehicles = []
+ statuses = ["available"] * 22 + ["rented"] * 10 + ["cleaning"] * 6 + ["maintenance"] * 6 + ["blocked"] * 6
+ rnd.shuffle(statuses)
+ for i in range(1, 51):
+ make, model = MAKES_MODELS[(i - 1) % len(MAKES_MODELS)]
+ km = 18000 + i * 730 + rnd.randint(0, 900)
+ vehicles.append({
+ "public_ref": f"MO-{i:03d}",
+ "make": make,
+ "model": model,
+ "model_year": 2019 + (i % 7),
+ "registration_number": f"2-MOB-{i:03d}",
+ "location": "Geel",
+ "operational_status": statuses[i - 1],
+ "odometer_km": km,
+ "next_service_km": ((km // 10000) + 1) * 10000,
+ "active": "true",
+ })
+
+ # Fixed return scenario.
+ vehicles[23]["operational_status"] = "rented" # MO-024
+ vehicles[23]["odometer_km"] = 54820
+ # Fixed attention and conflict scenarios.
+ vehicles[30]["operational_status"] = "blocked" # MO-031
+ vehicles[15]["operational_status"] = "available" # MO-016 legacy conflict
+
+ bookings = []
+ base_dt = datetime.combine(anchor, time(9, 0), tzinfo=timezone.utc)
+ # 220 historical bookings.
+ for i in range(1, 221):
+ end = base_dt - timedelta(days=2 + (i % 320), hours=i % 8)
+ duration = 2 + (i % 10)
+ start = end - timedelta(days=duration)
+ vehicle_idx = (i * 7) % 50
+ start_km = int(vehicles[vehicle_idx]["odometer_km"]) - 3000 - (i % 700)
+ end_km = start_km + 200 + (i % 900)
+ bookings.append({
+ "public_ref": f"BK-H-{i:04d}",
+ "customer_ref": f"CUS-{((i * 11) % 180) + 1:04d}",
+ "vehicle_ref": f"MO-{vehicle_idx + 1:03d}",
+ "starts_at": iso(start),
+ "ends_at": iso(end),
+ "status": "returned",
+ "start_odometer_km": start_km,
+ "end_odometer_km": end_km,
+ "requirements_complete": "true",
+ })
+
+ # Active demo return booking.
+ bookings.append({
+ "public_ref": "BK-DEMO-RETURN",
+ "customer_ref": "CUS-0042",
+ "vehicle_ref": "MO-024",
+ "starts_at": iso(base_dt - timedelta(days=4)),
+ "ends_at": iso(base_dt),
+ "status": "active",
+ "start_odometer_km": 53610,
+ "end_odometer_km": "",
+ "requirements_complete": "true",
+ })
+
+ # 22 additional future bookings.
+ for i in range(1, 23):
+ start = base_dt + timedelta(days=1 + i, hours=(i % 5))
+ end = start + timedelta(days=3 + (i % 8))
+ vehicle = 1 + ((i * 9) % 50)
+ bookings.append({
+ "public_ref": f"BK-F-{i:03d}",
+ "customer_ref": f"CUS-{((i * 13) % 180) + 1:04d}",
+ "vehicle_ref": f"MO-{vehicle:03d}",
+ "starts_at": iso(start),
+ "ends_at": iso(end),
+ "status": "reserved",
+ "start_odometer_km": "",
+ "end_odometer_km": "",
+ "requirements_complete": "false" if i in (3, 11) else "true",
+ })
+
+ # MO-031 near-future booking, missing return inspection attention.
+ bookings.append({
+ "public_ref": "BK-DEMO-NEXT",
+ "customer_ref": "CUS-0088",
+ "vehicle_ref": "MO-031",
+ "starts_at": iso(base_dt + timedelta(days=1, hours=1)),
+ "ends_at": iso(base_dt + timedelta(days=6)),
+ "status": "reserved",
+ "start_odometer_km": "",
+ "end_odometer_km": "",
+ "requirements_complete": "true",
+ })
+
+ # Controlled legacy overlap on MO-016.
+ for suffix, offset in [("A", 2), ("B", 4)]:
+ bookings.append({
+ "public_ref": f"BK-DEMO-OVERLAP-{suffix}",
+ "customer_ref": "CUS-0101" if suffix == "A" else "CUS-0102",
+ "vehicle_ref": "MO-016",
+ "starts_at": iso(base_dt + timedelta(days=offset)),
+ "ends_at": iso(base_dt + timedelta(days=offset + 5)),
+ "status": "reserved",
+ "start_odometer_km": "",
+ "end_odometer_km": "",
+ "requirements_complete": "true",
+ })
+
+ inspections = []
+ for i, booking in enumerate(bookings[:75], 1):
+ inspections.append({
+ "public_ref": f"INSP-{i:04d}",
+ "booking_ref": booking["public_ref"],
+ "vehicle_ref": booking["vehicle_ref"],
+ "type": "return",
+ "fuel_level_percent": 50 + (i % 5) * 10,
+ "cleanliness_ok": "true",
+ "damage_reported": "false",
+ "technical_warning": "false",
+ "odometer_km": booking["end_odometer_km"] or "",
+ "completed_at": booking["ends_at"],
+ })
+
+ maintenance = []
+ for i in range(1, 41):
+ v = vehicles[(i * 3) % 50]
+ maintenance.append({
+ "public_ref": f"MNT-{i:04d}",
+ "vehicle_ref": v["public_ref"],
+ "occurred_at": iso(base_dt - timedelta(days=20 + i * 5)),
+ "odometer_km": int(v["odometer_km"]) - 1000 - i * 20,
+ "category": "periodic_service" if i % 3 else "repair",
+ "summary": "Synthetic scheduled service record" if i % 3 else "Synthetic minor repair record",
+ })
+
+ quality = [
+ {"public_ref":"DQ-DEMO-DUPLICATE","rule_type":"possible_duplicate_customer","entity_ref":"CUS-0012","related_ref":"CUS-0178","severity":"high","status":"open","evidence":"exact email; exact phone; exact postal code; similar name"},
+ {"public_ref":"DQ-DEMO-OVERLAP","rule_type":"booking_overlap","entity_ref":"MO-016","related_ref":"BK-DEMO-OVERLAP-A|BK-DEMO-OVERLAP-B","severity":"high","status":"open","evidence":"controlled legacy import overlap"},
+ {"public_ref":"DQ-DEMO-STATUS","rule_type":"vehicle_status_conflict","entity_ref":"MO-016","related_ref":"","severity":"high","status":"open","evidence":"vehicle marked available while reserved bookings conflict"},
+ {"public_ref":"DQ-DEMO-ATTENTION","rule_type":"missing_required_field","entity_ref":"MO-031","related_ref":"BK-DEMO-NEXT","severity":"high","status":"open","evidence":"near-future booking; required operational inspection missing"},
+ ]
+ for i in range(5, 16):
+ quality.append({
+ "public_ref": f"DQ-{i:04d}",
+ "rule_type": ["missing_required_field","odometer_regression","vehicle_status_conflict"][i % 3],
+ "entity_ref": f"MO-{((i * 7) % 50) + 1:03d}",
+ "related_ref": "",
+ "severity": ["low","medium","high"][i % 3],
+ "status": "open" if i < 12 else "resolved",
+ "evidence": "Synthetic deterministic seed issue",
+ })
+
+ workflow_runs = []
+ for i in range(1, 21):
+ status = "succeeded"
+ error = ""
+ if i == 20:
+ status = "failed"
+ error = "Synthetic connection timeout to n8n"
+ workflow_runs.append({
+ "event_id": f"00000000-0000-4000-8000-{i:012d}",
+ "event_type": "vehicle.returned.v1",
+ "aggregate_ref": f"BK-H-{i:04d}",
+ "status": status,
+ "attempts": 3 if status == "failed" else 1,
+ "last_error": error,
+ "occurred_at": iso(base_dt - timedelta(hours=i)),
+ })
+
+ write_csv(out / "customers.csv", customers)
+ write_csv(out / "vehicles.csv", vehicles)
+ write_csv(out / "bookings.csv", bookings)
+ write_csv(out / "inspections.csv", inspections)
+ write_csv(out / "maintenance.csv", maintenance)
+ write_csv(out / "data_quality_issues.csv", quality)
+ write_csv(out / "workflow_runs.csv", workflow_runs)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--anchor", type=date.fromisoformat, default=date(2026, 8, 1))
+ parser.add_argument("--seed", type=int, default=20260801)
+ parser.add_argument("--out", type=Path, default=Path(__file__).resolve().parent)
+ args = parser.parse_args()
+ build(args.anchor, args.seed, args.out)
diff --git a/seed/inspections.csv b/seed/inspections.csv
new file mode 100644
index 0000000..6fce3b2
--- /dev/null
+++ b/seed/inspections.csv
@@ -0,0 +1,76 @@
+public_ref,booking_ref,vehicle_ref,type,fuel_level_percent,cleanliness_ok,damage_reported,technical_warning,odometer_km,completed_at
+INSP-0001,BK-H-0001,MO-008,return,60,true,false,false,21133,2026-07-29T08:00:00Z
+INSP-0002,BK-H-0002,MO-015,return,70,true,false,false,26861,2026-07-28T07:00:00Z
+INSP-0003,BK-H-0003,MO-022,return,80,true,false,false,31610,2026-07-27T06:00:00Z
+INSP-0004,BK-H-0004,MO-029,return,90,true,false,false,36912,2026-07-26T05:00:00Z
+INSP-0005,BK-H-0005,MO-036,return,50,true,false,false,41860,2026-07-25T04:00:00Z
+INSP-0006,BK-H-0006,MO-043,return,60,true,false,false,46719,2026-07-24T03:00:00Z
+INSP-0007,BK-H-0007,MO-050,return,70,true,false,false,51892,2026-07-23T02:00:00Z
+INSP-0008,BK-H-0008,MO-007,return,80,true,false,false,20889,2026-07-22T09:00:00Z
+INSP-0009,BK-H-0009,MO-014,return,90,true,false,false,25628,2026-07-21T08:00:00Z
+INSP-0010,BK-H-0010,MO-021,return,50,true,false,false,31298,2026-07-20T07:00:00Z
+INSP-0011,BK-H-0011,MO-028,return,60,true,false,false,36159,2026-07-19T06:00:00Z
+INSP-0012,BK-H-0012,MO-035,return,70,true,false,false,41234,2026-07-18T05:00:00Z
+INSP-0013,BK-H-0013,MO-042,return,80,true,false,false,46748,2026-07-17T04:00:00Z
+INSP-0014,BK-H-0014,MO-049,return,90,true,false,false,51135,2026-07-16T03:00:00Z
+INSP-0015,BK-H-0015,MO-006,return,50,true,false,false,20363,2026-07-15T02:00:00Z
+INSP-0016,BK-H-0016,MO-013,return,60,true,false,false,24950,2026-07-14T09:00:00Z
+INSP-0017,BK-H-0017,MO-020,return,70,true,false,false,29816,2026-07-13T08:00:00Z
+INSP-0018,BK-H-0018,MO-027,return,80,true,false,false,35702,2026-07-12T07:00:00Z
+INSP-0019,BK-H-0019,MO-034,return,90,true,false,false,40257,2026-07-11T06:00:00Z
+INSP-0020,BK-H-0020,MO-041,return,50,true,false,false,45643,2026-07-10T05:00:00Z
+INSP-0021,BK-H-0021,MO-048,return,60,true,false,false,50893,2026-07-09T04:00:00Z
+INSP-0022,BK-H-0022,MO-005,return,70,true,false,false,19413,2026-07-08T03:00:00Z
+INSP-0023,BK-H-0023,MO-012,return,80,true,false,false,24059,2026-07-07T02:00:00Z
+INSP-0024,BK-H-0024,MO-019,return,90,true,false,false,29446,2026-07-06T09:00:00Z
+INSP-0025,BK-H-0025,MO-026,return,50,true,false,false,34473,2026-07-05T08:00:00Z
+INSP-0026,BK-H-0026,MO-033,return,60,true,false,false,39761,2026-07-04T07:00:00Z
+INSP-0027,BK-H-0027,MO-040,return,70,true,false,false,45109,2026-07-03T06:00:00Z
+INSP-0028,BK-H-0028,MO-047,return,80,true,false,false,49652,2026-07-02T05:00:00Z
+INSP-0029,BK-H-0029,MO-004,return,90,true,false,false,18355,2026-07-01T04:00:00Z
+INSP-0030,BK-H-0030,MO-011,return,50,true,false,false,23360,2026-06-30T03:00:00Z
+INSP-0031,BK-H-0031,MO-018,return,60,true,false,false,28782,2026-06-29T02:00:00Z
+INSP-0032,BK-H-0032,MO-025,return,70,true,false,false,33629,2026-06-28T09:00:00Z
+INSP-0033,BK-H-0033,MO-032,return,80,true,false,false,38991,2026-06-27T08:00:00Z
+INSP-0034,BK-H-0034,MO-039,return,90,true,false,false,43857,2026-06-26T07:00:00Z
+INSP-0035,BK-H-0035,MO-046,return,50,true,false,false,49157,2026-06-25T06:00:00Z
+INSP-0036,BK-H-0036,MO-003,return,60,true,false,false,18258,2026-06-24T05:00:00Z
+INSP-0037,BK-H-0037,MO-010,return,70,true,false,false,22533,2026-06-23T04:00:00Z
+INSP-0038,BK-H-0038,MO-017,return,80,true,false,false,27796,2026-06-22T03:00:00Z
+INSP-0039,BK-H-0039,MO-024,return,90,true,false,false,52020,2026-06-21T02:00:00Z
+INSP-0040,BK-H-0040,MO-031,return,50,true,false,false,38349,2026-06-20T09:00:00Z
+INSP-0041,BK-H-0041,MO-038,return,60,true,false,false,43830,2026-06-19T08:00:00Z
+INSP-0042,BK-H-0042,MO-045,return,70,true,false,false,48460,2026-06-18T07:00:00Z
+INSP-0043,BK-H-0043,MO-002,return,80,true,false,false,17163,2026-06-17T06:00:00Z
+INSP-0044,BK-H-0044,MO-009,return,90,true,false,false,22403,2026-06-16T05:00:00Z
+INSP-0045,BK-H-0045,MO-016,return,50,true,false,false,27697,2026-06-15T04:00:00Z
+INSP-0046,BK-H-0046,MO-023,return,60,true,false,false,32824,2026-06-14T03:00:00Z
+INSP-0047,BK-H-0047,MO-030,return,70,true,false,false,37413,2026-06-13T02:00:00Z
+INSP-0048,BK-H-0048,MO-037,return,80,true,false,false,42854,2026-06-12T09:00:00Z
+INSP-0049,BK-H-0049,MO-044,return,90,true,false,false,47811,2026-06-11T08:00:00Z
+INSP-0050,BK-H-0050,MO-001,return,50,true,false,false,16284,2026-06-10T07:00:00Z
+INSP-0051,BK-H-0051,MO-008,return,60,true,false,false,21133,2026-06-09T06:00:00Z
+INSP-0052,BK-H-0052,MO-015,return,70,true,false,false,26861,2026-06-08T05:00:00Z
+INSP-0053,BK-H-0053,MO-022,return,80,true,false,false,31610,2026-06-07T04:00:00Z
+INSP-0054,BK-H-0054,MO-029,return,90,true,false,false,36912,2026-06-06T03:00:00Z
+INSP-0055,BK-H-0055,MO-036,return,50,true,false,false,41860,2026-06-05T02:00:00Z
+INSP-0056,BK-H-0056,MO-043,return,60,true,false,false,46719,2026-06-04T09:00:00Z
+INSP-0057,BK-H-0057,MO-050,return,70,true,false,false,51892,2026-06-03T08:00:00Z
+INSP-0058,BK-H-0058,MO-007,return,80,true,false,false,20889,2026-06-02T07:00:00Z
+INSP-0059,BK-H-0059,MO-014,return,90,true,false,false,25628,2026-06-01T06:00:00Z
+INSP-0060,BK-H-0060,MO-021,return,50,true,false,false,31298,2026-05-31T05:00:00Z
+INSP-0061,BK-H-0061,MO-028,return,60,true,false,false,36159,2026-05-30T04:00:00Z
+INSP-0062,BK-H-0062,MO-035,return,70,true,false,false,41234,2026-05-29T03:00:00Z
+INSP-0063,BK-H-0063,MO-042,return,80,true,false,false,46748,2026-05-28T02:00:00Z
+INSP-0064,BK-H-0064,MO-049,return,90,true,false,false,51135,2026-05-27T09:00:00Z
+INSP-0065,BK-H-0065,MO-006,return,50,true,false,false,20363,2026-05-26T08:00:00Z
+INSP-0066,BK-H-0066,MO-013,return,60,true,false,false,24950,2026-05-25T07:00:00Z
+INSP-0067,BK-H-0067,MO-020,return,70,true,false,false,29816,2026-05-24T06:00:00Z
+INSP-0068,BK-H-0068,MO-027,return,80,true,false,false,35702,2026-05-23T05:00:00Z
+INSP-0069,BK-H-0069,MO-034,return,90,true,false,false,40257,2026-05-22T04:00:00Z
+INSP-0070,BK-H-0070,MO-041,return,50,true,false,false,45643,2026-05-21T03:00:00Z
+INSP-0071,BK-H-0071,MO-048,return,60,true,false,false,50893,2026-05-20T02:00:00Z
+INSP-0072,BK-H-0072,MO-005,return,70,true,false,false,19413,2026-05-19T09:00:00Z
+INSP-0073,BK-H-0073,MO-012,return,80,true,false,false,24059,2026-05-18T08:00:00Z
+INSP-0074,BK-H-0074,MO-019,return,90,true,false,false,29446,2026-05-17T07:00:00Z
+INSP-0075,BK-H-0075,MO-026,return,50,true,false,false,34473,2026-05-16T06:00:00Z
diff --git a/seed/maintenance.csv b/seed/maintenance.csv
new file mode 100644
index 0000000..9644a4f
--- /dev/null
+++ b/seed/maintenance.csv
@@ -0,0 +1,41 @@
+public_ref,vehicle_ref,occurred_at,odometer_km,category,summary
+MNT-0001,MO-004,2026-07-07T09:00:00Z,20135,periodic_service,Synthetic scheduled service record
+MNT-0002,MO-007,2026-07-02T09:00:00Z,22649,periodic_service,Synthetic scheduled service record
+MNT-0003,MO-010,2026-06-27T09:00:00Z,24273,repair,Synthetic minor repair record
+MNT-0004,MO-013,2026-06-22T09:00:00Z,26670,periodic_service,Synthetic scheduled service record
+MNT-0005,MO-016,2026-06-17T09:00:00Z,29397,periodic_service,Synthetic scheduled service record
+MNT-0006,MO-019,2026-06-12T09:00:00Z,31126,repair,Synthetic minor repair record
+MNT-0007,MO-022,2026-06-07T09:00:00Z,33270,periodic_service,Synthetic scheduled service record
+MNT-0008,MO-025,2026-06-02T09:00:00Z,35269,periodic_service,Synthetic scheduled service record
+MNT-0009,MO-028,2026-05-28T09:00:00Z,37779,repair,Synthetic minor repair record
+MNT-0010,MO-031,2026-05-23T09:00:00Z,39949,periodic_service,Synthetic scheduled service record
+MNT-0011,MO-034,2026-05-18T09:00:00Z,41837,periodic_service,Synthetic scheduled service record
+MNT-0012,MO-037,2026-05-13T09:00:00Z,44414,repair,Synthetic minor repair record
+MNT-0013,MO-040,2026-05-08T09:00:00Z,46649,periodic_service,Synthetic scheduled service record
+MNT-0014,MO-043,2026-05-03T09:00:00Z,48239,periodic_service,Synthetic scheduled service record
+MNT-0015,MO-046,2026-04-28T09:00:00Z,50657,repair,Synthetic minor repair record
+MNT-0016,MO-049,2026-04-23T09:00:00Z,52615,periodic_service,Synthetic scheduled service record
+MNT-0017,MO-002,2026-04-18T09:00:00Z,18623,periodic_service,Synthetic scheduled service record
+MNT-0018,MO-005,2026-04-13T09:00:00Z,20853,repair,Synthetic minor repair record
+MNT-0019,MO-008,2026-04-08T09:00:00Z,22553,periodic_service,Synthetic scheduled service record
+MNT-0020,MO-011,2026-04-03T09:00:00Z,24760,periodic_service,Synthetic scheduled service record
+MNT-0021,MO-014,2026-03-29T09:00:00Z,27008,repair,Synthetic minor repair record
+MNT-0022,MO-017,2026-03-24T09:00:00Z,29156,periodic_service,Synthetic scheduled service record
+MNT-0023,MO-020,2026-03-19T09:00:00Z,31156,periodic_service,Synthetic scheduled service record
+MNT-0024,MO-023,2026-03-14T09:00:00Z,34144,repair,Synthetic minor repair record
+MNT-0025,MO-026,2026-03-09T09:00:00Z,35773,periodic_service,Synthetic scheduled service record
+MNT-0026,MO-029,2026-03-04T09:00:00Z,38192,periodic_service,Synthetic scheduled service record
+MNT-0027,MO-032,2026-02-27T09:00:00Z,40251,repair,Synthetic minor repair record
+MNT-0028,MO-035,2026-02-22T09:00:00Z,42474,periodic_service,Synthetic scheduled service record
+MNT-0029,MO-038,2026-02-17T09:00:00Z,45050,periodic_service,Synthetic scheduled service record
+MNT-0030,MO-041,2026-02-12T09:00:00Z,46843,repair,Synthetic minor repair record
+MNT-0031,MO-044,2026-02-07T09:00:00Z,48991,periodic_service,Synthetic scheduled service record
+MNT-0032,MO-047,2026-02-02T09:00:00Z,50812,periodic_service,Synthetic scheduled service record
+MNT-0033,MO-050,2026-01-28T09:00:00Z,53032,repair,Synthetic minor repair record
+MNT-0034,MO-003,2026-01-23T09:00:00Z,19378,periodic_service,Synthetic scheduled service record
+MNT-0035,MO-006,2026-01-18T09:00:00Z,21463,periodic_service,Synthetic scheduled service record
+MNT-0036,MO-009,2026-01-13T09:00:00Z,23483,repair,Synthetic minor repair record
+MNT-0037,MO-012,2026-01-08T09:00:00Z,25119,periodic_service,Synthetic scheduled service record
+MNT-0038,MO-015,2026-01-03T09:00:00Z,27901,periodic_service,Synthetic scheduled service record
+MNT-0039,MO-018,2025-12-29T09:00:00Z,29802,repair,Synthetic minor repair record
+MNT-0040,MO-021,2025-12-24T09:00:00Z,32298,periodic_service,Synthetic scheduled service record
diff --git a/seed/vehicles.csv b/seed/vehicles.csv
new file mode 100644
index 0000000..086887f
--- /dev/null
+++ b/seed/vehicles.csv
@@ -0,0 +1,51 @@
+public_ref,make,model,model_year,registration_number,location,operational_status,odometer_km,next_service_km,active
+MO-001,Adria,Matrix,2020,2-MOB-001,Geel,maintenance,19084,20000,true
+MO-002,Dethleffs,Trend,2021,2-MOB-002,Geel,maintenance,19963,20000,true
+MO-003,Carado,T-Series,2022,2-MOB-003,Geel,rented,21058,30000,true
+MO-004,Hymer,Exsis,2023,2-MOB-004,Geel,cleaning,21155,30000,true
+MO-005,Bürstner,Lyseo,2024,2-MOB-005,Geel,available,22213,30000,true
+MO-006,Sunlight,Cliff,2025,2-MOB-006,Geel,cleaning,23163,30000,true
+MO-007,Adria,Matrix,2019,2-MOB-007,Geel,rented,23689,30000,true
+MO-008,Dethleffs,Trend,2020,2-MOB-008,Geel,available,23933,30000,true
+MO-009,Carado,T-Series,2021,2-MOB-009,Geel,blocked,25203,30000,true
+MO-010,Hymer,Exsis,2022,2-MOB-010,Geel,available,25333,30000,true
+MO-011,Bürstner,Lyseo,2023,2-MOB-011,Geel,available,26160,30000,true
+MO-012,Sunlight,Cliff,2024,2-MOB-012,Geel,available,26859,30000,true
+MO-013,Adria,Matrix,2025,2-MOB-013,Geel,rented,27750,30000,true
+MO-014,Dethleffs,Trend,2019,2-MOB-014,Geel,rented,28428,30000,true
+MO-015,Carado,T-Series,2020,2-MOB-015,Geel,available,29661,30000,true
+MO-016,Hymer,Exsis,2021,2-MOB-016,Geel,available,30497,40000,true
+MO-017,Bürstner,Lyseo,2022,2-MOB-017,Geel,available,30596,40000,true
+MO-018,Sunlight,Cliff,2023,2-MOB-018,Geel,maintenance,31582,40000,true
+MO-019,Adria,Matrix,2024,2-MOB-019,Geel,rented,32246,40000,true
+MO-020,Dethleffs,Trend,2025,2-MOB-020,Geel,available,32616,40000,true
+MO-021,Carado,T-Series,2019,2-MOB-021,Geel,cleaning,34098,40000,true
+MO-022,Hymer,Exsis,2020,2-MOB-022,Geel,available,34410,40000,true
+MO-023,Bürstner,Lyseo,2021,2-MOB-023,Geel,rented,35624,40000,true
+MO-024,Sunlight,Cliff,2022,2-MOB-024,Geel,rented,54820,40000,true
+MO-025,Adria,Matrix,2023,2-MOB-025,Geel,blocked,36429,40000,true
+MO-026,Dethleffs,Trend,2024,2-MOB-026,Geel,blocked,37273,40000,true
+MO-027,Carado,T-Series,2025,2-MOB-027,Geel,available,38502,40000,true
+MO-028,Hymer,Exsis,2019,2-MOB-028,Geel,cleaning,38959,40000,true
+MO-029,Bürstner,Lyseo,2020,2-MOB-029,Geel,rented,39712,40000,true
+MO-030,Sunlight,Cliff,2021,2-MOB-030,Geel,maintenance,40213,50000,true
+MO-031,Adria,Matrix,2022,2-MOB-031,Geel,blocked,41149,50000,true
+MO-032,Dethleffs,Trend,2023,2-MOB-032,Geel,rented,41791,50000,true
+MO-033,Carado,T-Series,2024,2-MOB-033,Geel,available,42561,50000,true
+MO-034,Hymer,Exsis,2025,2-MOB-034,Geel,available,43057,50000,true
+MO-035,Bürstner,Lyseo,2019,2-MOB-035,Geel,rented,44034,50000,true
+MO-036,Sunlight,Cliff,2020,2-MOB-036,Geel,available,44660,50000,true
+MO-037,Adria,Matrix,2021,2-MOB-037,Geel,available,45654,50000,true
+MO-038,Dethleffs,Trend,2022,2-MOB-038,Geel,available,46630,50000,true
+MO-039,Carado,T-Series,2023,2-MOB-039,Geel,rented,46657,50000,true
+MO-040,Hymer,Exsis,2024,2-MOB-040,Geel,available,47909,50000,true
+MO-041,Bürstner,Lyseo,2025,2-MOB-041,Geel,blocked,48443,50000,true
+MO-042,Sunlight,Cliff,2019,2-MOB-042,Geel,available,49548,50000,true
+MO-043,Adria,Matrix,2020,2-MOB-043,Geel,available,49519,50000,true
+MO-044,Dethleffs,Trend,2021,2-MOB-044,Geel,available,50611,60000,true
+MO-045,Carado,T-Series,2022,2-MOB-045,Geel,blocked,51260,60000,true
+MO-046,Hymer,Exsis,2023,2-MOB-046,Geel,cleaning,51957,60000,true
+MO-047,Bürstner,Lyseo,2024,2-MOB-047,Geel,available,52452,60000,true
+MO-048,Sunlight,Cliff,2025,2-MOB-048,Geel,maintenance,53693,60000,true
+MO-049,Adria,Matrix,2019,2-MOB-049,Geel,blocked,53935,60000,true
+MO-050,Dethleffs,Trend,2020,2-MOB-050,Geel,cleaning,54692,60000,true
diff --git a/seed/workflow_runs.csv b/seed/workflow_runs.csv
new file mode 100644
index 0000000..411975a
--- /dev/null
+++ b/seed/workflow_runs.csv
@@ -0,0 +1,21 @@
+event_id,event_type,aggregate_ref,status,attempts,last_error,occurred_at
+00000000-0000-4000-8000-000000000001,vehicle.returned.v1,BK-H-0001,succeeded,1,,2026-08-01T08:00:00Z
+00000000-0000-4000-8000-000000000002,vehicle.returned.v1,BK-H-0002,succeeded,1,,2026-08-01T07:00:00Z
+00000000-0000-4000-8000-000000000003,vehicle.returned.v1,BK-H-0003,succeeded,1,,2026-08-01T06:00:00Z
+00000000-0000-4000-8000-000000000004,vehicle.returned.v1,BK-H-0004,succeeded,1,,2026-08-01T05:00:00Z
+00000000-0000-4000-8000-000000000005,vehicle.returned.v1,BK-H-0005,succeeded,1,,2026-08-01T04:00:00Z
+00000000-0000-4000-8000-000000000006,vehicle.returned.v1,BK-H-0006,succeeded,1,,2026-08-01T03:00:00Z
+00000000-0000-4000-8000-000000000007,vehicle.returned.v1,BK-H-0007,succeeded,1,,2026-08-01T02:00:00Z
+00000000-0000-4000-8000-000000000008,vehicle.returned.v1,BK-H-0008,succeeded,1,,2026-08-01T01:00:00Z
+00000000-0000-4000-8000-000000000009,vehicle.returned.v1,BK-H-0009,succeeded,1,,2026-08-01T00:00:00Z
+00000000-0000-4000-8000-000000000010,vehicle.returned.v1,BK-H-0010,succeeded,1,,2026-07-31T23:00:00Z
+00000000-0000-4000-8000-000000000011,vehicle.returned.v1,BK-H-0011,succeeded,1,,2026-07-31T22:00:00Z
+00000000-0000-4000-8000-000000000012,vehicle.returned.v1,BK-H-0012,succeeded,1,,2026-07-31T21:00:00Z
+00000000-0000-4000-8000-000000000013,vehicle.returned.v1,BK-H-0013,succeeded,1,,2026-07-31T20:00:00Z
+00000000-0000-4000-8000-000000000014,vehicle.returned.v1,BK-H-0014,succeeded,1,,2026-07-31T19:00:00Z
+00000000-0000-4000-8000-000000000015,vehicle.returned.v1,BK-H-0015,succeeded,1,,2026-07-31T18:00:00Z
+00000000-0000-4000-8000-000000000016,vehicle.returned.v1,BK-H-0016,succeeded,1,,2026-07-31T17:00:00Z
+00000000-0000-4000-8000-000000000017,vehicle.returned.v1,BK-H-0017,succeeded,1,,2026-07-31T16:00:00Z
+00000000-0000-4000-8000-000000000018,vehicle.returned.v1,BK-H-0018,succeeded,1,,2026-07-31T15:00:00Z
+00000000-0000-4000-8000-000000000019,vehicle.returned.v1,BK-H-0019,succeeded,1,,2026-07-31T14:00:00Z
+00000000-0000-4000-8000-000000000020,vehicle.returned.v1,BK-H-0020,failed,3,Synthetic connection timeout to n8n,2026-07-31T13:00:00Z