- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
How Rent Payments and Invoices Flow Through a Property Management System
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:
- Contract: the agreement. Who rents what, for how long, at what rent, paid how often.
- Schedule: the plan. Every expected payment, with its due date and amount.
- Invoice: the claim. "This amount is now due." Raised from the schedule as dates arrive.
- Payment: the fact. Money was received, by some method, on some date.
- 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. PlainaddMonths()would overflow 31 January into early March. - Money is an integer. Store amounts in the smallest unit (dirhams, cents) as integers, or as
DECIMALin the database. Never floats.0.1 + 0.2is not0.3in 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?

Be first to comment it...