intermediate
shop-rentals
damage
return

Damage at Return

Logging incidents when a bike comes back damaged — atomic complete-return + service-log + reservation total adjustment

Levy Fleets TeamMay 7, 20265 min read

When a customer returns a bike with damage beyond normal wear and tear, you log the incident, attach photos, and (optionally) charge them for the repair — all in one atomic operation.

What it does

A single API call:

  1. Stamps actual_return_at and transitions the reservation to completed
  2. Inserts a vehicle_service_log row with event_type='damage'
  3. Adjusts the reservation total upward by the damage charge
  4. Marks the bike as needing repair (optional, depends on follow-up)

When to use it instead of "Complete return"

The plain Complete return button is for clean returns — bike came back fine, no follow-up. Use the damage flow when:

  • Visible damage (scratches, dents, missing parts, broken light)
  • Bike returned dirty enough to require professional cleaning
  • Tire is flat (might be normal puncture, but log it)
  • Customer admitted to a crash even if no visible damage

If in doubt, log a damage event with damageChargeCents=0 — you can always not charge the customer, but you can't retroactively log an event you didn't capture.

API endpoint

POST /api/reservations/[id]/return
{
  "conditionOk": false,
  "damageDescription": "Bent front fender, scratched paint on top tube",
  "damageChargeCents": 5000,
  "photoUrls": [
    "https://your-photo-host/photo1.jpg",
    "https://your-photo-host/photo2.jpg"
  ],
  "odometerKm": 12.3
}

If conditionOk: true, the endpoint just transitions to completed without creating a service log row. If false, the rest of the body is processed.

What gets written

reservations table

status = 'completed'
actual_return_at = now()
adjustment_cents = adjustment_cents + damageChargeCents
adjustment_reason = "Damage at return: {damageDescription}"
total_cents = total_cents + damageChargeCents
balance_due_cents = max(0, total_cents - amount_paid_cents)

So if the customer's deposit covered $50 and the damage is $50, the balance_due stays at $0. If damage is $80, the customer owes $30 more.

vehicle_service_log table

vehicle_uuid: the assigned vehicle
event_type: 'damage'
description: damageDescription (or default fallback)
damage_charge_cents: damageChargeCents
photo_urls: array of URLs
reservation_uuid: this reservation
occurred_at: now()

This row lives forever — it's the audit trail for insurance claims, disputes, or future maintenance scheduling.

Capturing the damage charge from the deposit

Damage logging marks the reservation as having a balance. To actually collect:

  • From the deposit hold — if the deposit is still held (not released), capture the held amount. This is part of the (planned) deposit capture/release flow at return time.
  • From the saved card — the existing payment method on file can be charged via the standard charge flow.
  • Via wallet debit — if your customer has a wallet balance.

For now, the damage flow records the charge but doesn't auto-collect. The operator follows up with the customer or captures from the deposit manually.

Photo handling

photo_urls accepts an array of HTTPS URLs. We don't yet have a built-in upload flow — most operators:

  • Take photos on the iPad
  • Upload to a folder in their existing cloud storage (Google Drive, Dropbox)
  • Share-link those and paste the URLs into the API call

A native upload-to-Supabase-Storage flow for damage photos is on the roadmap.

What happens to the bike afterward

The damage event doesn't automatically take the bike out of service. If repairs are needed, separately mark it out of service from the vehicle service log section on the vehicle detail page. This way you can keep renting the bike if the damage is cosmetic.

Reporting / audit

To pull all damage events for a date range or a vehicle:

SELECT
  vsl.occurred_at,
  v.vehicle_number,
  vsl.description,
  vsl.damage_charge_cents,
  r.reservation_number,
  c.full_name
FROM vehicle_service_log vsl
JOIN vehicles v ON v.id = vsl.vehicle_uuid
LEFT JOIN reservations r ON r.id = vsl.reservation_uuid
LEFT JOIN customers c ON c.id = r.customer_uuid
WHERE vsl.event_type = 'damage'
  AND vsl.occurred_at BETWEEN '2026-01-01' AND '2026-12-31'
ORDER BY vsl.occurred_at DESC;

For frequent damage on a specific vehicle (sign of needing replacement), sum by vehicle_uuid.

Common scenarios

Customer disputes the charge

You have:

  • The signed waiver acknowledging damage policy
  • Time-stamped photos of the damage
  • The service log entry with description and the operator who logged it
  • The original walk-in and check-in events

This is typically enough for a Stripe chargeback dispute.

Bike comes back with no damage but is dirty

Log a cleaning event (use event_type='maintenance' and description="Required deep clean post-rental"). Most shops don't charge the customer unless it's egregious.

Multiple damage events on one return

You can call the endpoint once with all damage in the description, OR submit one call to mark complete + first damage, then add additional service log entries via the service log endpoint. The first approach is cleaner for the audit trail.

Damage discovered after return is closed

Log a service log event without a reservation_uuid (or with the reservation if you want the link, even after completion). Then issue a post-hoc charge to the customer's saved card via the existing charge flow. This is messy — try to catch damage at return time.