rental-workflow
Business logic and workflow for motorbike check-in and check-out processes, including damage documentation.
When & Why to Use This Skill
This Claude skill provides a comprehensive business logic framework and automated workflow for motorbike rental operations. It streamlines the entire lifecycle from check-in and deposit collection to damage documentation and final check-out processing, ensuring data consistency, operational transparency, and reduced human error in rental management.
Use Cases
- Automating the check-in process by integrating renter registration, real-time motorbike availability checks, and digital agreement signing into a single workflow.
- Streamlining check-out procedures with automated mileage calculation, insurance-adjusted damage cost estimation, and deposit refund processing.
- Implementing a standardized damage documentation system that utilizes before-and-after photo comparisons to minimize disputes and ensure accurate repair billing.
- Developing a guided 'Stepper' UI for rental staff to ensure all mandatory operational steps—such as insurance selection and deposit collection—are completed correctly.
| name | rental-workflow |
|---|---|
| description | Business logic and workflow for motorbike check-in and check-out processes, including damage documentation. |
Rental Workflow
Check-in and check-out business logic for MotoRent.
Workflow Overview
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Reserved │────>│ Active │────>│ Completed │ │ Cancelled │
│ │ │ │ │ │ │ │
│ (Booking) │ │ (Check-In) │ │ (Check-Out) │ │ (Cancel) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
Check-In Process
Steps
Select/Register Renter
- Search existing renters
- Or register new renter with OCR
Select Motorbike
- Show available bikes only
- Display daily rate, deposit
Choose Add-ons
- Insurance package (optional)
- Accessories (helmets, phone holders)
Collect Deposit
- Cash or card pre-authorization
- Record deposit details
Capture Before Photos
- Front, back, sides
- Document existing scratches/damage
Sign Agreement
- Display terms and conditions
- Capture digital signature
Confirm & Receipt
- Generate rental record
- Print/email receipt
Check-In Service
// Services/RentalService.cs
public class RentalService
{
private readonly RentalDataContext m_context;
public async Task<Rental> CheckInAsync(CheckInRequest request)
{
// Validate motorbike availability
var motorbike = await m_context.LoadOneAsync<Motorbike>(
m => m.MotorbikeId == request.MotorbikeId);
if (motorbike is null)
throw new ValidationException("Motorbike not found");
if (motorbike.Status != "Available")
throw new ValidationException("Motorbike is not available");
// Create rental
var rental = new Rental
{
ShopId = request.ShopId,
RenterId = request.RenterId,
MotorbikeId = request.MotorbikeId,
StartDate = DateTimeOffset.Now,
ExpectedEndDate = request.ExpectedEndDate,
MileageStart = motorbike.Mileage,
DailyRate = motorbike.DailyRate,
Status = "Active",
InsuranceId = request.InsuranceId
};
// Calculate total
var days = (rental.ExpectedEndDate - rental.StartDate).Days;
rental.TotalAmount = days * rental.DailyRate;
// Add insurance if selected
if (request.InsuranceId.HasValue)
{
var insurance = await m_context.LoadOneAsync<Insurance>(
i => i.InsuranceId == request.InsuranceId);
rental.TotalAmount += days * insurance!.DailyRate;
}
// Create deposit
var deposit = new Deposit
{
DepositType = request.DepositType,
Amount = request.DepositAmount,
Status = "Held",
CollectedOn = DateTimeOffset.Now,
CardLast4 = request.CardLast4,
TransactionRef = request.TransactionRef
};
// Update motorbike status
motorbike.Status = "Rented";
// Save all
using var session = m_context.OpenSession();
session.Attach(rental);
session.Attach(deposit);
session.Attach(motorbike);
await session.SubmitChanges("CheckIn");
// Link deposit to rental
rental.DepositId = deposit.DepositId;
session.Attach(rental);
await session.SubmitChanges("LinkDeposit");
return rental;
}
}
CheckInRequest Model
public class CheckInRequest
{
public int ShopId { get; set; }
public int RenterId { get; set; }
public int MotorbikeId { get; set; }
public DateTimeOffset ExpectedEndDate { get; set; }
public int? InsuranceId { get; set; }
public List<int> AccessoryIds { get; set; } = [];
// Deposit
public string DepositType { get; set; } = "Cash"; // Cash, CardPreAuth
public decimal DepositAmount { get; set; }
public string? CardLast4 { get; set; }
public string? TransactionRef { get; set; }
}
Check-Out Process
Steps
Find Active Rental
- Search by renter or motorbike
Record Return
- Enter end mileage
- Note return time
Capture After Photos
- Same angles as before
- Document any new damage
Damage Assessment
- Compare before/after photos
- Document any damage
- Estimate repair cost
Calculate Final Charges
- Rental days (actual)
- Extra days if overdue
- Damage charges
- Less insurance coverage
Process Payment
- Collect balance due
- Or refund overpayment
Refund Deposit
- Full if no damage
- Partial if damage deducted
Complete Rental
- Update status
- Generate receipt
Check-Out Service
public async Task<CheckOutResult> CheckOutAsync(CheckOutRequest request)
{
var rental = await m_context.LoadOneAsync<Rental>(
r => r.RentalId == request.RentalId);
if (rental is null)
throw new ValidationException("Rental not found");
if (rental.Status != "Active")
throw new ValidationException("Rental is not active");
var motorbike = await m_context.LoadOneAsync<Motorbike>(
m => m.MotorbikeId == rental.MotorbikeId);
// Calculate actual rental period
rental.ActualEndDate = DateTimeOffset.Now;
rental.MileageEnd = request.MileageEnd;
var actualDays = Math.Ceiling((rental.ActualEndDate.Value - rental.StartDate).TotalDays);
var rentalAmount = (decimal)actualDays * rental.DailyRate;
// Add insurance if applicable
decimal insuranceAmount = 0;
if (rental.InsuranceId.HasValue)
{
var insurance = await m_context.LoadOneAsync<Insurance>(
i => i.InsuranceId == rental.InsuranceId);
insuranceAmount = (decimal)actualDays * insurance!.DailyRate;
}
// Calculate damage charges
decimal damageAmount = request.DamageCharges;
// Calculate insurance coverage
decimal insuranceCoverage = 0;
if (rental.InsuranceId.HasValue && damageAmount > 0)
{
var insurance = await m_context.LoadOneAsync<Insurance>(
i => i.InsuranceId == rental.InsuranceId);
insuranceCoverage = Math.Min(damageAmount - insurance!.Deductible, insurance.MaxCoverage);
insuranceCoverage = Math.Max(0, insuranceCoverage);
}
// Final calculation
var totalCharges = rentalAmount + insuranceAmount + damageAmount - insuranceCoverage;
var deposit = await m_context.LoadOneAsync<Deposit>(d => d.DepositId == rental.DepositId);
var depositAmount = deposit?.Amount ?? 0;
var balanceDue = totalCharges - depositAmount;
// Update entities
rental.TotalAmount = totalCharges;
rental.Status = "Completed";
motorbike!.Status = "Available";
motorbike.Mileage = request.MileageEnd;
if (deposit is not null)
{
deposit.Status = damageAmount > 0 ? "Forfeited" : "Refunded";
deposit.RefundedOn = DateTimeOffset.Now;
}
// Save
using var session = m_context.OpenSession();
session.Attach(rental);
session.Attach(motorbike);
if (deposit is not null)
session.Attach(deposit);
await session.SubmitChanges("CheckOut");
return new CheckOutResult
{
RentalDays = (int)actualDays,
RentalAmount = rentalAmount,
InsuranceAmount = insuranceAmount,
DamageAmount = damageAmount,
InsuranceCoverage = insuranceCoverage,
TotalCharges = totalCharges,
DepositAmount = depositAmount,
BalanceDue = balanceDue
};
}
Damage Documentation
public async Task<DamageReport> RecordDamageAsync(DamageRequest request)
{
var damage = new DamageReport
{
RentalId = request.RentalId,
MotorbikeId = request.MotorbikeId,
Description = request.Description,
Severity = request.Severity, // Minor, Moderate, Major
EstimatedCost = request.EstimatedCost,
Status = "Pending",
ReportedOn = DateTimeOffset.Now
};
using var session = m_context.OpenSession();
session.Attach(damage);
await session.SubmitChanges("RecordDamage");
// Save photos
foreach (var photo in request.Photos)
{
var damagePhoto = new DamagePhoto
{
DamageReportId = damage.DamageReportId,
PhotoType = photo.Type, // Before, After
ImagePath = await SavePhotoAsync(photo.Stream),
CapturedOn = DateTimeOffset.Now
};
session.Attach(damagePhoto);
}
await session.SubmitChanges("SaveDamagePhotos");
return damage;
}
Stepper UI Pattern
<MudStepper @ref="m_stepper" Linear="true">
<MudStep Title="Select Renter" Icon="@Icons.Material.Filled.Person">
<RenterSelector @bind-SelectedRenterId="m_renterId" />
</MudStep>
<MudStep Title="Select Bike" Icon="@Icons.Material.Filled.TwoWheeler">
<MotorbikeSelector @bind-SelectedMotorbikeId="m_motorbikeId" ShopId="m_shopId" />
</MudStep>
<MudStep Title="Add-ons" Icon="@Icons.Material.Filled.AddCircle">
<InsuranceSelector @bind-SelectedInsuranceId="m_insuranceId" />
<AccessorySelector @bind-SelectedAccessoryIds="m_accessoryIds" />
</MudStep>
<MudStep Title="Deposit" Icon="@Icons.Material.Filled.Payment">
<DepositForm @bind-DepositType="m_depositType" @bind-Amount="m_depositAmount" />
</MudStep>
<MudStep Title="Photos" Icon="@Icons.Material.Filled.CameraAlt">
<BeforePhotoCapture MotorbikeId="m_motorbikeId" @bind-Photos="m_beforePhotos" />
</MudStep>
<MudStep Title="Agreement" Icon="@Icons.Material.Filled.Description">
<AgreementSignature ShopId="m_shopId" @bind-SignaturePath="m_signaturePath" />
</MudStep>
<MudStep Title="Confirm" Icon="@Icons.Material.Filled.Check">
<CheckInSummary Request="BuildRequest()" />
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="CompleteCheckIn">
Complete Check-In
</MudButton>
</MudStep>
</MudStepper>
Source
- Business logic from: MotoRent requirements