M54: harden operations and demo resilience
This commit is contained in:
@@ -5,10 +5,10 @@ DQ-DEMO-STATUS,vehicle_status_conflict,MO-016,,high,open,vehicle marked availabl
|
||||
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,Recommended status: maintenance (44660 km reported against a 44000 km service threshold)
|
||||
DQ-0006,missing_required_field,MO-043,,low,open,Missing: location
|
||||
DQ-0007,odometer_regression,MO-050,,medium,open,Booking BK-H-0007 recorded 51700 km, below the 51892 km recorded by earlier booking BK-H-0057.
|
||||
DQ-0007,odometer_regression,MO-050,,medium,open,"Return inspection INSP-0007 recorded 51700 km, below the 51892 km recorded by earlier inspection INSP-0057."
|
||||
DQ-0008,vehicle_status_conflict,MO-007,,high,open,Recommended status: available (marked rented with no active booking)
|
||||
DQ-0009,missing_required_field,MO-014,,low,open,Missing: location
|
||||
DQ-0010,odometer_regression,MO-021,,medium,open,Booking BK-H-0010 recorded 31150 km, below the 31298 km recorded by earlier booking BK-H-0060.
|
||||
DQ-0010,odometer_regression,MO-021,,medium,open,"Return inspection INSP-0010 recorded 31150 km, below the 31298 km recorded by earlier inspection INSP-0060."
|
||||
DQ-0011,vehicle_status_conflict,MO-028,,high,open,Recommended status: maintenance (38959 km reported against a 38000 km service threshold)
|
||||
DQ-0012,missing_required_field,MO-035,,low,resolved,Missing: registration_number
|
||||
DQ-0013,missing_required_field,MO-042,,low,resolved,Missing: location
|
||||
|
||||
|
+553
-144
@@ -3,25 +3,70 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import csv
|
||||
import random
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
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")]
|
||||
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]))
|
||||
# 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(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
return dt.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def build(anchor: date, seed: int, out: Path) -> None:
|
||||
@@ -34,56 +79,70 @@ def build(anchor: date, seed: int, out: Path) -> None:
|
||||
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": "",
|
||||
})
|
||||
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"],
|
||||
})
|
||||
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"],
|
||||
})
|
||||
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
|
||||
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",
|
||||
})
|
||||
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
|
||||
@@ -91,9 +150,21 @@ def build(anchor: date, seed: int, out: Path) -> None:
|
||||
# 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=timezone.utc)
|
||||
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)
|
||||
@@ -102,118 +173,454 @@ def build(anchor: date, seed: int, out: Path) -> None:
|
||||
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.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",
|
||||
})
|
||||
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))
|
||||
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",
|
||||
})
|
||||
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)),
|
||||
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.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]
|
||||
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",
|
||||
})
|
||||
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"},
|
||||
]
|
||||
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}",
|
||||
{
|
||||
"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": ["low","medium","high"][i % 3],
|
||||
"status": "open" if i < 12 else "resolved",
|
||||
"evidence": "Synthetic deterministic seed issue",
|
||||
})
|
||||
"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):
|
||||
@@ -222,15 +629,17 @@ def build(anchor: date, seed: int, out: Path) -> None:
|
||||
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)),
|
||||
})
|
||||
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)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
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-0001,BK-H-0001,MO-008,return,60,true,false,false,21133,2026-08-01T08: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-0007,BK-H-0007,MO-050,return,70,true,false,false,51700,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-0010,BK-H-0010,MO-021,return,50,true,false,false,31150,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
|
||||
|
||||
|
+40
-40
@@ -1,41 +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
|
||||
MNT-0001,MO-004,2026-07-07T09:00:00Z,18355,periodic_service,Synthetic scheduled service record
|
||||
MNT-0002,MO-007,2026-07-02T09:00:00Z,20889,periodic_service,Synthetic scheduled service record
|
||||
MNT-0003,MO-010,2026-06-27T09:00:00Z,22533,repair,Synthetic minor repair record
|
||||
MNT-0004,MO-013,2026-06-22T09:00:00Z,24950,periodic_service,Synthetic scheduled service record
|
||||
MNT-0005,MO-016,2026-06-17T09:00:00Z,27697,periodic_service,Synthetic scheduled service record
|
||||
MNT-0006,MO-019,2026-06-12T09:00:00Z,29446,repair,Synthetic minor repair record
|
||||
MNT-0007,MO-022,2026-06-07T09:00:00Z,31610,periodic_service,Synthetic scheduled service record
|
||||
MNT-0008,MO-025,2026-06-02T09:00:00Z,33629,periodic_service,Synthetic scheduled service record
|
||||
MNT-0009,MO-028,2026-05-28T09:00:00Z,36159,repair,Synthetic minor repair record
|
||||
MNT-0010,MO-031,2026-05-23T09:00:00Z,38349,periodic_service,Synthetic scheduled service record
|
||||
MNT-0011,MO-034,2026-05-18T09:00:00Z,40257,periodic_service,Synthetic scheduled service record
|
||||
MNT-0012,MO-037,2026-05-13T09:00:00Z,42854,repair,Synthetic minor repair record
|
||||
MNT-0013,MO-040,2026-05-08T09:00:00Z,45109,periodic_service,Synthetic scheduled service record
|
||||
MNT-0014,MO-043,2026-05-03T09:00:00Z,46719,periodic_service,Synthetic scheduled service record
|
||||
MNT-0015,MO-046,2026-04-28T09:00:00Z,49157,repair,Synthetic minor repair record
|
||||
MNT-0016,MO-049,2026-04-23T09:00:00Z,51135,periodic_service,Synthetic scheduled service record
|
||||
MNT-0017,MO-002,2026-04-18T09:00:00Z,17163,periodic_service,Synthetic scheduled service record
|
||||
MNT-0018,MO-005,2026-04-13T09:00:00Z,19413,repair,Synthetic minor repair record
|
||||
MNT-0019,MO-008,2026-04-08T09:00:00Z,21133,periodic_service,Synthetic scheduled service record
|
||||
MNT-0020,MO-011,2026-04-03T09:00:00Z,23360,periodic_service,Synthetic scheduled service record
|
||||
MNT-0021,MO-014,2026-03-29T09:00:00Z,25628,repair,Synthetic minor repair record
|
||||
MNT-0022,MO-017,2026-03-24T09:00:00Z,27796,periodic_service,Synthetic scheduled service record
|
||||
MNT-0023,MO-020,2026-03-19T09:00:00Z,29816,periodic_service,Synthetic scheduled service record
|
||||
MNT-0024,MO-023,2026-03-14T09:00:00Z,32824,repair,Synthetic minor repair record
|
||||
MNT-0025,MO-026,2026-03-09T09:00:00Z,34473,periodic_service,Synthetic scheduled service record
|
||||
MNT-0026,MO-029,2026-03-04T09:00:00Z,36912,periodic_service,Synthetic scheduled service record
|
||||
MNT-0027,MO-032,2026-02-27T09:00:00Z,38991,repair,Synthetic minor repair record
|
||||
MNT-0028,MO-035,2026-02-22T09:00:00Z,41234,periodic_service,Synthetic scheduled service record
|
||||
MNT-0029,MO-038,2026-02-17T09:00:00Z,43830,periodic_service,Synthetic scheduled service record
|
||||
MNT-0030,MO-041,2026-02-12T09:00:00Z,45643,repair,Synthetic minor repair record
|
||||
MNT-0031,MO-044,2026-02-07T09:00:00Z,47811,periodic_service,Synthetic scheduled service record
|
||||
MNT-0032,MO-047,2026-02-02T09:00:00Z,49652,periodic_service,Synthetic scheduled service record
|
||||
MNT-0033,MO-050,2026-01-28T09:00:00Z,51892,repair,Synthetic minor repair record
|
||||
MNT-0034,MO-003,2026-01-23T09:00:00Z,18258,periodic_service,Synthetic scheduled service record
|
||||
MNT-0035,MO-006,2026-01-18T09:00:00Z,20363,periodic_service,Synthetic scheduled service record
|
||||
MNT-0036,MO-009,2026-01-13T09:00:00Z,22403,repair,Synthetic minor repair record
|
||||
MNT-0037,MO-012,2026-01-08T09:00:00Z,24059,periodic_service,Synthetic scheduled service record
|
||||
MNT-0038,MO-015,2026-01-03T09:00:00Z,26861,periodic_service,Synthetic scheduled service record
|
||||
MNT-0039,MO-018,2025-12-29T09:00:00Z,28782,repair,Synthetic minor repair record
|
||||
MNT-0040,MO-021,2025-12-24T09:00:00Z,31298,periodic_service,Synthetic scheduled service record
|
||||
|
||||
|
Reference in New Issue
Block a user