Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

How Rent Payments and Invoices Flow Through a Property Management System

16 May 2025Category : Blog

About Post

From the tenant's side, paying rent is one tap. From the system's side, that tap is the middle of a long story that started the day the contract was signed and won't end until the money shows up on a bank statement and someone agrees it's really there.

I work on Kate PMS, the property management system behind our web portal, staff app and tenant app, which handles 2,000+ tenancy contracts across a multi-property portfolio. Money is the part of a system like this where small design shortcuts turn into very long afternoons. So here's how rent flows through a property system in general terms, and the design rules I hold onto. No secrets, just the shape of it.

The chain: five things, five different meanings

The biggest clarity win is realising that these are five different things, and each one means something different:

  1. Contract: the agreement. Who rents what, for how long, at what rent, paid how often.
  2. Schedule: the plan. Every expected payment, with its due date and amount.
  3. Invoice: the claim. "This amount is now due." Raised from the schedule as dates arrive.
  4. Payment: the fact. Money was received, by some method, on some date.
  5. Receipt: the proof the tenant gets that we received it.

And then there's reconciliation: confirming that the payments we recorded match the money that actually arrived in the bank.

Mixing these up is the classic mistake. If you treat "scheduled" and "invoiced" as the same thing, you can't show a tenant what's coming next without claiming they owe it. If you treat "payment recorded" and "money received" as the same thing, your balances are only as good as the last person who typed one in.

Step 1: The contract generates a schedule

When a contract is signed, the system generates the full schedule up front. That gives staff and tenants a clear view of the year ahead, and it turns "what's due when" into a simple query instead of a calculation done on every page load.

Here's a simplified version for monthly rent. It's an illustration, not production code:

public function schedule(Contract $contract): array
{
    $start = CarbonImmutable::parse($contract->start_date);
    $rows = [];

    for ($i = 0; $i < $contract->duration_months; $i++) {
        $rows[] = [
            'contract_id' => $contract->id,
            'due_date' => $start->addMonthsNoOverflow($i)->toDateString(),
            'amount' => $contract->monthly_rent, // integer, minor units
            'status' => 'scheduled',
        ];
    }

    return $rows;
}

Two details in there are worth stealing:

  • Always count from the start date. If you add one month to the previous due date, a contract starting on 31 January drifts: 31 Jan, 28 Feb, 28 Mar, 28 Apr... Counting from the start with addMonthsNoOverflow($i) gives 31 Jan, 28 Feb, 31 Mar, 30 Apr. Plain addMonths() would overflow 31 January into early March.
  • Money is an integer. Store amounts in the smallest unit (dirhams, cents) as integers, or as DECIMAL in the database. Never floats. 0.1 + 0.2 is not 0.3 in floating point, and rent totals are exactly where you'd notice.

Real contracts add more: quarterly or yearly frequencies, a deposit, a first period that starts mid-month, rent-free periods. They're all variations of the same idea: the contract terms go in, a list of expected payments comes out.

Step 2: Invoices are raised when they're due

A scheduled job turns schedule rows into invoices as their dates approach. An invoice has a small, strict lifecycle:

issued → partially_paid → paid, with void as the only other exit.

Making the status a proper state machine (only certain transitions are allowed, and every change is logged) saves you from the "how did a voided invoice become paid?" class of bug. It also means reminders and overdue notices can be driven entirely from status and due date.

Step 3: Payments are allocated, not just recorded

Rent doesn't always arrive in neat, matching amounts. A tenant pays two months at once, or half now and half next week, or a little more than they owe. So a payment isn't "attached to an invoice". It's allocated across invoices:

DB::transaction(function () use ($payment) {
    $remaining = $payment->amount;

    $invoices = Invoice::where('contract_id', $payment->contract_id)
        ->whereIn('status', ['issued', 'partially_paid'])
        ->orderBy('due_date')
        ->lockForUpdate()
        ->get();

    foreach ($invoices as $invoice) {
        if ($remaining <= 0) break;

        $applied = min($remaining, $invoice->balance());
        $payment->allocations()->create(['invoice_id' => $invoice->id, 'amount' => $applied]);
        $invoice->refreshStatus();
        $remaining -= $applied;
    }

    // Anything left over becomes a credit on the contract, not a lost amount.
});

Oldest invoice first is a sensible default, and the allocation table gives you a precise answer to "which payment paid which invoice", which is the first question anyone asks when a balance looks wrong. The transaction and row lock stop two payments being allocated to the same invoice at the same moment.

Step 4: Receipts and reconciliation

A receipt should be generated from the payment, never typed by hand, so it can never disagree with what was recorded. And it should reach the tenant immediately, in the app or by email, because "did you get my payment?" is a support call you can avoid.

Reconciliation is the reality check. Online payments typically come with a gateway reference that can be matched automatically. Bank transfers and other offline methods are usually matched against bank statements by finance staff. Until a payment is reconciled, it's recorded but not confirmed, and the system should be honest about that difference.

The rules I don't bend

  • Never edit money, reverse it. A wrong payment isn't changed or deleted. It's reversed with a new entry, and the correct one is recorded. The history always adds up, and an auditor can follow every step.
  • Everything is idempotent. Payment webhooks arrive twice, and users tap "Pay" twice. Make the gateway's transaction reference unique in the database, so a duplicate can't create a second payment.
  • Balances are derived, not stored and hoped for. A tenant's balance comes from invoices minus allocations. If you cache it, you can always rebuild it.
  • Permissions follow responsibility. Recording, voiding and reversing payments should be separate permissions in role-based access, and every action attributed to a person.
  • Changes only touch the future. An early termination or a renewal regenerates future schedule rows. Issued invoices and recorded payments stay as they are.

If you build anything with money in it: separate the plan, the claim and the fact; store amounts as integers; never update a financial record when you can add a correcting one. Those three rules prevent most of the bugs that are hardest to explain to an accountant.

What the tenant sees

All of this exists so the tenant app can show three simple things: what I owe now, what's coming next, and proof of what I've paid. The simpler that screen looks, the more careful the model behind it has to be.

If you've built billing or payments: how do you handle partial payments and overpayments? Oldest invoice first, or let the payer choose?

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close