Property management platforms require complex workflows for listings, tenants, and payments. Laravel provides the foundation for robust real estate applications. At ZIRA Software, we've built platforms managing thousands of properties.
Database Schema
// Properties
Schema::create('properties', function (Blueprint $table) {
$table->id();
$table->foreignId('owner_id')->constrained('users');
$table->string('name');
$table->string('address');
$table->string('city');
$table->string('state');
$table->string('zip_code');
$table->enum('type', ['single_family', 'multi_family', 'apartment', 'condo', 'commercial']);
$table->integer('bedrooms')->nullable();
$table->decimal('bathrooms', 3, 1)->nullable();
$table->integer('square_feet')->nullable();
$table->year('year_built')->nullable();
$table->text('description')->nullable();
$table->json('amenities')->nullable();
$table->timestamps();
});
// Units (for multi-family properties)
Schema::create('units', function (Blueprint $table) {
$table->id();
$table->foreignId('property_id')->constrained()->onDelete('cascade');
$table->string('unit_number');
$table->integer('bedrooms');
$table->decimal('bathrooms', 3, 1);
$table->integer('square_feet');
$table->decimal('rent_amount', 10, 2);
$table->enum('status', ['available', 'occupied', 'maintenance']);
$table->timestamps();
$table->unique(['property_id', 'unit_number']);
});
// Tenants
Schema::create('tenants', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained();
$table->string('first_name');
$table->string('last_name');
$table->string('email');
$table->string('phone');
$table->date('date_of_birth')->nullable();
$table->string('ssn_encrypted')->nullable();
$table->decimal('credit_score', 3)->nullable();
$table->timestamps();
});
// Leases
Schema::create('leases', function (Blueprint $table) {
$table->id();
$table->foreignId('unit_id')->constrained();
$table->foreignId('tenant_id')->constrained();
$table->date('start_date');
$table->date('end_date');
$table->decimal('monthly_rent', 10, 2);
$table->decimal('security_deposit', 10, 2);
$table->integer('rent_due_day')->default(1);
$table->enum('status', ['pending', 'active', 'expired', 'terminated']);
$table->text('terms')->nullable();
$table->timestamps();
});
// Rent Payments
Schema::create('rent_payments', function (Blueprint $table) {
$table->id();
$table->foreignId('lease_id')->constrained();
$table->decimal('amount', 10, 2);
$table->date('due_date');
$table->date('paid_date')->nullable();
$table->string('payment_method')->nullable();
$table->string('transaction_id')->nullable();
$table->enum('status', ['pending', 'paid', 'late', 'partial']);
$table->decimal('late_fee', 10, 2)->default(0);
$table->timestamps();
});
// Maintenance Requests
Schema::create('maintenance_requests', function (Blueprint $table) {
$table->id();
$table->foreignId('unit_id')->constrained();
$table->foreignId('tenant_id')->constrained();
$table->string('title');
$table->text('description');
$table->enum('priority', ['low', 'medium', 'high', 'emergency']);
$table->enum('status', ['open', 'in_progress', 'completed', 'cancelled']);
$table->timestamp('completed_at')->nullable();
$table->decimal('cost', 10, 2)->nullable();
$table->timestamps();
});
Property Listing Management
// app/Http/Controllers/PropertyController.php
class PropertyController extends Controller
{
public function index(Request $request)
{
$properties = Property::query()
->with(['units' => fn ($q) => $q->withCount('activeLease')])
->when($request->type, fn ($q, $type) => $q->where('type', $type))
->when($request->city, fn ($q, $city) => $q->where('city', $city))
->withCount(['units', 'units as occupied_count' => function ($q) {
$q->where('status', 'occupied');
}])
->paginate(20);
return view('properties.index', compact('properties'));
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'address' => 'required|string',
'city' => 'required|string',
'state' => 'required|string|size:2',
'zip_code' => 'required|string',
'type' => 'required|in:single_family,multi_family,apartment,condo,commercial',
'amenities' => 'array',
]);
$property = auth()->user()->properties()->create($validated);
return redirect()->route('properties.show', $property)
->with('success', 'Property created successfully');
}
}
Tenant Portal
// app/Http/Controllers/Tenant/DashboardController.php
class DashboardController extends Controller
{
public function index()
{
$tenant = auth()->user()->tenant;
$lease = $tenant->activeLeases()->with('unit.property')->first();
return view('tenant.dashboard', [
'lease' => $lease,
'upcomingPayment' => $this->getUpcomingPayment($lease),
'maintenanceRequests' => $tenant->maintenanceRequests()
->latest()
->take(5)
->get(),
'documents' => $lease?->documents ?? collect(),
]);
}
private function getUpcomingPayment(?Lease $lease): ?RentPayment
{
if (!$lease) return null;
return $lease->payments()
->where('status', 'pending')
->orderBy('due_date')
->first();
}
}
Rent Collection with Stripe
// app/Services/RentPaymentService.php
class RentPaymentService
{
public function processPayment(RentPayment $payment, string $paymentMethodId): RentPayment
{
$tenant = $payment->lease->tenant;
try {
$charge = Stripe::charges()->create([
'amount' => (int) (($payment->amount + $payment->late_fee) * 100),
'currency' => 'usd',
'customer' => $tenant->stripe_customer_id,
'payment_method' => $paymentMethodId,
'description' => "Rent payment for {$payment->lease->unit->full_address}",
'metadata' => [
'payment_id' => $payment->id,
'lease_id' => $payment->lease_id,
],
]);
$payment->update([
'status' => 'paid',
'paid_date' => now(),
'payment_method' => 'stripe',
'transaction_id' => $charge['id'],
]);
event(new RentPaymentReceived($payment));
} catch (CardException $e) {
throw new PaymentFailedException($e->getMessage());
}
return $payment;
}
public function generateMonthlyPayments(): void
{
Lease::active()
->with('payments')
->each(function ($lease) {
$dueDate = now()->setDay($lease->rent_due_day);
// Check if payment already exists for this month
$exists = $lease->payments()
->whereMonth('due_date', $dueDate->month)
->whereYear('due_date', $dueDate->year)
->exists();
if (!$exists) {
RentPayment::create([
'lease_id' => $lease->id,
'amount' => $lease->monthly_rent,
'due_date' => $dueDate,
'status' => 'pending',
]);
}
});
}
}
Maintenance Request Workflow
// app/Http/Controllers/MaintenanceController.php
class MaintenanceController extends Controller
{
public function store(Request $request, Unit $unit)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'description' => 'required|string',
'priority' => 'required|in:low,medium,high,emergency',
'photos.*' => 'image|max:5120',
]);
$maintenanceRequest = MaintenanceRequest::create([
'unit_id' => $unit->id,
'tenant_id' => auth()->user()->tenant->id,
'title' => $validated['title'],
'description' => $validated['description'],
'priority' => $validated['priority'],
'status' => 'open',
]);
// Handle photo uploads
if ($request->hasFile('photos')) {
foreach ($request->file('photos') as $photo) {
$maintenanceRequest->addMedia($photo)
->toMediaCollection('photos');
}
}
// Notify property manager
$unit->property->owner->notify(
new MaintenanceRequestCreated($maintenanceRequest)
);
return back()->with('success', 'Maintenance request submitted');
}
}
Lease Document Generation
// app/Services/LeaseDocumentService.php
class LeaseDocumentService
{
public function generate(Lease $lease): string
{
$pdf = Pdf::loadView('documents.lease', [
'lease' => $lease,
'tenant' => $lease->tenant,
'unit' => $lease->unit,
'property' => $lease->unit->property,
]);
$path = "leases/{$lease->id}/lease-agreement.pdf";
Storage::put($path, $pdf->output());
return $path;
}
}
Conclusion
Property management platforms with Laravel handle complex real estate workflows including listings, tenant management, rent collection, and maintenance tracking.
Building a property platform? Contact ZIRA Software for real estate software development.