Skip to content

Architecture

  • 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_import for server-to-server import endpoints (supports X-API-Key when configured).
  • Jobs / queues (api/app/Jobs/*): async work for imports and hybrid matching runs.
  • OpenAPI spec (api/app/OpenApi/* → generated to api/storage/api-docs/api-docs.json): canonical API specification.
  • 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.
  • 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).

Propria uses Laravel Sanctum personal access tokens for the dashboard and mobile PWA.

MethodUse case
POST /auth/loginEmail + 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/logoutRevokes the current token.
GET /auth/me / PUT /profileCurrent user and profile updates.

Roles

RoleDashboard access
adminFull 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.
agentDay-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 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 adds arrears_extra_amount to pending months from start_billing_period onward (see Repayment plans below).

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_month is stamped on all pending payments from start_billing_period forward, increasing the expected amount bank matching uses.
  • Cancel clears arrears_extra_amount on those pending rows.
  • Rent revisions update base_amount but preserve any active plan extra.

API: GET|PUT|DELETE /leases/{lease}/repayment-plan.

Incoming credits only (amount >= 0); debits are excluded. All imports deduplicate by provider ids, canonical event key, and fingerprint.

MethodEndpointAuthDescription
Ponto refreshPOST /bank-transactions/refreshSanctumFetches new transactions from configured Ponto accounts (ponto_settings). Returns a job_id; poll GET /job-status/{job_id}.
Bulk JSON importPOST /bank-transactions/importauth.bank_importBody: JSON array or { transactions: [...] }. Queued for large batches; optional job_id for status tracking.
Single upsertPOST /bank-transactionsSanctumIdentity upsert for one transaction (integrations, manual correction).
Billing syncPOST /payments/sync-billing-periods-for-active-leasesauth.bank_importCreates 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).

Matching links BankTransaction credits to Payment rows (monthly rent). Core service: PaymentMatchingService.

Two flows

  1. Transaction-centric — given a bank line, rank lease/payment candidates (GET /bank-transactions/{id}/match-candidates). IBAN match is required; no IBAN → no candidates.
  2. Payment-centric — given an expected payment, rank unmatched credits (GET /payments/{payment}/match-candidates).

Rule-based scoring (max ~100, tunable via matching_settings):

SignalDefault weightNotes
IBAN match30Against any tenant ibans[]; hard stop if missing or in excluded_ibans.
Amount within tolerance25vs lease.rent_amount or payment.amount (includes repayment extra).
Billing period alignment25Transaction month vs billing_period_start month.
Late payment window25Payment after billing month, within late_window_max_months (default 12).
Name similarityup to 20Fuzzy 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 (pendingpartialmatched / overpaid). Manual match learns IBANs — the transaction IBAN is added to the tenant for future runs.

AI batch fallbackPOST /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.

Endpoint: POST /ai/chat (NDJSON stream). GET /ai/models lists configured providers.

Before the model runs, the API assembles context in this order:

  1. Admin system message — optional ai_chat_system_message from matching_settings (Settings → Assistant). Prepended as the first system message.
  2. Portfolio entity contextEntityContextService injects 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?”).
  3. Belgian legislation RAGKnowledgeRetrievalService retrieves 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.
  4. Property document RAG (optional) — when the client sends property_id, PropertyDocumentRetrievalService adds 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.

Singleton matching_settings (Settings → Matching / AI / Transactions tabs):

  • Scoring weights and thresholdsscore_*, min_score_*, rent_amount_tolerance, late_window_max_months, tenant_name_min_similarity, infer_month_from_message.
  • Automationauto_match_enabled, IBAN exclusions (excluded_ibans, transaction_blacklisted_ibans), last-run telemetry.
  • AI chat system messageai_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.

Three kinds of “document template” coexist; do not confuse them:

KindStoragePurpose
Communication templatescommunication_template_settings singletonPlain-text email/WhatsApp when sharing a rent revision from the lease UI
Built-in printable templatesbuiltin_document_templates per user + typeHTML letters/contracts (indexation_letter, residential_lease_contract, notice_of_default)
Custom printable templatescustom_templates per userUser-named HTML templates from the hub
Building PDFs (RAG)property_documents per propertyInsurance, claims, maintenance files — not printable templates; indexed for AI context

Dashboard flow

  1. Registrydashboard/src/documents/documentRegistry.tsx lists built-in types (icons, i18n keys, available vs placeholder).
  2. Typesdashboard/src/types/documentHub.ts (DocumentTypeId, preview layout); dashboard/src/types/documentTemplate.ts (editor/print payload, merge field IDs).
  3. Defaults — seed HTML in dashboard/src/documents/templates/* when the API has no row yet (404 on GET).
  4. Persistence — per-user rows on the server (GET|PUT /api/document-templates/{documentTypeId}). Legacy localStorage template keys are migrated away on startup.
  5. Preview layout — optional per-type tweaks in localStorage (documentHub.layout.v1), not synced to the server.
  6. 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.

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

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)

  • The frontend has an agentService.ts, but the backend routes do not expose /agents as a resource; “agents” are represented as users with role agent and are managed via /users in the current API.

The main real-estate entity is named differently at each layer:

LayerNameRationale
DB tablepropertiesStable, generic — supports future types
Laravel model / services / enumsProperty, PropertyDocument, PropertyStatusConsistent with DB
API routes/api/properties, /api/properties/{property}/property-documentsREST resource name
TS types / DTOsBuilding, BuildingUnit, BuildingStatus, BuildingOwnerSummaryDomain language shown to users
Frontend servicebuildingServiceWraps /api/properties but uses Building terminology
Frontend hooksuseBuildings, useBuilding, useCreateBuilding
Frontend components / pagesBuildingsPage, 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.