Imagine: a warehouse with 500+ units of equipment, managers spending hours on cross-booking, and customers complaining about duplicates. Classic Excel-based accounting yields 30% errors in inventory. We developed a turnkey mobile app that eliminates these issues: QR inventory, real-time automatic booking, and photo documentation of condition. The system is built on Swift 5.9+ for iOS, Kotlin Jetpack Compose for Android, and Python FastAPI on the server. Data syncs via GraphQL and Firebase Cloud Messaging. Rental management becomes transparent: every operation is recorded, customers see real equipment availability. Through automation, the company saves up to 2 million rubles annually with a fleet of 500+ units. Our accumulated experience has allowed us to launch 12 similar solutions for warehouses ranging from 100 to 2000 units. Average ROI is 4 months due to reduced downtime and conflict elimination. We will estimate your project in 2 days. Contact us to learn how to speed up checkout by 5 times.
What problems does the app solve?
Equipment loss and accounting errors
Without a system, each unit requires manual entry. Our experience shows that implementing QR inventory reduces losses by 95%. According to statistics, 60% of return disputes are resolved thanks to photo documentation.
Double bookings and lost orders
Booking conflicts are a common headache. Automatic availability checking with real-time date locking eliminates overlaps. Server-side optimistic locking provides instant response: 200 OK or 409 Conflict.
Disputes upon equipment return
Photo documentation of the condition before and after rental is ironclad proof of completeness. The app compresses images while preserving EXIF and attaches them to the contract.
Role of photo documentation at checkout
Before checkout, the operator photographs the equipment. Photos are attached to the contract as evidence of condition. This is critical for resolving disputes upon return.
// Android: CameraX for condition photo documentation
private fun takeHandoverPhoto() {
val imageCapture = imageCapture ?: return
val outputOptions = ImageCapture.OutputFileOptions.Builder(
createTempFile("handover_${System.currentTimeMillis()}", ".jpg")
).build()
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val photoFile = output.savedUri?.toFile() ?: return
viewModel.addHandoverPhoto(photoFile, currentRentalId)
}
override fun onError(exception: ImageCaptureException) {
showError("Capture error: ${exception.message}")
}
}
)
}
Photos are compressed to 1200px on the long side before uploading to the server—the original 12MP 4 MB shot is excessive for this purpose.
How does QR inventory work?
Each piece of equipment receives a unique identifier—a QR or barcode sticker affixed to the body. The operator scans it for checkout and return:
// iOS: Fast scan using AVCaptureSession
class BarcodeScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
func setupCapture() {
let session = AVCaptureSession()
guard let device = AVCaptureDevice.default(for: .video),
let input = try? AVCaptureDeviceInput(device: device) else { return }
session.addInput(input)
let output = AVCaptureMetadataOutput()
session.addOutput(output)
output.setMetadataObjectsDelegate(self, queue: .main)
output.metadataObjectTypes = [.qr, .code128, .ean13]
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.frame = view.bounds
view.layer.addSublayer(previewLayer)
session.startRunning()
}
func metadataOutput(
_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection
) {
guard let readableCode = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
let assetId = readableCode.stringValue else { return }
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
loadEquipmentDetails(assetId: assetId)
}
}
After scanning, the app displays the equipment card: name, serial number, current status (AVAILABLE / RENTED / MAINTENANCE), rental history.
Comparison: manual entry vs QR scanning
| Criterion | Manual entry | QR scanning |
|---|---|---|
| Time per unit | 15–30 sec | 1–2 sec |
| Input errors | 5–10% | <0.1% |
| Operator training | 1 hour | 5 minutes |
QR scanning is 15 times faster than manual entry—this speeds up checkout by 5 times.
How to prevent double booking?
The customer app shows an availability calendar. Overlaps are not allowed. The checking logic is on the server, but the UI blocks occupied dates:
// iOS: DatePicker with unavailable dates
func configureDatePicker(unavailableDates: [DateInterval]) {
datePicker.minimumDate = Date()
// For more flexible control, we use a custom calendar component
// with blocking of specific ranges
calendarView.disabledDateRanges = unavailableDates.map {
CalendarDateRange(start: $0.start, end: $0.end)
}
}
In case of concurrent booking (two users trying to book simultaneously), server-side optimistic locking: the first gets confirmation, the second gets a 409 Conflict error with a suggestion to choose other dates.
How is rental cost calculated?
Tariffs can be hourly, daily, or combined. Server-side calculation:
def calculate_rental_cost(
equipment: Equipment,
start_date: datetime,
end_date: datetime
) -> Decimal:
duration = end_date - start_date
hours = duration.total_seconds() / 3600
if hours <= 4:
return equipment.hourly_rate * Decimal(str(hours))
elif hours <= 24:
return equipment.daily_rate # daily rate is more advantageous
else:
days = math.ceil(hours / 24)
return equipment.daily_rate * Decimal(str(days))
The app shows the calculation in real time as dates change—via debounced API request or local calculation with the same rules.
Deposit and collateral
For expensive equipment, a deposit is required. The scheme: upon booking, the amount is blocked on the card (authorization hold); upon return in good condition, it is unblocked (void); in case of damage, it is charged (capture).
// Stripe: authorization-only payment intent
let params = STPPaymentIntentParams(clientSecret: clientSecret)
params.captureMethod = .manual // capture later if needed
STPPaymentHandler.shared().confirmPayment(params, with: self) { status, intent, error in
// intent.id is saved—needed for capture or void upon return
}
Process of work
- Analytics—study business processes, agree on prototype.
- Design—UX/UI design, database architecture.
- Development—iOS (Swift 5.9+, SwiftUI), Android (Kotlin, Jetpack Compose), server (Python/FastAPI).
- Testing—unit tests, integration with App Store Connect and Google Play Console.
- Deployment—publishing to stores, setting up push notifications (APNs/FCM).
Typical stage durations
| Stage | Duration |
|---|---|
| Analytics and prototype | 1–2 weeks |
| Design (UX/UI) | 2–3 weeks |
| Mobile app development (iOS + Android) | 4–6 weeks |
| Backend and integration | 2–3 weeks |
| Testing and deployment | 1–2 weeks |
What's included in the work
- Source code and API documentation.
- Access to backend and admin panel.
- Operator training (2 hours).
- Technical support for 30 days after launch.
- Assistance in passing App Store Review Guidelines (Section 4.2/5.1).
Typical implementation mistakes: neglecting role-based access rights setup (operator vs admin), lack of testing under weak signal—critical for QR scanning, ignoring personal data storage requirements (GDPR/152-FZ). Our experience helps avoid these—we incorporate correct configurations at the design stage.
Timeframe estimates
Basic version (catalog, booking, QR inventory, payment): 5–7 weeks. Operator module with photo documentation and contracts: another 2–3 weeks. Cost is calculated individually, but savings from implementation reach 2 million rubles per year for an average warehouse.
We are a team with over 7 years of experience in developing mobile rental solutions. We guarantee passing App Store Review and Google Play. Get a consultation for your project—we will prepare a commercial offer within 2 days.







