Aller au contenu

omnibase/estate

Property management for omnibase: organisations, buildings and units, associates, leases and tenants, rent calls, receipts and online payment, contractors and work orders, invitations and a private space for each - posting to omnibase/ledger

Étage
Structure préparée · Omnibase
Version
main
Vérifié en réel
Son README ne le dit pas
Licence
MIT
composer require omnibase/estate:dev-main

Property management for the sites built on omnisense/omnibase: the organisations that own or manage homes (an SCI, an agency), their buildings and units, associates, leases and tenants, rent calls, receipts and online payment, contractors and work orders, invitations, and a private space for each of those people - posting to omnibase/ledger when the organisation keeps its books there. It is a Symfony bundle (Base\Estate\EstateBundle) on the core 3 or 4, with its back office on omnibase/admin. Its words on the pages and in the e-mails are French: it was written for French residential lettings (the loi du 6 juillet 1989, the IRL revision, the quittance).

Nobody signs up. The management invites an associate, a tenant, a contractor or a member of its team; the link creates the account or attaches an existing one; what each person may open comes from what they are to the organisation, never from a role stored on the account.

  • The management (a Member, or any ROLE_ADMIN): the back office - every record, the rents to call and collect, the work orders to give, the applications, the messages.
  • Associates (/associes): the company's figures, its feed, its deadlines, its documents and - with omnibase/ledger - its accounts and each one's share of the result.
  • Tenants (/locataire): their lease, its rents (paid online, notices and receipts to download, a certificate of rent up to date), documents, insurance, messages, the problems they report, the building's notices.
  • Contractors (/artisan): the jobs given to them, with the address and the access, and a form to give a date, a quote, an invoice and a report.
  • The public: the published homes (/biens), a visit request, an application form.

How this manual is organised

Proposer une modification docs/index.md

Installation

Requirements

What composer.json requires: PHP 8.2 or later, doctrine/orm 2.15 or 3, omnisense/omnibase 3.x or 4.0, omnibridge/symfony 1.x, omnibase/admin, and symfony/config, dependency-injection, form, http-kernel, mailer and security-bundle 7 or 8.

What it suggests, each loaded only when present (config/services.php):

Package Adds
omnibase/ledger rent calls, payments and work invoices posted to the organisation's book (Service\Books); bank lines reconciled with rent calls pay them (Ledger\BankSettlement); associates read the books (Ledger\AssociateBookAccess) and their accounts in their space (Ledger\CompanyAccounts)
omnisense/omnitrade with omnitrade/stripe rents paid online by card or SEPA debit (Service\OnlinePayments)
dompdf/dompdf notices, receipts and certificates as PDF (without it, printable pages)
omnisense/omnistate (with omnistate/annuaire-entreprises, omnistate/vies) a company's identity read from the registers (Service\CompanyRecords, estate:company:refresh)

The bundle

// config/bundles.php
return [
    // ...
    Base\BaseBundle::class => ['all' => true],
    Base\Admin\AdminBundle::class => ['all' => true],
    Base\Estate\EstateBundle::class => ['all' => true],
];

EstateBundle aliases Base\Estate\Entity\* to App\Entity\Estate\* and the repositories to App\Repository\Estate\* unless the application declares its own classes there; its templates/ is @Estate.

The routes

# config/routes.yaml
estate_controller:
    resource: "@EstateBundle/src/Controller"
    type: attribute
    prefix: /
Route Path Who
estate_units /biens public
estate_unit /biens/{slug} (GET, POST: visit request) public
estate_unit_apply /biens/{slug}/candidature public
estate_invitation /invitation/{token} (48 hex) public
estate_associate /associes (?societe=<slug>) an associate
estate_tenant /locataire (?bail=<id>; GET, POST) a tenant
estate_rent_pay /locataire/loyer/{id}/payer (POST) a tenant of the lease
estate_rent_paid /locataire/loyer/{id}/retour the payment's return
estate_rent_document /locataire/loyer/{id}/avis or /quittance a tenant of the lease
estate_tenant_attestation /locataire/bail/{id}/attestation a tenant of the lease
estate_contractor /artisan a contractor
estate_work_order /artisan/intervention/{id} (GET, POST) the work order's contractor
estate_document /fichier/{id} whoever may see the document
estate_admin_* /admin/estate/... ROLE_ADMIN (The back office)

