Architecture
Architecture Overview
Section titled “Architecture Overview”Backend (Laravel API)
Section titled “Backend (Laravel API)”- Controllers (
api/app/Http/Controllers/Api/*): thin HTTP layer, delegates to services; most endpoints are documented with OpenAPI attributes. - Services (
api/app/Services/*): business logic and workflows (payments, matching, bank ingestion, AI matching, etc.). - Repositories (
api/app/Repositories/*): encapsulate database querying for core resources (users, properties, units, tenants, leases, payments). - Models (
api/app/Models/*): Eloquent models. - Database: MySQL is the development and runtime database. Do not assume SQLite-specific behavior for migrations, tests, JSON columns, constraints, or SQL syntax.
- Resources / Collections (
api/app/Http/Resources/*): consistent JSON responses for most CRUD endpoints ({ data, meta?, message? }). - Form Requests (
api/app/Http/Requests/*): validation and request shaping. - Auth & middleware:
- Bearer token auth via Sanctum (
auth:sanctum) for dashboard usage. auth.bank_importfor server-to-server import endpoints (supportsX-API-Keywhen configured).
- Bearer token auth via Sanctum (
- Jobs / queues (
api/app/Jobs/*): async work for imports and hybrid matching runs. - OpenAPI spec (
api/app/OpenApi/*→ generated toapi/storage/api-docs/api-docs.json): canonical API specification.
Frontend (React)
Section titled “Frontend (React)”- Pages (
dashboard/src/pages/*): route-level screens. - Components (
dashboard/src/components/*): UI-only building blocks. - Hooks (
dashboard/src/hooks/*): TanStack Query-based data fetching and mutations (the preferred way for pages/components to talk to the API). - Services (
dashboard/src/services/*): API communication layer for each resource (thin wrappers around the API client). - API client (
dashboard/src/api/*): centralized Axios client + shared response/error types. - Types (
dashboard/src/types/*): TypeScript DTOs used by services/hooks/pages.
Communication
Section titled “Communication”- The dashboard uses TanStack Query hooks to call the Laravel API via the shared Axios client (
dashboard/src/api/client.ts). - Many endpoints return Laravel Resource shapes:
{ data, meta?, message? }. - Some endpoints intentionally return plain DTOs (e.g. matching settings / Ponto settings) or custom shapes (e.g. job status polling).
Key workflows
Section titled “Key workflows”Authentication & user roles
Section titled “Authentication & user roles”Propria uses Laravel Sanctum personal access tokens for the dashboard and mobile PWA.
| Method | Use case |
|---|---|
POST /auth/login | Email + password → { user, token }. Only users with role admin or agent can sign in. |
Authorization: Bearer <token> | All dashboard and mobile API calls under auth:sanctum. |
POST /auth/logout | Revokes the current token. |
GET /auth/me / PUT /profile | Current user and profile updates. |
Roles
| Role | Dashboard access |
|---|---|
admin | Full access: portfolio, payments, transactions, documents hub, settings (matching tuning, AI system message, Ponto, knowledge base upload/index). Users menu (/users) — create and manage admin/agent accounts. |
agent | Day-to-day operations: properties, units, parties, leases, payments, transactions, documents hub, settings tabs that apply to their workflow. No Users menu. Knowledge-base upload/delete/index is rejected by the API (admin only). |
Tokens are scoped by device name (dashboard, mobile-pwa, or default api). Stale tokens for the same device name older than 90 days are pruned on login.
Server-to-server auth (auth.bank_import middleware) protects bank import, payment listing for automation, and job-status polling. Accepts either a Sanctum Bearer token or X-API-Key when N8N_API_KEY is set in api/.env (typical for n8n / scheduled jobs).
Passwords are hashed with bcrypt; API responses never include password fields.
Payments & billing periods
Section titled “Payments & billing periods”- Payments are modeled as monthly rows (
billing_period_start= first day of calendar month). - A dedicated endpoint can create missing monthly rows for active leases:
POST /payments/sync-billing-periods-for-active-leases. amount = base_amount + arrears_extra_amount. An active repayment plan addsarrears_extra_amountto pending months fromstart_billing_periodonward (see Repayment plans below).
Repayment plans
Section titled “Repayment plans”When a tenant owes arrears, an agent can attach a LeaseRepaymentPlan to the lease (lease detail UI → repayment plan panel).
- One active plan per lease — upserting a new plan cancels the previous one.
extra_amount_per_monthis stamped on all pending payments fromstart_billing_periodforward, increasing the expectedamountbank matching uses.- Cancel clears
arrears_extra_amounton those pending rows. - Rent revisions update
base_amountbut preserve any active plan extra.
API: GET|PUT|DELETE /leases/{lease}/repayment-plan.
Bank transaction import methods
Section titled “Bank transaction import methods”Incoming credits only (amount >= 0); debits are excluded. All imports deduplicate by provider ids, canonical event key, and fingerprint.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| Ponto refresh | POST /bank-transactions/refresh | Sanctum | Fetches new transactions from configured Ponto accounts (ponto_settings). Returns a job_id; poll GET /job-status/{job_id}. |
| Bulk JSON import | POST /bank-transactions/import | auth.bank_import | Body: JSON array or { transactions: [...] }. Queued for large batches; optional job_id for status tracking. |
| Single upsert | POST /bank-transactions | Sanctum | Identity upsert for one transaction (integrations, manual correction). |
| Billing sync | POST /payments/sync-billing-periods-for-active-leases | auth.bank_import | Creates missing monthly payment rows before matching runs. |
After import, call POST /bank-transactions/run-auto-match or POST /payments/run-auto-match (hybrid inline/queued) to apply rule-based scoring. Ponto credentials and account selection live in Settings → Bank (GET|PUT /ponto-settings).
Bank matching & reconciliation
Section titled “Bank matching & reconciliation”Matching links BankTransaction credits to Payment rows (monthly rent). Core service: PaymentMatchingService.
Two flows
- Transaction-centric — given a bank line, rank lease/payment candidates (
GET /bank-transactions/{id}/match-candidates). IBAN match is required; no IBAN → no candidates. - Payment-centric — given an expected payment, rank unmatched credits (
GET /payments/{payment}/match-candidates).
Rule-based scoring (max ~100, tunable via matching_settings):
| Signal | Default weight | Notes |
|---|---|---|
| IBAN match | 30 | Against any tenant ibans[]; hard stop if missing or in excluded_ibans. |
| Amount within tolerance | 25 | vs lease.rent_amount or payment.amount (includes repayment extra). |
| Billing period alignment | 25 | Transaction month vs billing_period_start month. |
| Late payment window | 25 | Payment after billing month, within late_window_max_months (default 12). |
| Name similarity | up to 20 | Fuzzy counterparty name; optional month inference from message. |
Auto-match thresholds (defaults): full match ≥ 75, partial link ≥ 50, show in UI dialog ≥ 60. After link/unlink, payment status is recomputed (pending → partial → matched / overpaid). Manual match learns IBANs — the transaction IBAN is added to the tenant for future runs.
AI batch fallback — POST /bank-transactions/ai-match queues provider-backed matching (ollama, openai, gemini) for ambiguous cases; poll GET /ai-match-batch-jobs/{job_id}. Rule-based scoring always runs first.
AI assistant (dashboard chat)
Section titled “AI assistant (dashboard chat)”Endpoint: POST /ai/chat (NDJSON stream). GET /ai/models lists configured providers.
Before the model runs, the API assembles context in this order:
- Admin system message — optional
ai_chat_system_messagefrommatching_settings(Settings → Assistant). Prepended as the firstsystemmessage. - Portfolio entity context —
EntityContextServiceinjects a cached snapshot (5 min TTL) of properties, tenants, active leases, and recent payments so the assistant can answer operational questions (“who is late?”, “rent for unit X?”). - Belgian legislation RAG —
KnowledgeRetrievalServiceretrieves the top 5 chunks from indexed knowledge documents (admin-uploaded PDFs in Settings → General). Embedding search via Ollama when available; keyword fallback otherwise. Chunks cite document name and page. - Property document RAG (optional) — when the client sends
property_id,PropertyDocumentRetrievalServiceadds the top 5 chunks from that building’s indexed PDFs (insurance, claims, maintenance — uploaded on the building detail page).
Request body: { provider?, model, messages[], property_id? }. All authenticated users can chat; only admins can upload or reindex the global knowledge base.
Admin configuration (matching & AI)
Section titled “Admin configuration (matching & AI)”Singleton matching_settings (Settings → Matching / AI / Transactions tabs):
- Scoring weights and thresholds —
score_*,min_score_*,rent_amount_tolerance,late_window_max_months,tenant_name_min_similarity,infer_month_from_message. - Automation —
auto_match_enabled, IBAN exclusions (excluded_ibans,transaction_blacklisted_ibans), last-run telemetry. - AI chat system message —
ai_chat_system_message(max 10 000 chars): persistent instructions for the assistant (tone, jurisdiction, disclaimer, internal procedures).
GET|PUT /matching-settings requires Sanctum; in practice this is an admin responsibility (global tuning affects all agents).
Knowledge base (Belgian legislation PDFs): admin-only upload, index, reindex, delete via /knowledge-documents/* or Settings → General. Stored privately under storage/app/knowledge-base/belgium-legislation. Index via dashboard or php artisan knowledge:index.
Documents hub (printable templates)
Section titled “Documents hub (printable templates)”Three kinds of “document template” coexist; do not confuse them:
| Kind | Storage | Purpose |
|---|---|---|
| Communication templates | communication_template_settings singleton | Plain-text email/WhatsApp when sharing a rent revision from the lease UI |
| Built-in printable templates | builtin_document_templates per user + type | HTML letters/contracts (indexation_letter, residential_lease_contract, notice_of_default) |
| Custom printable templates | custom_templates per user | User-named HTML templates from the hub |
| Building PDFs (RAG) | property_documents per property | Insurance, claims, maintenance files — not printable templates; indexed for AI context |
Dashboard flow
- Registry —
dashboard/src/documents/documentRegistry.tsxlists built-in types (icons, i18n keys,availablevsplaceholder). - Types —
dashboard/src/types/documentHub.ts(DocumentTypeId, preview layout);dashboard/src/types/documentTemplate.ts(editor/print payload, merge field IDs). - Defaults — seed HTML in
dashboard/src/documents/templates/*when the API has no row yet (404 on GET). - Persistence — per-user rows on the server (
GET|PUT /api/document-templates/{documentTypeId}). LegacylocalStoragetemplate keys are migrated away on startup. - Preview layout — optional per-type tweaks in
localStorage(documentHub.layout.v1), not synced to the server. - Print — sheet components under
dashboard/src/components/documents/sheets/;printInIframe(); merge fields resolved from lease/revision context (hub preview uses mocks).
Routes: /documents (hub), /documents/:documentTypeId (built-in editor), /documents/custom/:id (custom template editor).
Lease integration: indexation letters from rent revisions; residential contract from lease details — both reuse the same API templates as the hub.
Merge fields use {{camelCase}} in HTML (tenantName, propertyAddress, baseRent, landlordName, …). See docs/domain.md for the full schema, placeholder lists, and custom_templates API.
Project Structure
Section titled “Project Structure”Backend (Laravel API)
Section titled “Backend (Laravel API)”api/ ├── app/ │ ├── Http/ │ │ ├── Controllers/ │ │ │ └── Api/ │ │ │ ├── AuthController.php │ │ │ ├── UserController.php │ │ │ ├── PropertyController.php │ │ │ ├── PropertyDocumentController.php │ │ │ ├── UnitController.php │ │ │ ├── TenantController.php │ │ │ ├── LeaseController.php │ │ │ ├── PaymentController.php │ │ │ ├── BankTransactionController.php │ │ │ ├── MatchingSettingsController.php │ │ │ ├── PontoSettingsController.php │ │ │ ├── AiChatController.php │ │ │ ├── BuiltinDocumentTemplateController.php │ │ │ ├── CustomTemplateController.php │ │ │ └── KnowledgeDocumentController.php │ │ ├── Requests/ │ │ └── Resources/ │ ├── Jobs/ │ ├── Models/ │ ├── OpenApi/ │ ├── Providers/ │ ├── Repositories/ │ │ ├── Contracts/ │ │ └── (User/Property/Unit/Tenant/Lease/Payment repositories) │ └── Services/ │ ├── AutoMatching/ │ ├── AiMatching/ │ └── (PaymentService, PropertyDocumentService, BankTransactionService, etc.) ├── routes/ │ └── api.php └── storage/ └── api-docs/ └── api-docs.json
Frontend (React + TypeScript):
Section titled “Frontend (React + TypeScript):”dashboard/ └── src/ ├── api/ │ ├── client.ts │ ├── types.ts │ ├── pontoSettings.ts │ ├── matchingSettings.ts │ └── knowledgeDocuments.ts │ ├── components/ │ └── (UI components only) │ ├── pages/ │ └── (route-level screens: buildings, units, tenants, leases, payments, documents hub, settings, etc.) ├── documents/ │ └── (documentRegistry, default HTML seeds, layout localStorage, print helpers) │ ├── hooks/ │ └── (TanStack Query hooks: useBuildings, useUnits, useLeases, usePayments, etc.) │ ├── services/ │ ├── authService.ts │ ├── profileService.ts │ ├── userService.ts │ ├── buildingService.ts │ ├── buildingDocumentService.ts │ ├── unitService.ts │ ├── tenantService.ts │ ├── leaseService.ts │ ├── paymentService.ts │ ├── bankTransactionService.ts │ ├── builtinTemplateService.ts │ ├── customTemplateService.ts │ └── aiChatService.ts │ ├── types/ │ └── (DTOs: building, lease, documentHub, documentTemplate, builtinDocumentTemplate, …) │ └── utils/ └── (pure helpers)
Notes / gotchas
Section titled “Notes / gotchas”- The frontend has an
agentService.ts, but the backend routes do not expose/agentsas a resource; “agents” are represented as users with roleagentand are managed via/usersin the current API.
Property ↔ Building naming split
Section titled “Property ↔ Building naming split”The main real-estate entity is named differently at each layer:
| Layer | Name | Rationale |
|---|---|---|
| DB table | properties | Stable, generic — supports future types |
| Laravel model / services / enums | Property, PropertyDocument, PropertyStatus … | Consistent with DB |
| API routes | /api/properties, /api/properties/{property}/property-documents | REST resource name |
| TS types / DTOs | Building, BuildingUnit, BuildingStatus, BuildingOwnerSummary … | Domain language shown to users |
| Frontend service | buildingService | Wraps /api/properties but uses Building terminology |
| Frontend hooks | useBuildings, useBuilding, useCreateBuilding … | — |
| Frontend components / pages | BuildingsPage, BuildingForm, BuildingDocumentsManager … | — |
Future-proofing: adding a type enum column to properties (building | parking | commercial | land) is all that’s needed to extend the domain — no model or route rename required. The frontend can switch UI labels per type without touching the API contract.