Permit-Condition Reports
Most city permits include enforceable conditions — fleet cap, equity-zone deployment percentage, complaint SLA, end-of-day trip report, parking corral utilization. Levy stores each as a row in permit_conditions, evaluates them on a schedule, and renders the pass/fail trend in both the operator dashboard and the city portal.
The five condition types
condition_type | What it checks | Cadence |
|---|---|---|
fleet_cap | COUNT(active vehicles in jurisdiction) <= cap | Daily |
equity_zone_pct | % of deployed fleet inside equity geographies >= threshold | Hourly |
complaint_sla | Each complaint's responded_at within N hours of created_at | Daily |
trip_report_eod | A trip CSV is generated daily at 23:59 jurisdiction-local time | Daily |
corral_utilization | Hourly snapshot of parking corral occupancy | Hourly |
Each row in permit_conditions carries the jurisdiction_id, condition_type, and a config JSONB that varies by type.
Configuring a condition
From the operator dashboard at /dashboard/compliance/{jurisdiction-id} -> Permit conditions tab, click Add condition.
fleet_cap
{
"cap": 250,
"vehicle_types": ["scooter"],
"exclude_statuses": ["maintenance", "removed"]
}
The evaluator counts vehicles where:
subaccount_idmatches the jurisdiction's subaccountlast_lat/last_lngis insidemds_jurisdictions.geometry(or NULL withlast_seenin the last 24h — generous to vehicles that just lost GPS)status NOT IN config.exclude_statusesmodel.vehicle_type IN config.vehicle_types(if set)
Pass = count <= cap. Fail = count > cap. The dashboard scoreboard shows the current count and the cap side by side.
equity_zone_pct
{
"threshold_pct": 20,
"equity_geography_ids": ["geo-1", "geo-2"],
"min_vehicles_required": 5
}
Pass = deployed-in-equity / total-deployed >= threshold_pct / 100. The min_vehicles_required is a floor — if there are fewer than that deployed at all, the condition is marked n/a for the hour rather than failed (sparse early-morning deployments).
Equity geographies come from either the city's Policy feed (rules with rule_type = 'equity_zone') or a manually configured list (when the city doesn't publish them — common with smaller cities). The manual list lives in the same permit_conditions.config.
complaint_sla
{
"max_response_hours": 24,
"complaint_sources": ["311", "in-app", "email"]
}
The evaluator joins complaints to find rows where responded_at IS NULL AND created_at < now() - interval '<hours>' (open and overdue) or responded_at - created_at > interval '<hours>' (closed but late). Pass = zero late complaints in the window.
trip_report_eod
{
"format": "csv",
"delivery": "email",
"recipient_email": "permits@city.gov"
}
A scheduled report runs at 23:59 jurisdiction-local time and emits a trip CSV for the day. Pass = the report was successfully delivered. Fail = email bounced or the report row didn't materialize.
corral_utilization
{
"min_utilization_pct": 10,
"max_utilization_pct": 90,
"exclude_corral_ids": []
}
Hourly snapshot of each parking corral's occupancy. Pass = all corrals between the configured floor and ceiling. Fail = at least one corral outside the band. The condition is used by cities that want to ensure parking is actually being used (low end) and that operators aren't over-concentrating (high end).
The scoreboard
Operators see the scoreboard at /dashboard/compliance/{jurisdiction-id}. Cities see it inside the city portal at /city/{slug}. The shape is the same:
| Condition | Current value | Threshold | Status (today) | 30-day pass rate |
|---|---|---|---|---|
| Fleet cap | 247 vehicles | 250 | Pass | 100% |
| Equity zone % | 18% | >= 20% | Fail | 88% |
| Complaint SLA | 2 open over 24h | 0 | Fail | 92% |
| Trip report EOD | Delivered | n/a | Pass | 100% |
| Corral utilization | 6 of 6 in band | All in band | Pass | 95% |
Failing conditions are highlighted in red. The 30-day pass rate is computed from the city_compliance_reports history.
The reporter library
src/lib/compliance/permit-reporter.ts exposes per-condition evaluators. Each takes the jurisdiction + the relevant permit_conditions.config and returns:
type ConditionResult = {
condition_id: string;
passed: boolean;
current_value: number | string;
threshold: number | string;
measured_at: string; // RFC3339
details: Record<string, unknown>;
};
The aggregate report at digest time:
const report = await buildComplianceReport({
jurisdictionId,
period: 'daily', // or 'weekly' / 'monthly'
date: '2026-05-18',
});
// report.conditions: ConditionResult[]
// report.summary.passed: number
// report.summary.failed: number
// report.trips: { count, total_distance_km, ... }
// report.complaints: { open, closed, late, ... }
// report.enforcement_events: { speed_limits_applied, locks_issued, ... }
This shape is what the digest email renders, what the compliance-report API serves, and what gets persisted to city_compliance_reports.payload.
The compliance-report API
City contacts pull formal reports from:
GET /api/city/{slug}/compliance-report?period=monthly&date=2026-05
Returns the same JSON shape as the digest plus a pdf_url when period=monthly (we render a one-page PDF for monthly reports — useful for permit renewals).
Every fetched report is persisted to city_compliance_reports — the audit trail of every report the city has actually retrieved. If a city auditor asks "did you send a report for January?", the row in city_compliance_reports is the answer.
When conditions fail
A failing condition does not auto-pause your fleet. It surfaces:
- A red row on the scoreboard
- A line in today's digest email
- A Sentry breadcrumb tagged
compliance.condition.failed
What you do about it depends on the condition. For fleet_cap overage, deploy fewer vehicles. For equity_zone_pct, re-distribute. For complaint_sla, respond to the open complaints. The system doesn't make operational decisions for you.
The exception is fleet_cap, where the dashboard offers a "Pause new ride starts in jurisdiction" toggle. This is a soft constraint — existing rides continue, no new rides start in the jurisdiction until the cap is back under threshold. The toggle is operator-controlled; we don't activate it automatically because false-positive auto-pauses would be much worse than a transient fleet_cap fail.
Custom conditions
If your permit includes a condition that doesn't fit the five built-in types (e.g., "must achieve 90% multilingual signage"), add a row with condition_type = 'custom' and a manual passed field. The evaluator returns whatever you set; cities still see it on the scoreboard. This is the escape hatch for permits that have one weird clause.
What's next
- Digest Emails and Cadence — when these results land in the city's inbox.
- City Portal & Magic-Link Auth — where the city contact views the scoreboard.
Running a fleet on this hardware?
Levy Fleets runs scooters, e-bikes, mopeds and golf carts on the IoT hardware you already have, with integration pages for 29 vendors including Segway, Okai, Omni, Queclink, Invers and Comodule. Keyless unlock, geofencing, rider payments and rider support are included, and the Managed plan is a share of revenue with no per-vehicle software fee.