Build a vehicle inspection app by managing inspection center slots, requiring fee payment via Paystack before the appointment is confirmed, and issuing a digital inspection certificate (PDF or QR-verifiable record) only after the inspector marks the inspection as completed. Webhook confirms payment; inspector app updates the inspection result and triggers certificate generation.
Tables: centers (name, location, capacity_per_slot, paystack_subaccount_code), inspection_slots (center_id, date, time, booked_count, capacity), inspections (owner_id, vehicle_id, center_id, slot_id, type, fee, status, paystack_ref, certificate_url), vehicles (owner_id, plate_number, make, model, year, color).
var INSPECTION_FEES = {
annual_roadworthiness: 350000, // NGN 3,500
pre_purchase: 500000, // NGN 5,000
insurance_valuation: 250000, // NGN 2,500
};
async function bookInspection(ownerEmail, ownerId, vehicleId, centerId, slotId, type) {
var fee = INSPECTION_FEES[type];
if (!fee) throw new Error('Invalid inspection type');
var slot = await db.inspectionSlots.findById(slotId);
if (slot.booked_count >= slot.capacity) throw new Error('Slot is fully booked');
var reference = 'INSP-' + Date.now();
var center = await db.centers.findById(centerId);
var inspection = await db.inspections.create({
owner_id: ownerId, vehicle_id: vehicleId, center_id: centerId, slot_id: slotId,
type, fee, status: 'pending_payment', paystack_ref: reference,
});
var res = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
email: ownerEmail, amount: fee, currency: 'NGN', reference,
subaccount: center.paystack_subaccount_code, // center gets their share
metadata: { inspection_id: inspection.id, vehicle_id: vehicleId, slot_id: slotId },
}),
});
return (await res.json()).data.authorization_url;
}
// Webhook: confirm inspection slot
if (event.event === 'charge.success') {
var meta = event.data.metadata;
if (!meta.inspection_id) return res.sendStatus(200);
await db.inspections.update(meta.inspection_id, { status: 'confirmed' });
await db.inspectionSlots.increment(meta.slot_id, 'booked_count', 1);
var inspection = await db.inspections.findById(meta.inspection_id);
var vehicle = await db.vehicles.findById(meta.vehicle_id);
var slot = await db.inspectionSlots.findById(meta.slot_id);
await sendBookingConfirmation(inspection.owner_id, vehicle.plate_number, slot.date, slot.time);
}
// Inspector submits results via inspector app
async function submitInspectionResult(inspectorId, inspectionId, result, findings, mileage) {
var inspection = await db.inspections.findById(inspectionId);
var vehicle = await db.vehicles.findById(inspection.vehicle_id);
var owner = await db.owners.findById(inspection.owner_id);
await db.inspections.update(inspectionId, {
status: result === 'pass' ? 'passed' : 'failed',
inspector_id: inspectorId,
findings: findings,
inspected_at: new Date(),
mileage_at_inspection: mileage,
});
if (result === 'pass') {
// Generate certificate with unique verification code
var certCode = 'CERT-' + vehicle.plate_number + '-' + new Date().getFullYear() + '-' + Math.random().toString(36).slice(2, 8).toUpperCase();
var validUntil = new Date();
validUntil.setFullYear(validUntil.getFullYear() + 1); // 1-year certificate
var certificateUrl = await generateCertificatePdf({
inspectionId, vehicle, owner, certCode, validUntil, mileage,
});
await db.inspections.update(inspectionId, {
certificate_url: certificateUrl,
certificate_code: certCode,
certificate_valid_until: validUntil,
});
await sendCertificateEmail(owner.email, owner.name, vehicle.plate_number, certificateUrl);
} else {
// Failed inspection — send remediation report
await sendFailureReport(owner.email, vehicle.plate_number, findings);
}
}
// Public certificate verification (for police/insurers)
async function verifyCertificate(certCode) {
var inspection = await db.inspections.findByCertCode(certCode);
if (!inspection) return { valid: false };
return {
valid: new Date() < new Date(inspection.certificate_valid_until),
plate_number: inspection.vehicle.plate_number,
result: inspection.status,
valid_until: inspection.certificate_valid_until,
};
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
See build a barbershop appointment and payment app for a simpler slot booking with deposit pattern without the certificate generation component.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course