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
(PROD-3) 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.
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 inferred money never mix. summary.total_amount_owed_cents and
summary.total_over_free_time_seconds cover evidenced and open visits only; the
excluded magnitude is reported as inferred_max_additional_cents /
inferred_max_additional_over_free_time_seconds, which are CEILINGS. Per row the
same split is amount_owed_cents (an upper bound when departure_inferred) versus
evidenced_amount_owed_cents (0 when inferred), so a CSV export can be summed
without silently billing a guess.
Request
Responses
- 200
- 400
- 401
- 403
- 404
- 422
- 500
OK
Bad Request
Unauthorized
Forbidden
Not Found
Validation Error
Internal Server Error