EYThe LogEmre Yakut
← all entries
Portals · Systems · deep

I rewrote the portal that shows the bill

Someone living in a flat wants to see their heating bill. Sounds simple. But the bill lives in another system, the meter in another table, and a tenant who moved out must have their history frozen. That is why I rewrote it from scratch.

CRM faturaların kaynağı sor cevap portal önbelleğe yazar sakin faturasını görür PULL: kaynak portalı hiç tanımaz — veri hiç farklılaşmaztaşınan kiracı: visible_from … visible_until → yalnız oturduğu dönemrelation_frozen = 1 → CRM’e BİR DAHA HİÇ SORULMAZfiltreyi biri unutur; yapılmamış bir sorguyu kimse unutamaz

The last link in heat cost allocation is this: a person in a flat receives a bill and asks “where did this number come from?”

The place that answers is a portal. One existed, but it sat on a ten-year-old e-commerce codebase and every new requirement strained it further. I rewrote it: app.brunatazenner.tr.

What it shows

  • Bills — line by line: heat share, common areas, water, fixed charges. Downloadable as PDF.
  • A daily consumption chart — meter readings with the day’s outdoor temperature on top. Side by side, “the winter was cold” stops being a claim and becomes a graph.
  • Comparison with the building average — was it you, or the building? Half the incoming questions end at this box.
  • Requests — a simple ticket system with messages.
  • Household sub-users — so a partner or child can log in on their own.

There is also a separate site manager panel: blocks and units, bulk SMS to residents, announcements, incoming requests.

The real architectural decision: PULL

Bills are not produced in the portal. They are produced in the CRM — period by period, in their own databases.

Two options:

PUSHPULL
HowCRM sends to the portal when a bill is approvedPortal asks the CRM when it needs to
CRM must know the portalyesno
If the portal is downthe send is lost, you need a queuenothing happens
Data driftinevitable over timethe source is always right

I chose PULL. When the portal needs a bill list it calls the CRM endpoint, takes the answer and writes it to its own table (cache-aside). Next time it reads from cache.

When the cache is refreshed it does “purge then insert”, not “upsert”. The reason: if a bill was cancelled in the CRM, an insert-only strategy would never remove it from the portal. The resident would keep seeing a cancelled bill for months.

The tenant-who-moved-out problem

This is the part I thought hardest about, and it is not a technical problem but an ethical one.

A tenant moves out in March. They must still be able to see February’s bill — they lived there, they paid it. But they must not see April’s; the flat is now somebody else’s and so is the consumption in it.

Two mechanisms:

relation: user ↔ unit
  visible_from    = date they moved in
  visible_until   = date they left        ← nothing after this
  relation_frozen = 1                     ← never ask the CRM again

The visibility window decides which periods they can see. The frozen relation is stronger: the system never calls the CRM for that relation again, and shows only what was already cached.

Without the second, a bug or a mismatched record could let a former tenant see the new tenant’s data. With a frozen relation that is physically impossible — the query is never made.

the principle

Building authorisation as a show/hide filter is not enough. What is actually safe is never fetching the data. Someone will eventually forget a filter; nobody can forget a query that was never written.

The technology side

I used no framework, but I did not write everything either. Three pieces borrowed, the rest mine:

routing   : nikic/fast-route
templates : Twig
database  : PDO + a thin wrapper
PDF       : mpdf  (renders the bill from structured data, not a screenshot of HTML)
env       : phpdotenv  (secrets outside the web root)

The old database is used unchanged. Table names, prefixes, even the legacy password hashing were kept — users log in with their old passwords and are silently upgraded to a modern hash on first login. Not having to reset anyone’s password while replacing a portal is a bigger win than it sounds.

And a demo mode

The portal has to be shown in sales meetings. You cannot open a real resident’s bill.

So there is /demo: a full account — 24 months of consumption, three locations, realistic bills. All generated data, and the demo account never calls the CRM (the second use for the frozen-relation mechanism).

What I learned

The hard part of a portal is not the screens. It is who may see what — and that question is almost never solved by roles. It changes with time, with moves, with cancellations, with corrections.

Modelling visibility as a date range was, for me, the most valuable decision in this project.

PortalsSystems