The spaces need a signed-in user (ROLE_USER); the core's firewall provides the sign-in.

Configuration

# config/packages/estate.yaml (every key optional)
estate:
    user_class: App\Entity\User
    from_email: '%env(default::MAILER_CONTACT)%'
    payment_gateway: stripe
    payment_methods: [card, sepa_debit]
    invitation_days: 14
    reminder_days: 5
    documents_route: null

The schema

bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate

Tables estate_*: organisation, building, unit, unit_photo, associate, member, lease, lease_party, rent_call, payment, contractor, work_order, work_order_event, document, notice, deadline, activity, application, message, invitation.

The crons

0 6 25 * *  bin/console estate:rent:call              # next month's rents, on the 25th
0 7 * * *   bin/console estate:rent:late              # late rents marked, tenants reminded once
0 3 * * *   bin/console estate:applications:purge     # applications decided over three months ago
0 4 * * 1   bin/console estate:company:refresh        # the companies read again from the registers
Command Options
estate:rent:call --month=YYYY-MM (next month by default), --no-mail; run twice, it calls nothing twice
estate:rent:late --no-mail
estate:applications:purge --months=3
estate:company:refresh - (with omnisense/omnistate); exit 1 when a register did not answer

Proposer une modification docs/installation.md

Organisations, homes and leases

Amounts are integers in cents. Every record hangs off an Organisation, directly or through its unit or lease, with onDelete: CASCADE.

The organisation and its people

