Files
MobilityOps/seed/generate_seed.py
T
NuklearRabbit 81e3fd63bd
MobilityOps acceptance / backend (push) Failing after 19s
MobilityOps acceptance / frontend (push) Successful in 25s
MobilityOps acceptance / e2e (push) Skipped
M54: harden operations and demo resilience
2026-08-24 03:31:03 +02:00

660 lines
22 KiB
Python

from __future__ import annotations
import argparse
import csv
import random
from datetime import UTC, date, datetime, time, timedelta
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:
# csv.excel defaults to CRLF even on Linux. The repository canonicalises text
# files to LF, so make that byte contract explicit and platform-independent.
writer = csv.DictWriter(handle, fieldnames=list(rows[0]), lineterminator="\n")
writer.writeheader()
writer.writerows(rows)
def iso(dt: datetime) -> str:
return dt.astimezone(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
vehicles[6]["operational_status"] = "rented" # MO-007 has no active booking
vehicles_by_ref = {vehicle["public_ref"]: vehicle for vehicle in vehicles}
# Real missing-field evidence for every seeded open missing-field issue. Resolved
# rows deliberately keep their field populated: the seed represents the post-fix
# state while preserving the historical issue.
for vehicle_ref in ("MO-009", "MO-014", "MO-025", "MO-041", "MO-043", "MO-045", "MO-049"):
vehicles_by_ref[vehicle_ref]["location"] = ""
vehicles_by_ref["MO-026"]["registration_number"] = ""
# Deliberate service-threshold conflicts with evidence that is true in the CSV.
vehicles_by_ref["MO-028"]["next_service_km"] = 38000
vehicles_by_ref["MO-036"]["next_service_km"] = 44000
bookings = []
base_dt = datetime.combine(anchor, time(9, 0), tzinfo=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",
}
)
bookings_by_ref = {booking["public_ref"]: booking for booking in bookings}
# Two intentional imported regressions. The later return and its inspection retain
# the submitted reading as evidence while an earlier return remains the canonical
# higher observation used by DQ-0007 and DQ-0010.
bookings_by_ref["BK-H-0007"]["end_odometer_km"] = (
int(bookings_by_ref["BK-H-0057"]["end_odometer_km"]) - 192
)
bookings_by_ref["BK-H-0010"]["end_odometer_km"] = (
int(bookings_by_ref["BK-H-0060"]["end_odometer_km"]) - 148
)
# One historical return is curated onto the anchor day for the dashboard; its
# generated inspection inherits the same completion timestamp.
bookings_by_ref["BK-H-0001"]["starts_at"] = iso(base_dt - timedelta(days=3, hours=1))
bookings_by_ref["BK-H-0001"]["ends_at"] = iso(base_dt - timedelta(hours=1))
# 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(hours=i)
if i in (1, 2)
else 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",
}
)
# Eight additional anchor-day movements make the operational overview feel like a
# working fleet while staying deterministic for any requested anchor date.
traffic_rows = [
(
"BK-T-001",
"CUS-0033",
"MO-003",
-8,
time(5, 30),
0,
time(5, 30),
"returned",
20200,
20850,
),
(
"BK-T-002",
"CUS-0055",
"MO-011",
-5,
time(6, 30),
0,
time(6, 30),
"returned",
25400,
25980,
),
(
"BK-T-003",
"CUS-0077",
"MO-020",
-10,
time(8, 45),
0,
time(8, 45),
"returned",
31800,
32350,
),
(
"BK-T-004",
"CUS-0099",
"MO-033",
-2,
time(12, 0),
0,
time(12, 0),
"returned",
41700,
42200,
),
("BK-T-005", "CUS-0111", "MO-006", 0, time(7, 15), 3, time(7, 15), "reserved", "", ""),
("BK-T-006", "CUS-0123", "MO-017", 0, time(10, 30), 5, time(10, 30), "reserved", "", ""),
("BK-T-007", "CUS-0141", "MO-029", 0, time(13, 15), 7, time(13, 15), "reserved", "", ""),
("BK-T-008", "CUS-0155", "MO-038", 0, time(14, 30), 8, time(14, 30), "reserved", "", ""),
]
for (
public_ref,
customer_ref,
vehicle_ref,
start_day,
start_time,
end_day,
end_time,
status,
start_odometer,
end_odometer,
) in traffic_rows:
bookings.append(
{
"public_ref": public_ref,
"customer_ref": customer_ref,
"vehicle_ref": vehicle_ref,
"starts_at": iso(
datetime.combine(anchor + timedelta(days=start_day), start_time, tzinfo=UTC)
),
"ends_at": iso(
datetime.combine(anchor + timedelta(days=end_day), end_time, tzinfo=UTC)
),
"status": status,
"start_odometer_km": start_odometer,
"end_odometer_km": end_odometer,
"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"],
}
)
inspections_by_ref = {inspection["public_ref"]: inspection for inspection in inspections}
returned_readings_by_vehicle: dict[str, list[tuple[datetime, int]]] = {}
for booking in bookings:
if booking["status"] != "returned" or not booking["end_odometer_km"]:
continue
returned_readings_by_vehicle.setdefault(booking["vehicle_ref"], []).append(
(
datetime.fromisoformat(str(booking["ends_at"]).replace("Z", "+00:00")),
int(booking["end_odometer_km"]),
)
)
for readings in returned_readings_by_vehicle.values():
readings.sort()
maintenance = []
for i in range(1, 41):
v = vehicles[(i * 3) % 50]
occurred_at = base_dt - timedelta(days=20 + i * 5)
readings = returned_readings_by_vehicle[v["public_ref"]]
prior_readings = [reading for reading in readings if reading[0] <= occurred_at]
# Keep the synthetic cross-source history internally consistent. Historical
# bookings for a vehicle can share a reading in this compact PoC dataset, so the
# nearest known returned reading is a safer seed baseline than deriving an
# unrelated value from today's canonical odometer.
maintenance_odometer = (prior_readings[-1] if prior_readings else readings[0])[1]
maintenance.append(
{
"public_ref": f"MNT-{i:04d}",
"vehicle_ref": v["public_ref"],
"occurred_at": iso(occurred_at),
"odometer_km": maintenance_odometer,
"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",
},
{
"public_ref": "DQ-0005",
"rule_type": "vehicle_status_conflict",
"entity_ref": "MO-036",
"related_ref": "",
"severity": "high",
"status": "open",
"evidence": (
"Recommended status: maintenance "
f"({vehicles_by_ref['MO-036']['odometer_km']} km reported against a "
f"{vehicles_by_ref['MO-036']['next_service_km']} km service threshold)"
),
},
{
"public_ref": "DQ-0006",
"rule_type": "missing_required_field",
"entity_ref": "MO-043",
"related_ref": "",
"severity": "low",
"status": "open",
"evidence": "Missing: location",
},
{
"public_ref": "DQ-0007",
"rule_type": "odometer_regression",
"entity_ref": "MO-050",
"related_ref": "",
"severity": "medium",
"status": "open",
"evidence": (
"Return inspection INSP-0007 recorded "
f"{inspections_by_ref['INSP-0007']['odometer_km']} km, below the "
f"{inspections_by_ref['INSP-0057']['odometer_km']} km recorded by "
"earlier inspection INSP-0057."
),
},
{
"public_ref": "DQ-0008",
"rule_type": "vehicle_status_conflict",
"entity_ref": "MO-007",
"related_ref": "",
"severity": "high",
"status": "open",
"evidence": "Recommended status: available (marked rented with no active booking)",
},
{
"public_ref": "DQ-0009",
"rule_type": "missing_required_field",
"entity_ref": "MO-014",
"related_ref": "",
"severity": "low",
"status": "open",
"evidence": "Missing: location",
},
{
"public_ref": "DQ-0010",
"rule_type": "odometer_regression",
"entity_ref": "MO-021",
"related_ref": "",
"severity": "medium",
"status": "open",
"evidence": (
"Return inspection INSP-0010 recorded "
f"{inspections_by_ref['INSP-0010']['odometer_km']} km, below the "
f"{inspections_by_ref['INSP-0060']['odometer_km']} km recorded by "
"earlier inspection INSP-0060."
),
},
{
"public_ref": "DQ-0011",
"rule_type": "vehicle_status_conflict",
"entity_ref": "MO-028",
"related_ref": "",
"severity": "high",
"status": "open",
"evidence": (
"Recommended status: maintenance "
f"({vehicles_by_ref['MO-028']['odometer_km']} km reported against a "
f"{vehicles_by_ref['MO-028']['next_service_km']} km service threshold)"
),
},
{
"public_ref": "DQ-0012",
"rule_type": "missing_required_field",
"entity_ref": "MO-035",
"related_ref": "",
"severity": "low",
"status": "resolved",
"evidence": "Missing: registration_number",
},
{
"public_ref": "DQ-0013",
"rule_type": "missing_required_field",
"entity_ref": "MO-042",
"related_ref": "",
"severity": "low",
"status": "resolved",
"evidence": "Missing: location",
},
{
"public_ref": "DQ-0014",
"rule_type": "missing_required_field",
"entity_ref": "MO-049",
"related_ref": "",
"severity": "low",
"status": "resolved",
"evidence": "Missing: registration_number",
},
{
"public_ref": "DQ-0015",
"rule_type": "missing_required_field",
"entity_ref": "MO-006",
"related_ref": "",
"severity": "low",
"status": "resolved",
"evidence": "Missing: location",
},
{
"public_ref": "DQ-0016",
"rule_type": "missing_required_field",
"entity_ref": "MO-009",
"related_ref": "",
"severity": "medium",
"status": "open",
"evidence": "Missing: location",
},
{
"public_ref": "DQ-0017",
"rule_type": "missing_required_field",
"entity_ref": "MO-025",
"related_ref": "",
"severity": "medium",
"status": "open",
"evidence": "Missing: location",
},
{
"public_ref": "DQ-0018",
"rule_type": "missing_required_field",
"entity_ref": "MO-026",
"related_ref": "",
"severity": "medium",
"status": "open",
"evidence": "Missing: registration_number",
},
{
"public_ref": "DQ-0019",
"rule_type": "missing_required_field",
"entity_ref": "MO-041",
"related_ref": "",
"severity": "medium",
"status": "open",
"evidence": "Missing: location",
},
{
"public_ref": "DQ-0020",
"rule_type": "missing_required_field",
"entity_ref": "MO-045",
"related_ref": "",
"severity": "medium",
"status": "open",
"evidence": "Missing: location",
},
{
"public_ref": "DQ-0021",
"rule_type": "missing_required_field",
"entity_ref": "MO-049",
"related_ref": "",
"severity": "medium",
"status": "open",
"evidence": "Missing: location",
},
]
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)