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)