When a customer keeps a bike past their scheduled return time, the late-fee system flags the booking, computes a fee, and lets you apply it. We deliberately don't auto-charge — chargebacks come from surprised customers, and a human-in-the-loop step prevents them.
How detection works
A cron job runs every 15 minutes at
/api/cron/reservation-late-returns. It looks for reservations where:
statusisactiveorchecked_inreturn_at < now()(scheduled return is in the past)actual_return_at IS NULL(bike is still out)
For each match, it reads the pricing snapshot for that booking and calculates:
grace_minutes = pricing_snapshot.late_return_grace_minutes (default 60)
late_rate = pricing_snapshot.late_return_hourly_rate_cents (default 0)
late_by_ms = now() - return_at
grace_ms = grace_minutes * 60 * 1000
if late_by_ms < grace_ms:
skip
else:
late_hours = ceil((late_by_ms - grace_ms) / 1 hour)
fee_cents = late_hours * late_rate
set is_late=true, late_fee_cents=fee_cents
So a booking that returned 10 minutes late doesn't get flagged. A booking 2 hours late with a 60-minute grace and $15/hr rate gets flagged with $15 fee (1 rounded-up hour past grace).
What the operator sees
The booking detail page shows an amber Late return banner above the financial summary:
⚠ Late return
This rental is past the scheduled return time.
Computed late fee: $30.00
[Apply late fee]
The banner only appears when both is_late = true and
late_fee_cents > 0.
Applying the late fee
Click Apply late fee on the banner. The endpoint:
- Adds
late_fee_centstoadjustment_cents(line item on financial summary) - Adds it to
total_centsandbalance_due_cents - Sets
adjustment_reason = "Late return fee"(or with a custom suffix) - Resets
late_fee_cents = 0andis_late = false
The banner disappears. The booking's financial section now shows the adjustment line and an updated balance due.
What the late fee doesn't do
- Doesn't charge the card automatically. The fee is recorded as a balance the customer owes.
- Doesn't email the customer. No "you've been charged $30 in late fees" email is sent. Operator should follow up if needed.
- Doesn't release the deposit. The deposit hold remains until return is completed; it can absorb the late fee or be captured then released minus the fee.
Collecting the fee
Three paths:
- Capture from the deposit at return time — the deposit hold can absorb the fee. Use the existing capture flow.
- Charge the saved card off-session via the admin charge-card endpoint or wallet-debit pattern.
- Wait for the customer to pay manually — they see the balance due on their manage-link page.
For most shops, option 1 (capture from deposit) is cleanest.
Editing the fee before applying
If the auto-computed fee feels wrong (customer had a legitimate reason to be late), you can:
- Apply the fee then partial-refund it back as a goodwill credit
- Override the late_fee_cents value via SQL before clicking Apply (rarely needed)
- Skip applying entirely — the late_fee just sits unflagged once return is completed
The endpoint accepts an optional amountCents parameter to override the
computed fee:
POST /api/reservations/[id]/charge-late-fee
{ "amountCents": 1500, "reason": "Customer late due to weather" }
Recurring late behavior
The cron re-evaluates every 15 minutes, so if you ignore the banner the late fee continues to accrue. If the customer is 5 hours late, you'll see the fee climb each cron tick.
If you don't want this — the customer is just dropping the bike off
soon and you'll handle it personally — you can clear is_late manually
or wait for the operator to mark the return complete.
Reporting
SELECT
date_trunc('week', applied_at) AS week,
count(*) AS late_returns_charged,
sum(amount_cents) / 100.0 AS late_fee_revenue
FROM (
SELECT
cancelled_at AS applied_at,
adjustment_cents AS amount_cents
FROM reservations
WHERE adjustment_reason LIKE 'Late return fee%'
AND adjustment_cents > 0
) t
GROUP BY 1
ORDER BY 1 DESC;
(This is approximate — for clean reporting, consider a separate audit
table for fee applications. Currently the adjustment is folded into
adjustment_cents.)
Common scenarios
Customer claims they returned on time
Check actual_return_at on the booking. If it's stamped before
return_at, no late fee applies (the cron wouldn't have flagged it).
If they say "I dropped the bike off but no one was at the counter,"
they're technically still active until you press Complete return —
the cron sees the bike as "still out." Investigate via security
camera, then either back-date actual_return_at or stand by the fee.
Customer is 90 minutes late but well-meaning
Two options:
- Apply the fee then immediately partial-refund the same amount as goodwill (preserves audit trail)
- Don't apply the fee at all (just let it sit)
Recurring bad actor
Pull a report of all customers with multiple late returns:
SELECT customer_uuid, count(*)
FROM reservations
WHERE adjustment_reason LIKE 'Late return fee%'
GROUP BY 1
HAVING count(*) > 2;
Consider tightening the deposit amount or refusing future bookings.