Rental property under a tax-incentive scheme (E15)¶
See the real return of a rental investment and never miss an obligation of the scheme it sits in. Everything is computed by code from the bank flows of the
property's account, the amortization schedule of its loan (E9) and the facts YOU recorded in memory/assets.yaml. A fact that is not recorded stays
missing (it is listed and becomes an open question); a figure you typed (rent cap, tenant income limit, reduction rate, market rate, valuation) is used as
given. Nothing is looked up on the web and nothing is guessed. The tax-year figures are candidates, not a return; the indicators are facts with a neutral
reading, never a recommendation.
| Story | What | Where |
|---|---|---|
| E15-1 | a property account (purpose rental) linked to a real_estate_rental asset; every flow of it labelled to the property by generic French rules |
rental/model.py, classify/rules.yaml, classify/taxonomy.yaml |
| E15-2 | monthly cash flow, yearly P&L, effort d'epargne, vacancy months | rental/cashflow.py, coach rental cashflow / pnl, /rental, rental_overview |
| E15-3 | the scheme commitment: start, length, end, rent cap and tenant income checks, reminders, extension decision, missing facts as questions | rental/scheme.py, coach rental scheme / extension, alert kinds scheme_end, scheme_check, rent_missing |
| E15-4 | tax-year figures: micro-foncier vs reel, loan interest from the schedule, scheme reduction candidate, documents checklist | rental/taxyear.py, coach rental tax, tax_candidates |
| E15-5 | renegotiate-or-sell indicators: loan rate vs the market rate you entered, end of the commitment, net equity | rental/indicators.py, coach rental indicators, /rental |
There is no migration: the facts are memory (files you can read and edit), everything else is computed, and the alert events use the existing alert_events table.
1. The property and its account (E15-1)¶
A property is an asset of kind real_estate_rental in assets.yaml; its id is the property id (several properties are supported; keep the id free of an address or a town: models see it only as
asset-N).
assets:
- id: rental-flat-1
kind: real_estate_rental
account: Main rental account # uid or label of its bank account (purpose: rental)
loan: rental-loan # the loan file that financed it (else a loan whose `asset` is this id, else the mortgage debited from its account)
scheme: pinel # the scheme's NAME: free text, data only
value: 200000 # what you believe it is worth today, with as_of
as_of: 2026-09-01
rent_monthly: 620 # the lease rent
purchase_price: 190000
purchase_date: 2021-02-01
commitment: # E15-3, every figure is YOURS (the deed, the scheme's table)
start_date: 2021-03-05
years: 9
surface_m2: 40
rent_cap_monthly: 600 # or rent_cap_m2 (EUR a month per m2, as you computed it) with surface_m2
tenant_income_limit: 30000
tenant_income: 28000
reduction_rate_pct: 18 # total over the commitment (E15-4)
reduction_base_cap: 300000 # optional ceiling of the eligible price
reduction_schedule: # optional: a scheme that spreads its reduction unevenly (years at a yearly % of the price); wins over everything
- { years: 9, yearly_rate_pct: 2 }
- { years: 3, yearly_rate_pct: 1 }
reduction_first_year: 2021
extension: { decision: extend, years: 3, additional_rate_pct: 6, decided_on: 2029-06-01 } # E15-3
market_rate: { rate_pct: 3.1, as_of: 2026-09-30, source: a quote } # E15-5, typed by you
vacancies: # E15-2: periods you know it was empty
- { start: 2026-02-01, end: 2026-02-28, note: works }
coach rental add / edit and the Rental page write these fields (validated, previewed, recorded in the memory history). The older pinel_commitment_years still gives the length when no
commitment.years is set.
The account. The asset names it (account). When it does not, and exactly one property is still without an account and exactly one account has purpose: rental that no property names, that account
is used and reported as only_one so you can confirm it. A declared account that is not in the data is reported (memory check: rental_unknown_account). Several properties = several accounts; two
properties on one account need the tag below.
Everything it carries is a property flow. The shipped, generic French rules (no brand) put the usual flows in the property's categories; the loan instalment of an account whose purpose is rental
was already housing.rental_property_loan:
| Flow | Category | Rule (generic words) |
|---|---|---|
| rent received | income.rental |
LOYER coming in, unless the label says deposit, guarantee, caution, refund or regularisation; a property manager's name coming in (existing) |
| loan instalment | housing.rental_property_loan |
type rule of a rental account; the linked loan's own payments count whatever the account |
| co-ownership charges | housing.property_charges |
SYNDIC, COPROPRIETE, CHARGES DE COPRO |
| property tax | housing.property_tax (new) |
TAXE FONCIERE (before the public-finance rule: it is paid to the tax office but is a cost of the property) |
| management fees | housing.property_management (new) |
GESTION LOCATIVE, GERANCE; FRAIS / HONORAIRES DE GESTION only with LOCATIVE, LOYER, GERANCE or IMMOBILIER in the same label (a bare fee is also a bank, life-insurance or PEA fee) |
| insurance | housing.property_insurance (new) |
PNO, PROPRIETAIRE NON OCCUPANT, GLI, GARANTIE LOYERS IMPAYES |
| works | housing.renovation / housing.maintenance_diy |
your annotations (a contractor is a name, not a rule) |
An older copy of the taxonomy gets the three new leaves with uv run coach taxonomy merge-package (it never overwrites your edits). What the rules do not recognise stays in other flows and is listed
by coach rental flows (and the Rental page) so you can label it with a memory annotation; the account's own bank fees stay in other flows but are not asked about. A cost paid from ANOTHER
account counts for the property when a memory annotation gives it the tag property-<id> (for example the property tax paid from the main account). The owner's own transfers into the property
account (transfer.*) are never rent and never a cost: they are shown apart (owner_in).
2. Cash flow, P&L, effort and vacancy (E15-2)¶
All amounts are exact cents, money in the API / tools is a decimal string. A closed month is covered when the account's data cover it entirely (the coverage model of E4); a month that is not is shown but not averaged and never counted as a vacancy.
- Monthly cash flow:
rent - loan - charges - fees - taxes - insurance - works - other. The loan is a CASH flow (principal included). The month's effort d'epargne is its shortfall,max(0, -net): what you had to add. The page shows the average of the last twelve complete months. - Yearly P&L (
coach rental pnl --year Y): the sum of the closed months of the year, the number of months expected and found, the months that are not fully covered, and the loan of the year split into interest, borrower insurance and principal from the E9 schedule. The economic result takes the principal out (rent - other costs - interest - borrower insurance). The gross yield on the declared value appears for a complete year. - Vacancy: once the property is let (the first rent, or the commitment start), a closed month is
received(at least half of the expected rent),partial,late_paid(no rent but the next month holds about two rents),declared_vacancy(a period you declared),unknown(data incomplete) ormissing(nothing explains it). The expected rent is the declaredrent_monthly, else the median of the rents seen. The month in progress ispendingand only becomeslateafter the usual rent day plus[analytics] rental_rent_grace_days(7).
3. The scheme commitment (E15-3)¶
The model is generic: the scheme's name is data (scheme), its length is commitment.years (6, 9 or 12 for a Pinel-type scheme: another number is accepted, a Pinel name with another number is a warning),
and the rent cap, the tenant income limit, the tenant's reference income and the reduction rate are what you typed from the deed or the scheme's own table.
- End date = the day before the anniversary of
commitment.start_date+years(orcommitment.end_date, which wins). With a recorded extension (extension.decision: extend,years), the effective end moves by those years. Check the date against your deed. - Reminders run while the extension decision is
undecided: from[analytics] rental_reminder_months(12) before the end, then at 6 and 3 months, and up to six months after the end. A recorded decision (extend or not extend,coach rental extensionor the Rental page) stops them. They are insight cards of kindrentaland alert events of kindscheme_end(low above six months, medium from six, high from three or once ended). - Rent cap:
rent_cap_monthly, orrent_cap_m2 x surface_m2, against the lease rent (rent_monthly; the rent received only when no lease rent is declared, since it may be net of fees). Above the cap = an insight card and an alert eventscheme_check. Tenant income:tenant_incomeagainsttenant_income_limit, same treatment. - Missing facts (the account, the loan, the value, the rent, the price and date, and for a property under a scheme the start, the length, the cap, the income limit, the reduction rate) are listed by
coach rental scheme, reported bymemory check, and generated as ONE open question per property (fill:rental:<id>,coach questions list); they resolve by themselves once the facts are recorded.
Alert content is LOCAL (the property id appears in the title and body). External channels (ntfy, e-mail, Telegram) keep their minimal vocabulary: the kind label ("Rental scheme commitment ending", "Rental scheme condition to check", "Rent not received"), a count and a severity, never an amount, an id or a figure (tested with sentinels).
4. The tax-year summary (E15-4)¶
coach rental tax [ID] --year Y (income year Y, declared in the spring of Y+1; default: the year to declare next), GET /rental/{id}/tax, the Tax year tab and the tax_candidates tool (a fr-revenus-fonciers
candidate per property, and the Pinel candidate computed from your declared price and rate when you gave them). France only; the Italian return is not modelled.
- Micro-foncier: the household's gross rents (all properties together, to compare with the ceiling) less the flat abatement: the taxable figure.
- Reel: gross rents less the costs the return lets you deduct: management fees, insurance, property tax, co-ownership charges, works, loan interest and borrower insurance from the amortization schedule of the linked loan (when the loan is unknown the interest is listed as UNKNOWN, never estimated, and the net is flagged as an upper bound). The loan principal and your own transfers are never deducted. Co-ownership charges, property tax and works are upper bounds (only part may be deductible: the syndic's statement and the invoices decide), and the interest is the theoretical table (report the lender's annual statement).
- The lower of the two is named, with the difference. Choosing the real regime has consequences over several years: verify with the tax office or an adviser.
- Scheme reduction (a candidate), ONE function (
rental/reduction.py) shared by the page, the tool andtax_candidates. The eligible price ispurchase_pricecapped by yourreduction_base_capand, for a Pinel, by 300,000 EUR and (whensurface_m2is recorded) 5,500 EUR/m2, whichever is lowest. The yearly shape, in order: yourreduction_schedule; for a Pinel the known split (6 and 9 years even at 2 % a year for the classic rates; 12 years: 9 years at the 9-year rate then 3 years at the extra rate, 21 % in all, i.e. 5,000 a year then 2,500 on 250,000 EUR; reduced rates for a 2023 or 2024 purchase keep the 9 + 3 pattern; datedLAST_REVIEWED, verify); otherwise yourreduction_rate_pctspread evenly, flagged as an ASSUMPTION. It starts atreduction_first_year(else the purchase year, else the commitment start); an extension with its ownadditional_rate_pctadds years. Outside the window the candidate is 0. It lowers the tax, it is not a deduction from the rental income. - Documents checklist (manager's annual statement, leases, the property tax notice, the syndic's statement with the recoverable split, insurance receipts, invoices, the lender's statement, bank statements; and for a scheme the deed, the scheme's own declaration, the tenant income evidence). The page lets you tick them for yourself (not saved).
The reel regime also shows two dated candidate lines: the EUR 20 flat management allowance per property, and, when the result is negative, the land deficit (EUR 10,700 a year imputable on the global income for the part not due to the loan interest, the interest part and the excess carried forward on later rental income). The ceiling (15,000 EUR) and the abatement (30 %) are those known when the module was written (taxyear.LAST_REVIEWED): verify them. Every result carries the wording of coach.disclaimers ("not tax advice, nothing is filed").
5. Renegotiate or sell (E15-5)¶
coach rental indicators [ID] [--market-rate R --market-date D --bank-fees N ...], GET /rental/{id}/indicators, the last tab.
- Loan rate vs market: the nominal rate of the loan against the market rate YOU entered (an argument, or
market_raterecorded on the property with its date). A rate at least[analytics] rental_rate_gap_pts(0.5) above it is flagged "a quote may be worth asking for" and priced by the renegotiation scenario of E9-5 on the real schedule (capital and months left, IRA penalty for a French mortgage, the fees you give, break-even). A market rate older than 30 days or without a date is flagged; none =missing, never invented. - End of the commitment: state, date, months left, whether a decision is pending; a neutral reading ("ending it early can call the advantage into question: verify with the scheme's rules").
- Net equity: the valuation you declared less the capital still due from the schedule (else the declared outstanding; an unknown loan makes the equity unknown), with the loan-to-value ratio. It is BEFORE selling costs, the early-repayment penalty and any tax on a capital gain: none of them is modelled.
- Scenarios stay where they are:
coach loans scenario renegotiate|prepay, themortgage_checkandwhat_iftools. No lender, product or insurer is ever named or recommended; the closing line is "General information, not financial advice".
6. For the coach (MCP) and the web app¶
rental_overview(property?, sections?, months?, year?, market_rate_pct?, market_rate_date?, bank_fees?, guarantee_fees?, other_fees?, penalty?)(read-only, E15-2..5): a property isasset-Nand the kind "rental property", its accountaccount-rental-N, its loanliability-Nand its kind, a payment a hashed ref; the scheme name goes through the household scrubber and arrives wrapped as untrusted text; the address, the property manager, the lender, the tenant and the notes are never read. Missing facts are listed. A model that wants to record one callsmemory_proposeorquestions_propose; it never writes.- The Rental page (
/rental): the cash flow chart and table with the rent status of each month, the vacancy and the P&L; the commitment with its reminders and the extension decision; the tax year; the indicators. Every write previews its diff first. Like the loans, the page is about the whole household: the person switch does not narrow it. A child login cannot reach it (deny by default). - Settings (
[analytics]):rental_reminder_months(12),rental_rent_grace_days(7),rental_rate_gap_pts(0.5).
Limits, stated honestly¶
- One property account per property (a shared account needs the
property-<id>tag); a property financed by several loans is supported but the rate indicator uses the first mortgage. - The P&L is as good as the categories of the flows and the coverage of the account: a payment in the wrong category moves every figure.
- The scheme's rules are not encoded: no rent cap table, no income-limit table, no reduction table (they change, and you own the deed). The coach checks YOUR figures against each other and reminds you of the dates.
- Rent paid net of the manager's fees makes the received rent lower than the gross rent of the return: the manager's annual statement is the figure to report.
- Nothing here was run on a real browser session; the figures were checked on synthetic data and on a scratch restore of the real data (aggregates only).