Picture a dispatcher in Dallas on a busy morning. Five fuel orders are open, three trucks are on the road, and the delivery windows close at different times. Two trucks carry diesel, one carries regular, and an order in Fort Worth is due before anyone can drive there.
This is the core problem behind most fuel delivery app development services: decide which truck takes which order, and be able to explain the decision afterwards. This post builds a small version in Python so you can run it, break it and extend it. It is a teaching sketch, not a production router.
The rules come before the math
A fuel order is not a parcel. Before any distance is calculated, a truck must pass three hard checks:
- It carries the right fuel.
- It has enough fuel left on board.
- It can arrive before the delivery window closes.
Only trucks that pass all three get a score. This order matters, because a fast truck with the wrong fuel is not a good answer.
The data model
from dataclasses import dataclass
from math import radians, sin, cos, asin, sqrt
import hashlib
import json
AVG_MPH = 30 # assumption for mixed DFW roads; replace with learned ETAs later
SERVICE_MIN = 10 # minutes spent at each stop
@dataclass(frozen=True)
class Order:
id: str
lat: float
lon: float
fuel: str
gallons: int
due_min: int # minutes from now until the delivery window closes
@dataclass
class Truck:
id: str
lat: float
lon: float
fuel: str
gallons_left: int
busy_min: float = 0.0 # minutes until the truck is free
Distance, ETA and score
def miles(lat1, lon1, lat2, lon2):
"""Great-circle distance (haversine) in miles."""
p1, p2 = radians(lat1), radians(lat2)
dlat, dlon = p2 - p1, radians(lon2 - lon1)
a = sin(dlat / 2) ** 2 + cos(p1) * cos(p2) * sin(dlon / 2) ** 2
return 2 * 3958.8 * asin(sqrt(a))
def eta_min(truck, order):
drive = miles(truck.lat, truck.lon, order.lat, order.lon) / AVG_MPH * 60
return truck.busy_min + drive
def score(truck, order):
"""Lower is better. None means this truck cannot take the order."""
if truck.fuel != order.fuel or truck.gallons_left < order.gallons:
return None
eta = eta_min(truck, order)
if eta > order.due_min:
return None
# Tie-breaker: do not burn a nearly full truck on a small order.
return eta + 0.01 * (truck.gallons_left - order.gallons)
Notice that score returns None instead of a very large number. A missing score is a clear "no", and it keeps impossible assignments out of the results.
Greedy assignment
def assign(orders, trucks):
plan, unassigned = [], []
for order in sorted(orders, key=lambda o: o.due_min):
scored = [(score(t, order), t) for t in trucks]
scored = [(s, t) for s, t in scored if s is not None]
if not scored:
unassigned.append(order.id)
continue
_, best = min(scored, key=lambda x: x[0])
eta = eta_min(best, order)
plan.append((order.id, best.id, round(eta)))
best.lat, best.lon = order.lat, order.lon
best.busy_min = eta + SERVICE_MIN
best.gallons_left -= order.gallons
return plan, unassigned
Orders are handled from the tightest window to the loosest. After each assignment the truck "moves" to the order location, is busy until the stop is finished, and has less fuel left.
Running it on a Dallas-Fort Worth morning
trucks = [
Truck("T1", 32.7767, -96.7970, "diesel", 800), # Dallas depot
Truck("T2", 32.8140, -96.9489, "diesel", 500), # Irving
Truck("T3", 32.7357, -97.1081, "regular", 600), # Arlington
]
orders = [
Order("O1", 33.0198, -96.6989, "diesel", 300, 90), # Plano
Order("O2", 32.9126, -96.6389, "diesel", 200, 45), # Garland
Order("O3", 32.8998, -97.0403, "regular", 150, 60), # DFW Airport area
Order("O4", 32.7555, -97.3308, "diesel", 450, 40), # Fort Worth
Order("O5", 33.1507, -96.8236, "regular", 100, 120), # Frisco
]
plan, left = assign(orders, trucks)
for row in plan:
print("order %s -> truck %s, ETA %d min" % row)
print("unassigned:", left)
Output:
order O2 -> truck T1, ETA 26 min
order O3 -> truck T3, ETA 24 min
order O1 -> truck T2, ETA 41 min
order O5 -> truck T3, ETA 77 min
unassigned: ['O4']
Order O4 in Fort Worth cannot be served in time by any diesel truck, so the engine does not guess. It returns the order as unassigned, and a human can call the customer, widen the window or send a different truck. In a real product this list is one of the most useful screens you can build.
Where AI smart dispatch fits
The greedy rule above is easy to read, but it makes two weak assumptions: a flat 30 mph speed and one order at a time. A common next step is an AI smart dispatch layer that learns travel times from your own GPS history and predicts the ETA for each truck, order and hour of the day. Keep the three hard checks as guardrails around whatever model you add, so a prediction can never send diesel to a regular order. For larger fleets with many stops per route, a vehicle routing solver such as Google OR-Tools is a better base than a greedy loop.
A tamper-evident audit log
Every assignment should leave a record that is hard to edit quietly. A hash chain is a small way to get that: each entry stores the hash of the previous entry, so changing one old row breaks everything after it.
def append_audit(log, event):
prev = log[-1]["hash"] if log else "0" * 64
body = json.dumps(event, sort_keys=True)
digest = hashlib.sha256((prev + body).encode()).hexdigest()
log.append({"event": event, "prev": prev, "hash": digest})
def verify_audit(log):
prev = "0" * 64
for entry in log:
body = json.dumps(entry["event"], sort_keys=True)
if entry["prev"] != prev:
return False
if hashlib.sha256((prev + body).encode()).hexdigest() != entry["hash"]:
return False
prev = entry["hash"]
return True
log = []
for order_id, truck_id, eta in plan:
append_audit(log, {"action": "assign", "order": order_id, "truck": truck_id, "eta_min": eta})
print("audit valid:", verify_audit(log))
log[0]["event"]["truck"] = "T9"
print("audit valid after edit:", verify_audit(log))
audit valid: True
audit valid after edit: False
This is not a replacement for write-once storage or database permissions, but it makes silent edits easy to detect.
Who may do what
Dispatch actions should never depend on a button being hidden in the interface. In a typical setup, an API gateway validates the OAuth 2.0 access token (a JWT), and the service then checks the roles inside the validated claims:
PERMISSIONS = {
"dispatcher": {"assign", "reassign", "read_orders"},
"driver": {"read_own_orders", "complete_delivery"},
"admin": {"assign", "reassign", "read_orders", "export_audit"},
}
def can(claims, action):
return any(action in PERMISSIONS.get(r, set()) for r in claims.get("roles", []))
claims = {"sub": "u-102", "roles": ["dispatcher"]}
print("dispatcher can assign:", can(claims, "assign"))
print("dispatcher can export_audit:", can(claims, "export_audit"))
dispatcher can assign: True
dispatcher can export_audit: False
The can function trusts the claims it receives, so token validation (signature, expiry, audience) must happen before it runs.
Limits of this sketch
- Greedy assignment is fast but not optimal; a different order of orders can give a better plan.
- Straight-line distance ignores roads, tolls and traffic.
- It models one fuel grade per truck and no compartments.
- There is no persistence, retry logic or live location feed.
If you are choosing a partner instead of building
Teams that would rather not build this from scratch often look at fuel delivery app development services that cover dispatch, tracking, driver and customer apps and support after launch. Whether the app is custom fuel delivery app development for one fleet or B2B fuel delivery app development for several accounts, the questions are the same: how are assignments decided, how are they audited, and who can change them? Good fuel delivery app development services can answer all three in plain language and show them in a demo.
Disclosure: this article was created with the help of AI. The code was run before publishing and the outputs above are the real results. It was written by the Dev Technosys team, a software development company that builds on-demand, logistics and fuel delivery apps for clients globally.
Top comments (0)