Entity Table Fields
Organisation estate_organisation name (not blank), slug (unique), legalName, legalForm, siren, vatNumber, registryCheckedAt, foundedOn, address, postcode, city, email (the management's address: e-mails to it, replies from tenants), phone, iban, bookId (its omnibase/ledger book, when it keeps one); getFullAddress()
Member estate_member, unique on organisation and user the management team: name, email, title, user
Associate estate_associate firstName, lastName, email, phone, shares, quality (MANAGER, ASSOCIATE), since, public, bio, portrait (4 MB), user
Contractor estate_contractor company, contactName, trade, siret, email, phone, iban, user
Deadline estate_deadline title, dueOn, yearly (true), notes; next(?$today) - the next date, a yearly one moved year after year
Activity estate_activity the associates' feed: kind (ActivityKind), message, amount, unit, happenedAt

user links a person to their account once they accepted an invitation (SET NULL when the account goes; a member's cascades).

Homes

Building (estate_building): name, address, postcode, city, builtIn, access (how to get in, shown to contractors).

Unit (estate_unit):

Field Meaning
kind UnitKind: HOUSE, APARTMENT (default), STUDIO, PARKING, COMMERCIAL
name, slug (unique) the public page is /biens/{slug}
building, address, postcode, city, district, door where
surface (decimal 7,2), rooms, bedrooms, floor, heating
energyClass, energyValue, climateClass the DPE's letters and value
description, features (json list), furnished
rent, charges, deposit what a new lease takes
published on the public site
availableFrom free from that date; null: not offered (isAvailable())
facade the card's colour on the public site (ochre by default)
position
photos UnitPhotos (12 MB each, caption, position)
leases newest first

getCurrentLease(?$on), isLet(?$on), getCover().

Leases

Lease (estate_lease):

Field Meaning
unit, reference (unique) MA-2026-003 when made from an application
startOn, endOn, noticeOn
rent, charges, deposit taken from the unit when created; getMonthly() is rent and charges
dueDay 1 to 28, 5 by default: the rent call's due date
paymentMethod PaymentMethod: TRANSFER, CARD, SEPA_DEBIT, CHEQUE, CASH
status LeaseStatus: ACTIVE, NOTICE, ENDED
insuranceUntil the tenants' home insurance, from their certificate
indexQuarter, indexValue the IRL of reference ("T2 2025", decimal)
revisedOn the last revision
parties LeasePartys: role (TENANT, COTENANT, GUARANTOR), name, e-mail (lowercased), phone, IBAN, user
rentCalls newest first

getTenants() (the tenants and co-tenants), getTenantNames(), isRunningOn($on) (not ended, started, not past its end).

Rent calls and payments

RentCall (estate_rent_call, unique on lease and period): period (the month's first day), dueOn (the lease's due day of that month), reference (AVIS-<YYYY-MM>-<lease reference>, unique), rent and charges (the lease's when called), paid, status (RentStatus: CALLED, PARTIAL, PAID, LATE), calledAt, paidAt, remindedAt, receiptNumber (Q-2026-0007), payments. getTotal(), getRemaining(), isPaid(), isLate(?$today). addPayment() adds to paid: PAID (with paidAt the payment's date) once the total is reached, PARTIAL before.

Payment (estate_payment): amount, method, paidOn, reference (unique: a payment is recorded once per reference).

use Base\Estate\Entity\Lease;
use Base\Estate\Entity\LeaseParty;
use Base\Estate\Entity\Organisation;
use Base\Estate\Entity\Payment;
use Base\Estate\Entity\RentCall;
use Base\Estate\Entity\Unit;
use Base\Estate\Enum\PaymentMethod;

$organisation = new Organisation('SCI Les Tilleuls', 'les-tilleuls');
$unit = (new Unit($organisation, 'Appartement 3', 'appartement-3'))->setRent(85000)->setCharges(6000)->setDeposit(85000);
$lease = (new Lease($unit, 'LT-2026-001', new DateTimeImmutable('2026-01-01')))->setDueDay(5);
$lease->addParty(new LeaseParty('Jean', 'Dupont', 'Jean.Dupont@example.org'));

$call = new RentCall($lease, new DateTimeImmutable('2026-10-17'));
echo $call->getReference(), ' due ', $call->getDueOn()->format('Y-m-d'), ' total ', $call->getTotal(), "\n";

$call->addPayment(new Payment($call, 50000, PaymentMethod::TRANSFER, new DateTimeImmutable('2026-10-03')));
echo $call->getStatus()->value, ' remaining ', $call->getRemaining(), ' late on the 6th: ', var_export($call->isLate(new DateTimeImmutable('2026-10-06')), true), "\n";
$call->addPayment(new Payment($call, 41000, PaymentMethod::CARD, new DateTimeImmutable('2026-10-08')));
echo $call->getStatus()->value, ' paid at ', $call->getPaidAt()->format('Y-m-d'), "\n";
echo $lease->getTenantNames(), ' | running on 2026-10-01: ', var_export($lease->isRunningOn(new DateTimeImmutable('2026-10-01')), true), "\n";

Run in omnisense/omnibase's harness image, in memory:

AVIS-2026-10-LT-2026-001 due 2026-10-05 total 91000
partial remaining 41000 late on the 6th: true
paid paid at 2026-10-08
Jean Dupont | running on 2026-10-01: true

Works, documents, messages

Entity Fields
WorkOrder (estate_work_order) unit, reportedBy, title, description, category (Autre), urgency (LOW, NORMAL, URGENT), status (REPORTED, ASSIGNED, SCHEDULED, DONE, CLOSED, CANCELLED), contractor, visitAt, quoteAmount, invoiceAmount, invoiceNumber, report, createdAt, closedAt, events (WorkOrderEvent: author, name, body, date)
Document (estate_document) organisation and one of unit, lease, workOrder; kind (LEASE, INVENTORY, DIAGNOSTIC, RULES, INSURANCE, QUOTE, INVOICE, PHOTO, OTHER), title, file (PDF or image, 20 MB, outside public/), tenantVisible (true), validUntil, uploadedBy, uploadedByName
Message (estate_message) lease, author, authorName, fromManagement, body, createdAt, readAt
Notice (estate_notice) a notice to the tenants: organisation, building (none: all), title, body, sentAt
Application (estate_application) The public site and applications
Invitation (estate_invitation) Invitations

ActivityKind: RENT_CALLED, RENT_PAID, RENT_LATE, LEASE_STARTED, LEASE_ENDED, WORK_REPORTED, WORK_ASSIGNED, WORK_DONE, DOCUMENT, NOTICE, OTHER. The services write one line per event (Service\Activities::log()).

Proposer une modification docs/concepts.md

Rents

Calling the rents

Base\Estate\Service\RentCalls::call(\DateTimeImmutable $month, ?Organisation $organisation = null, bool $notify = true) makes one RentCall per lease running in that month (LeaseRepository::findRunningIn()), and none twice for a lease and a month. For each (callOne()):

  • calledAt is a week before the month, 9:00 (or now, if later) - when notices go out;
  • an activity line "Loyer de octobre 2026 appelé";
  • posted to the books, when the organisation keeps them (below);
  • with $notify, the notice (rent_call e-mail, "Avis d'échéance") to every tenant, reply-to the organisation's address.

estate:rent:call [--month=YYYY-MM] [--no-mail] runs it for every organisation (next month by default); the back office's button calls the current month.

Late rents and reminders

RentCalls::markLate(\DateTimeImmutable $today, bool $remind = true): every call past due and not paid becomes LATE (from CALLED or PARTIAL, with an activity line); a call that was not reminded yet and whose due date is estate.reminder_days (5) days past is reminded once (rent_reminder e-mail, remindedAt set). estate:rent:late [--no-mail], daily.

Payments

Base\Estate\Service\Payments::record(RentCall $call, int $amount, PaymentMethod $method, ?\DateTimeImmutable $paidOn = null, ?string $reference = null, bool $notify = true, bool $post = true): Payment:

  • a non-positive amount is an InvalidArgumentException;
  • a reference already recorded returns that payment: nothing twice;
  • the payment is added to the call; an activity line "Loyer reçu (virement)";
  • the call paid in full: its receipt numbered (Q-<year>-<0001>, per year of the period, in the order rents are paid); posted to the books (unless $post is false: a bank line already posted it); with $notify, the receipt e-mailed to the tenants, the PDF attached when dompdf is installed.

The back office records a payment by hand (amount, method, date, reference).

Receipts, notices and certificates

Base\Estate\Service\Receipts renders @Estate/pdf/receipt.html.twig (quittance), notice.html.twig (avis d'échéance) and attestation.html.twig (the certificate a tenant hands a bank, the CAF or a future landlord), as PDF with dompdf (A4, Helvetica, no remote resources), or as HTML without it. The tenant downloads the notice, the receipt once paid, and the certificate only when nothing past due is owed.

Online payment

Base\Estate\Service\OnlinePayments, through omnisense/omnitrade's Registry and the gateway estate.payment_gateway (stripe): available when the registry has that gateway.

  • start(RentCall $call, string $method, string $returnUrl, string $cancelUrl, ?string $email = null) - a purchase of the remaining amount in EUR, referenced by the call, described "Loyer 10/2026 — <home>", idempotent per call and amount already paid, the method among estate.payment_methods (card, sepa_debit), metadata rent_call, locale fr: the provider's page.
  • complete(RentCall $call, string $reference) - back from the provider (?session=): recorded as a card or SEPA payment once the transaction is paid. A SEPA debit is paid later.

The tenant's "Payer" is a POST with the token rent_pay_<id>; refused for a paid call or when online payment is off.

The IRL revision

Base\Estate\Service\RentRevisions (loi du 6 juillet 1989, art. 17-1):

  • nextOn(Lease $lease, ?$today) - a year after the start or the last revision, moved on year by year while it is more than eleven months behind;
  • rentAt(Lease $lease, string $newIndex) - the rent (charges aside) times the new index over the reference index, rounded to the cent; both indices are needed;
  • apply(Lease $lease, string $newQuarter, string $newIndex, \DateTimeImmutable $on) - the new rent, the new reference (quarter and value), revisedOn, an activity line and the tenants' letter (rent_revision e-mail); returns the former rent;
  • due(int $days = 45) - the active leases with a reference index whose revision falls within that many days, for the dashboard.

The back office's revision page takes the quarter ("T2 2025") and the INSEE value, shows the new rent, then applies it from the anniversary - never backdated: from today when asked later.

The books

With omnibase/ledger, and an organisation whose bookId names a book, Base\Estate\Service\Books posts:

Event Entry (journal) Source
a rent called VT: the tenant's account (411, party lease:<reference>) debited the total; 706 credited the rent, 708 the charges estate:rent:<call reference>
a payment BQ: 512 (5112 for a card or SEPA payment, until the provider's payout) debited; the tenant credited estate:payment:<id>
a work order closed with an invoice AC: 615 debited; the contractor's account (401, party contractor:<id>) credited estate:work:<id>

A call paid in full has its entry and its payments' lettered on the tenant's account. Each posting carries its source: nothing is posted twice.

Base\Estate\Ledger\BankSettlement listens to omnibase/ledger's BankLineReconciled: money in whose settled open items come from rent calls (estate:rent:<reference>) pays those calls (Payments::record(..., 'bank:<line id>:<call id>', notify: true, post: false)), the receipt sent, nothing posted twice.

Proposer une modification docs/usage-rents.md

Works, documents and messages

Work orders

Base\Estate\Service\WorkOrders follows a problem from its report to its invoice. Every step is a line of the order's timeline (WorkOrderEvent), a line of the associates' feed, and an e-mail to whoever it concerns.

Step Method Status E-mails
reported (by a tenant from their space, or the management) report(Unit, ?User, string $byName, string $title, string $description, string $category = 'Autre', Urgency $urgency = NORMAL) REPORTED the managers (the members and the organisation's address): "[Intervention #12] ..."
given to a contractor assign(WorkOrder, Contractor, ?\DateTimeImmutable $visitAt, ?User, string $byName = 'Gestion', ?string $note = null) SCHEDULED with a visit date, else ASSIGNED the contractor; the current lease's tenants
the contractor's word update(WorkOrder, ?User, string $byName, ?$visitAt, ?int $quote, ?string $report, bool $done, ?int $invoice, ?string $invoiceNumber) SCHEDULED for a new visit date, DONE when done the managers; the tenants for a visit date or the end
a note note(WorkOrder, ?User, string $byName, string $body) - -
closed by the management close(WorkOrder, ?User, string $byName = 'Gestion') CLOSED, closedAt - ; the invoice posted to the books

update() writes only what changed; with nothing new it does nothing. A tenant's report form offers the categories Plomberie, Chauffage, Électricité, Serrurerie, Menuiserie, Humidité, Parties communes, Autre, and the three urgencies.

The contractor's page (/artisan/intervention/{id}, ESTATE_WORK_DO) shows the address, the access, who lives there and the timeline, with two forms: the visit date, the quote, the invoice and its number, the report and "done"; and a file (quote, invoice, photo).

Documents

Base\Estate\Service\Documents:

  • insurance(Lease, File, \DateTimeImmutable $until, User, string $name) - a tenant's home insurance certificate: a document of kind INSURANCE on the lease, valid until that date; the lease's insuranceUntil moves forward with it; an activity line; the management told.
  • work(WorkOrder, DocumentKind, File, ?string $title, User, string $name, bool $tenantVisible = false) - a contractor's quote, invoice or photo on a work order, titled "Devis — <title>" when none is given; the management told.

Files are streamed by /fichier/{id} (estate_document) to whoever may see them (Spaces and access), inline, named after the document's title; the uploads stay out of public/.

Messages

Base\Estate\Service\Messages keeps the thread between a lease's tenants and the management:

  • post(Lease, string $body, bool $fromManagement, ?User $author, string $authorName) - kept, then e-mailed to the other side: every tenant of the lease (reply-to the organisation), or the organisation's address;
  • read(Lease, bool $byManagement) - the thread, the other side's messages marked read.

The tenant writes from their space; the management from /admin/estate/messages, every lease's thread, the latest first, with its unread count.

Notices

A Notice (the building's or every building's) is sent from the back office (estate_admin_notice_send): one e-mail to each tenant of the current leases of the organisation's units - of that building only when it names one -, sentAt set, an activity line. The tenants see the five last sent in their space.

Proposer une modification docs/usage-works.md

The public site and applications

The homes

/biens (estate_units) lists the published units - those free first (by availableFrom), then by position and name - with ?type=<UnitKind value> and ?libres=1 (free only); the page also gets every published unit, for a filter in the page. /biens/{slug} shows one, three others, and a visit request (name, e-mail, phone, message): e-mailed to the organisation, reply-to the visitor. A filled trap field (website) is a robot: thanked all the same, nothing sent.

Applying

/biens/{slug}/candidature (estate_unit_apply) is open while the home is offered (availableFrom set). The form (Base\Estate\Model\ApplicationRequest):

Field Constraints
firstName, lastName not blank, 80 at most
email not blank, an e-mail
phone 40 at most
occupants 1 to 12
moveInOn defaults to the later of the home's availability and today
occupation PERMANENT, FIXED_TERM, CIVIL_SERVANT, SELF_EMPLOYED, STUDENT, RETIRED, JOB_SEEKER, OTHER
income the household's net monthly income, in cents, 0 or more
guarantee VISALE, PERSON, COMPANY, NONE
dossierUrl a link to the applicant's file (DossierFacile)
message 3000 at most
consent required

A filled trap is thanked and nothing kept.

From received to let

Base\Estate\Service\Applications:

Step What happens
submit(Application) kept (NEW); an activity line; the organisation told; the applicant acknowledged
shortlist(Application) SHORTLISTED; nobody told
inviteToVisit(Application, \DateTimeImmutable $at, ?string $message) VISIT, the date; the applicant invited, with the management's words
decline(Application, ?string $message, bool $notify = true) DECLINED (only an open one); the applicant told politely
accept(Application, \DateTimeImmutable $startOn, bool $declineOthers = false) the lease made - the applicant its tenant, the home's rent, charges and deposit, starting then, its reference <initials>-<year>-<001> -; the home no longer offered; ACCEPTED; a "lease started" line; with $declineOthers, the other open applications declined
purge(int $months = 3) the applications decided (accepted or declined) more than that long ago deleted: estate:applications:purge, daily

An open application is NEW, SHORTLISTED or VISIT; deciding one sets decidedAt. Application::getIncomeRatio() is the income over rent and charges (landlords look for about 3). The lease reference's initials are the organisation name's first letters, without legal forms and small words (SCI, SARL, DE, LA...).

Proposer une modification docs/usage-applications.md

Invitations

Nobody signs up: the management invites an associate, a lease's party, a contractor or a member of the team (Invitations::invite()); the link creates the account or attaches the existing one (/invitation/{token}).

The token, its hash (only the SHA-256 is stored), the expiry, the lookup and the new account are omnisense/omnibase's:

  • Base\Estate\Entity\Invitation extends Base\Entity\User\Invitation (a mapped superclass: e-mail, name, tokenHash, createdAt, expiresAt, acceptedAt, invitedBy) and adds the organisation, the kind and the target. Same table (estate_invitation), same columns: no migration.
  • Base\Estate\Service\Invitations extends Base\Service\Invitations: it says what an invitation opens (target()) and what accepting does (accept(): the account on the associate, the party, the contractor; ROLE_ADMIN for a member). createUser() and existingUser() are the core's.

Configuration: estate.invitation_days (14), estate.user_class.

Inviting

Base\Estate\Service\Invitations::invite(Associate|LeaseParty|Contractor|Member $target, ?User $by = null, bool $send = true) returns the invitation and its token (sent, never stored): its kind (ASSOCIATE, TENANT, CONTRACTOR, MEMBER), the organisation, the target's id, e-mail and name, expiring after estate.invitation_days. With $send, the invitation e-mail ("<organisation> vous ouvre son accès") carries /invitation/{token}, reply-to the organisation.

The back office invites from a record (estate_admin_invite, /admin/estate/invite/{kind}/{id}, kind among member, associate, tenant, contractor, a link with its token).

Accepting

/invitation/{token} (estate_invitation, 48 hex characters):

  • an unknown, used or expired token: invitation_expired.html.twig, status 410;
  • signed in: a button (POST, token invitation_<id>) attaches the account;
  • an account exists for the address: the page says so (sign in first);
  • otherwise a form (username, password) creates the account of estate.user_class, attaches it and signs the user in (form_login, firewall main); a username taken is a form error.

accept() sets the target's user; a member who is not ROLE_STAFF gets ROLE_ADMIN. What the invitation opened must still exist (RuntimeException otherwise).

Proposer une modification docs/invitations.md

Spaces and access

Who someone is

Base\Estate\Service\Spaces says, from the relations and never from a role stored on the account, what a user is to the organisations (memoised per request, reset between requests):

Method Returns
managed(?User) the organisations whose back office they run: every one for a ROLE_ADMIN, else those they are a Member of
manages(?User, Organisation)
associateships(?User), isAssociateOf(?User, Organisation) their Associate records
tenancies(?User), isPartyOf(?User, Lease) the leases they are a party of
contractorships(?User) their Contractor records
of(?User) the spaces to offer, in order: associate (estate_associate), tenant (estate_tenant), contractor (estate_contractor)

EstateVoter

Attribute Subject Granted to (beyond the organisation's management, granted everything)
ESTATE_MANAGE Organisation nobody else
ESTATE_ASSOCIATE Organisation its associates
ESTATE_LEASE_VIEW Lease its parties
ESTATE_RENT_VIEW RentCall its lease's parties
ESTATE_UNIT_REPORT Unit the parties of its current lease
ESTATE_WORK_VIEW WorkOrder its contractor, the parties of the unit's current lease, the organisation's associates
ESTATE_WORK_DO WorkOrder its contractor
ESTATE_DOCUMENT_VIEW Document see below

Documents

The associates see every document of their company. A work order's document: its contractor, and - when it is tenant-visible - the parties of the unit's current lease. Any other document must be tenant-visible: a lease's to its parties, a unit's to the parties of its current lease while they live there.

The four spaces

  • Associates (/associes, ?societe=<slug> among several): the figures (Service\Figures::of(): units, lettable, let, vacant, occupancy, monthly rent, the year's rents called and received and the rate, late rents and their total, the last six months side by side, open work orders, what works cost this year, how rents were paid, the online part), the feed (15 lines), the units, the six next deadlines, the associates and their shares, a link to estate.documents_route when set, and - with omnibase/ledger - the accounts (Ledger\CompanyAccounts::of(): the year's compte de résultat so far beside the year before, each associate's share of the result by their shares - an SCI is transparent -, the bank accounts' balances and their eight latest movements). Read-only.
  • Tenants (/locataire, ?bail=<id> among several): the lease, its rents (pay online, download the notice and the receipt), the certificate, the lease's documents, the insurance certificate form, the messages, the work orders of the unit and a form to report one, the five last notices.
  • Contractors (/artisan): the work orders given to them, each at /artisan/intervention/{id}.
  • The management: the back office.

A user who is none of these gets a 403 on that space.

Twig

Base\Estate\Twig\EstateExtension:

estate_spaces() the signed-in user's spaces ({key, route}), for the header and "Mon accès"
estate_online_payment() whether online payment is on
x|euros(round = false) cents as the pages write money: 1 234,56 € (narrow no-break spaces), — for null; round drops the cents

Proposer une modification docs/usage-spaces.md

The back office

Loaded when omnibase/admin is installed. The CRUDs extend AbstractEstateCrudController, which opens creating, editing, deleting and the three saves to ROLE_ADMIN (not only the super-administrator); the action pages are #[IsGranted('ROLE_ADMIN')]. Labels are French.

The screens

CRUD Slug Actions of its own
OrganisationCrudController organisations Lire les registres (with omnisense/omnistate)
MemberCrudController members Inviter
AssociateCrudController associates Inviter
BuildingCrudController buildings
UnitCrudController units
UnitPhotoCrudController unit-photos
LeaseCrudController leases Réviser le loyer
LeasePartyCrudController lease-parties Inviter
RentCallCrudController rent-calls no new nor edit; Encaisser (record a payment), Appeler les loyers du mois
PaymentCrudController payments read-only
ContractorCrudController contractors Inviter
WorkOrderCrudController work-orders Confier, Clôturer
DocumentCrudController documents
NoticeCrudController notices Envoyer aux locataires
DeadlineCrudController deadlines
ApplicationCrudController applications no new; the next steps (shortlist, visit, decline, accept) while open
ActivityCrudController activities no new nor edit
InvitationCrudController invitations no new nor edit

The action pages

Base\Estate\Controller\Admin\ActionController, under /admin/estate:

Route Path
estate_admin_invite /invite/{kind}/{id} invite, link token estate_invite_<kind>_<id>
estate_admin_rents_call /rents/call (?month=) call the month's rents
estate_admin_payment /rent/{id}/payment (GET, POST) record a payment: amount (850,00), method, date, reference
estate_admin_assign /work-order/{id}/assign (GET, POST) give to one of the organisation's contractors, a visit date, a note
estate_admin_close /work-order/{id}/close close
estate_admin_notice_send /notice/{id}/send send to the tenants
estate_admin_application /application/{id}/{shortlist,visit,decline,accept} (GET, POST) the next step: a date and a word for a visit, a word to decline, the lease's start (and decline the others) to accept
estate_admin_revise /lease/{id}/revise (GET, POST) the IRL revision: the quarter (T2 2025) and the value, "calculate", then "apply"
estate_admin_company_refresh /company/{id}/refresh the registers (Sirene / RNE, VIES); 404 without omnisense/omnistate

Links carry a CSRF token in their query (<action>_<id>), forms in _token. After an action the page goes back to the back-office page it came from, and only to one under /admin.

MessageController: /admin/estate/messages (estate_admin_messages, every lease's thread, the latest first) and /admin/estate/messages/{id} (a lease's thread, answered from the organisation's name).

Reading the registers

Base\Estate\Service\CompanyRecords::refresh(Organisation $organisation): bool (with omnisense/omnistate): a valid SIREN read from Sirene / RNE - the legal name, the legal form, the creation date, the head office's street (capitals put back in their case: "36 A rue de la Ganzau"), postcode and city ("Saint-Louis") - and the VAT number from VIES (none when VIES does not know it, as for an SCI letting homes, which is not liable); registryCheckedAt set. false without a valid SIREN or for a company the register does not know; a register that does not answer throws omnistate's UnavailableException and nothing changes.

Dashboard tiles

  • estate_overview (OverviewWidgetType): per organisation, its figures, the rents to revise within 45 days, the tenants' messages unread, the new applications, the active leases whose insurance is missing or run out.
  • estate_payments (PaymentsWidgetType): the eight latest payments and how, this month's total per method, whether online payment is on.
yield MenuItem::block('estate_overview', 'Gestion', 'fa-solid fa-building');
yield MenuItem::block('estate_payments', 'Encaissements', 'fa-solid fa-euro-sign');

Proposer une modification docs/back-office.md

Configuration reference

estate:

Base\Estate\DependencyInjection\EstateConfiguration, dumped with Symfony's YamlReferenceDumper in omnisense/omnibase's harness image:

estate:

    # The application's user entity: invitations create accounts of this class.
    user_class:           App\Entity\User

    # Who the e-mails to tenants, associates and contractors come from.
    from_email:           '%env(default::MAILER_CONTACT)%'

    # The omnitrade gateway rents are paid online through. Empty: no online payment.
    payment_gateway:      stripe

    # What a tenant may pay with online, as omnitrade names it.
    payment_methods:

        # Defaults:
        - card
        - sepa_debit

    # How long an invitation link stays valid.
    invitation_days:      14

    # How many days after its due date an unpaid rent is marked late and reminded (estate:rent:late).
    reminder_days:        5

    # The application's documents page, linked from the associates' space. Null: none.
    documents_route:      null
Option Type Default Read by
user_class class App\Entity\User Invitations (accounts it creates), InvitationController (usernames taken)
from_email string %env(default::MAILER_CONTACT)% EstateMailer; empty: the mailer's own sender
payment_gateway string stripe OnlinePayments; empty: no online payment
payment_methods list card, sepa_debit OnlinePayments
invitation_days int, 1 or more 14 Invitations
reminder_days int, 1 or more 5 RentCalls::markLate()
documents_route route name or null null AssociateController

Services

config/services.php autowires and autoconfigures src/ but DependencyInjection/, Entity/, Enum/, Model/, Exception/, Controller/Admin/ and the bundle class, and leaves out Service/Books.php and Ledger/ without omnibase/ledger (Base\Ledger\Service\Posting), and Service/CompanyRecords.php and Command/CompanyRefreshCommand.php without omnisense/omnistate. Controller/Client/ is tagged controller.service_arguments; Controller/Admin/ too when omnibase/admin is installed. Books is an optional argument of Payments, RentCalls and WorkOrders; omnitrade's Registry of OnlinePayments. Spaces implements ResetInterface. The e-mails are @Estate/email/<name>.html.twig: rent_call, rent_reminder, receipt, rent_revision, invitation, message, notice, document_added, work_reported, work_assigned, work_update, visit_request, application_received, application_ack, application_visit, application_declined.

Proposer une modification docs/configuration.md

Testing

The package's suite is tests/Controller/CrudRequiredFieldsTest.php: on omnibase/admin's CrudFieldsTestCase, the back office's required fields against what each record needs. In omnisense/omnibase's harness, with this checkout and its omnibase/ledger sibling mounted:

docker run --rm --network none \
    -v "$PWD":/srv/demo/vendor/omnibase/estate:ro -v "$PWD/../ledger":/srv/demo/vendor/omnibase/ledger:ro \
    omnisense/omnibase-harness test estate
......                                                              6 / 6 (100%)

OK (6 tests, 7 assertions)

The package has no tests of its services: an application tests them through its own pages and commands. The entities need no database: the in-memory example in Organisations, homes and leases runs in the harness image as it is.

Proposer une modification docs/testing.md