Skip to main content

Detention report — one row per facility visit with billable overage

GET 

/api/v1/reports/detention

Per-VISIT detention rows: arrival, departure, time on site, billable overage.

A carrier bills detention when a truck waits at a shipper or receiver beyond the contracted free window. The claim needs a timestamped arrival and departure for a single visit, so this report emits exactly one row per visit — a visit that spans midnight, or several days, stays ONE row. (/reports/time-on-site buckets by (device, geofence, local day) instead, which is the right shape for job-site utilisation and the wrong shape for a claim.)

Visits come from GeofenceEvent entry/exit pairing (_pair_geofence_visits), which scans SYMMETRICALLY past both ends of the window: it seeds a visit that was already open when the window began, and resolves a visit that closes after the window ends. Both matter because a truck that arrives at 18:00 and leaves at 03:00 is one claim, and clipping either end understates the invoice. Dwell SignalEvents are deliberately NOT used here: a dwell signal has no departure event, and a claim without a departure timestamp is not payable.

NOT clipped to shift sessions. report_time_on_site applies _clip_to_shifts because a van parked at a job site after the crew clocks out is not working time. Detention is the exact opposite: a truck still sitting at a receiver after the driver's shift ends is precisely the time being billed, and clipping it would delete the claim. Do not "consistency-fix" this to match time-on-site.

A visit with no exit event splits into two distinct states. If a later re-entry proves the truck left, the row is departure_inferred=true (NOT open) with seconds_on_site bounded at that re-entry. Otherwise it is is_open=true with seconds_on_site measured to the earlier of now and the range end — never a silent whole-day figure. Both keep exited_at=null, because there is no departure event to cite. summary.open_visit_count therefore counts only trucks still on site; inferred departures are counted separately.

An open visit is only as good as the device behind it, and only while that device had CONTINUOUS contact. A silence longer than settings.DETENTION_STALE_CONTACT_HOURS anywhere inside the visit ends the evidence there and flags the row contact_lost=true. Contact comes from DeviceSession.last_contact_at (see _contact_intervals) — never from Device.last_location_time, which a reconnect refreshes, and never from DeviceLocation rows, which the distance filter skips for a parked truck. Those rows are counted and priced in their own bucket (contact_lost_visit_count / contact_lost_unresolved_cents) and are excluded from open_visit_count and from the owed total.

A visit that outlives its shift therefore stops being evidenced once the shift's telemetry does. That is not a clip to shift windows — the dwell keeps running and the money is still reported — but the platform cannot claim a truck was on site during hours it was not permitted to hear from the device (the device location routes reject off-shift fixes), so that portion sits in the unresolved bucket with its floor amount rather than in the claim total.

Money is integer cents end to end. over_free_time_seconds is max(0, seconds_on_site - free_time_minutes * 60); amount_owed_cents applies rate_per_hour_cents to it under the rounding rule in _detention_amount_cents. The default rate of 0 yields 0 owed, so an unpriced report shows no dollar figure rather than a fabricated one.

Evidenced money and unresolved money never mix. summary.total_amount_owed_cents and summary.total_over_free_time_seconds cover evidenced and LIVE open visits only. The two excluded magnitudes point in OPPOSITE directions and must not be described alike: inferred_max_additional_cents is a CEILING (the truck may have left right after arriving), while contact_lost_unresolved_cents is a FLOOR (the vehicle was there at least that long and may have stayed far longer — the stop is unresolved, not cheap). Per row the split is amount_owed_cents versus evidenced_amount_owed_cents (0 in both cases), so a CSV export can be summed without silently billing a guess.

Request​

Responses​

OK