Year-End CAM Reconciliation and True-Ups
Year-end CAM reconciliation compares the monthly estimates a tenant paid against its finalized actual recoverable share, then issues a true-up — a balance owed or a credit. It is part of the expense allocation logic and rule engines calculation core, sitting at the moment when a year of estimated billing is settled against the ledger that actually closed. Throughout the operating year a tenant pays a monthly estimate — a twelfth of the landlord’s forecast of that tenant’s recoverable obligation — because nobody can invoice actual common area maintenance costs before the costs are incurred. When the fiscal year closes and the general ledger is final, the reconciliation reverses the assumption: it takes the real recoverable pools, runs them through the same allocation constraints that govern any billing, and produces the one number that matters to a tenant’s accounts payable department — the difference between what was collected and what was genuinely owed.
That difference is the true-up. When the estimate under-collected, the tenant owes a balance and receives a bill. When the estimate over-collected, the tenant is owed a credit that either offsets next year’s estimates or is refunded outright. The arithmetic looks trivial — one subtraction — but the number is only defensible if the actual share on the left of that subtraction was itself computed correctly: exclusions stripped, occupancy grossed up, area-weighted allocation applied, expense stops subtracted, and caps clamped, all in the right order and all frozen against an immutable snapshot of the period. Get the order wrong or let a closed period drift, and the true-up becomes a figure no auditor can trace and no tenant will pay without a fight.
Prerequisites & Data Contracts
A true-up is a settlement between two numbers, and each number arrives from a different part of the reconciliation system. Before any subtraction runs, the engine must hold four contracts in hand, all bound to the same reconciliation period and the same tenant.
The first is the estimated charges billed: the sum of the monthly estimates the tenant was actually invoiced across the period. This is not a forecast the engine recomputes — it is a historical fact read from the billing ledger, because the tenant’s cash position is defined by what was billed and paid, not by what should have been estimated in hindsight. A tenant that moved in during month seven has five monthly estimates, not twelve, and the contract carries the count so proration downstream stays honest.
The second is the actual recoverable pools, GL-mapped and final. These are the closed-year expense pools produced by GL code mapping for CAM expenses, each already classified into a recoverable, capital, or tenant-direct bucket. The reconciliation consumes only the recoverable buckets, and it consumes them as of the period’s close — a vendor credit memo that posts after close belongs to the next period, not this one.
The third and most consequential contract is the tenant’s finalized actual share. This is not a raw pro rata slice; it is the fully clamped figure that emerges from the pro-rata allocation algorithm after exclusions have been stripped, gross-up has normalized for occupancy, area-weighted allocation has run, expense stops have been subtracted, and caps have been applied. The true-up engine treats this number as authoritative and does not re-derive it. Its single responsibility is comparison, which keeps the settlement logic narrow and lets the allocation engine own the hard arithmetic.
The fourth is the reconciliation-period snapshot contract. At close, the engine freezes the period’s inputs — the GL snapshot identifier, the resolved rule versions, the area table, and the estimate ledger — into an immutable record keyed by a content hash. Every true-up references that snapshot. If a tenant disputes a figure eighteen months later, the snapshot is what lets the platform reproduce the exact actual share and the exact estimate total that fed the settlement, without depending on data that has since moved on.
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
@dataclass(frozen=True)
class ReconciliationSnapshot:
"""Immutable identity for one closed reconciliation period.
Every true-up cites this so a disputed figure remains reproducible
from the inputs and rule versions in force at close, never from
data that has since changed.
"""
period: str # e.g. "2025-FY"
gl_snapshot_id: str # content hash of the closed GL pull
rule_version_set: str # resolved allocation-rule versions
area_table_id: str # BOMA-measured RSF as frozen at close
Algorithm or Rule Design
The true-up itself is a single signed subtraction. Let be the tenant’s finalized recoverable obligation for the period and the sum of the monthly estimates billed. Then:
A positive result means the estimates fell short of the real obligation, so the tenant owes the balance and receives a bill. A negative result means the estimates over-collected, so the tenant is owed a credit. Zero — vanishingly rare in practice — means the forecast landed exactly. The sign convention is deliberate and load-bearing: downstream statement generation reads the sign to decide whether a line reads “balance due” or “credit,” so the engine never returns an unsigned magnitude.
The subtraction is trivial; the integrity of the result lives entirely in how was constructed. That figure is not a free number — it is the output of an ordered pipeline of clamps, and the order is not interchangeable. Applying a cap before a stop, or grossing up after allocating, produces a different and wrong obligation. The engine fixes the order as:
Read from the inside out, that is: first exclusions remove any cost the tenant negotiated out of its CAM obligation, shrinking the recoverable pool the tenant participates in. Then gross-up normalizes variable costs to a stated occupancy so an under-occupied building does not push fixed recoveries below what a full building would carry. Then allocation applies the tenant’s area-weighted pro rata factor to the normalized pool, producing the tenant’s gross share. Then the expense stop subtracts the base-year amount the landlord agreed to absorb, leaving only the increment above that floor. Finally the cap clamps the result so a tenant’s recoverable growth cannot exceed a contractual ceiling. Each stage consumes the output of the previous one; reordering them changes the arithmetic, not merely the presentation.
Proration is the last modifier and it multiplies, never reorders. A tenant that occupied its premises for only part of the period is responsible for only the fraction of the fully clamped share that corresponds to its occupancy window:
so a tenant present for 146 of 365 days carries of the annual clamped share. Critically, the same factor must already have shaped the estimated side: a tenant that moved in mid-year was billed estimates only for the months it occupied, so both sides of the subtraction cover the same window and the true-up settles a like-for-like comparison rather than a full year of actual against a partial year of estimate.
Python Implementation
The implementation keeps the estimate and actual sides in separate typed models so the settlement never confuses a billed cash fact with a computed obligation. All money is Decimal, quantized to cents once at the boundary, following the Python decimal module discipline the whole allocation layer relies on.
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP
CENTS = Decimal("0.01")
@dataclass(frozen=True)
class EstimatedBilling:
"""What the tenant was actually invoiced across the period."""
tenant_id: str
monthly_estimate: Decimal # a twelfth of the annual forecast
months_billed: int # 12 for a full-year tenant, fewer if partial
@property
def estimated_paid(self) -> Decimal:
total = self.monthly_estimate * Decimal(self.months_billed)
return total.quantize(CENTS, ROUND_HALF_UP)
@dataclass(frozen=True)
class ActualShare:
"""The finalized recoverable share after exclusions, gross-up,
allocation, expense stop, and cap have all been applied upstream."""
tenant_id: str
clamped_share: Decimal # already fully constrained by the engine
days_occupied: int
days_in_period: int
def prorated(self) -> Decimal:
"""Scale the clamped annual share to the occupancy window."""
factor = Decimal(self.days_occupied) / Decimal(self.days_in_period)
return (self.clamped_share * factor).quantize(CENTS, ROUND_HALF_UP)
@dataclass(frozen=True)
class TrueUp:
tenant_id: str
actual_share: Decimal
estimated_paid: Decimal
balance: Decimal # signed: positive = owes, negative = credit
@property
def tenant_owes(self) -> bool:
return self.balance > Decimal("0")
def compute_true_up(estimate: EstimatedBilling, actual: ActualShare) -> TrueUp:
"""Settle one tenant's period: actual share minus estimates paid.
A positive balance is billed to the tenant; a negative balance is a
credit. The actual share is prorated to the occupancy window so a
partial-year tenant is compared like-for-like against its partial
estimates.
"""
if estimate.tenant_id != actual.tenant_id:
raise ValueError("estimate and actual belong to different tenants")
prorated_actual = actual.prorated()
estimated_paid = estimate.estimated_paid
balance = (prorated_actual - estimated_paid).quantize(CENTS, ROUND_HALF_UP)
return TrueUp(
tenant_id=actual.tenant_id,
actual_share=prorated_actual,
estimated_paid=estimated_paid,
balance=balance,
)
The function does one thing: it prorates the already-clamped share, subtracts the billed estimates, and returns a signed Decimal. It deliberately refuses to re-derive the actual share, and it refuses to hide the sign — a caller that wants a magnitude can take abs(balance), but the engine’s contract is signed so statement generation can trust the direction.
Validation Rules & Edge Cases
The subtraction is safe; the edges around it are where reconciliations go wrong.
Mid-year move-in and move-out. A tenant that took occupancy on August 1 was billed five monthly estimates and occupied 153 of 365 days. Its clamped annual share must be scaled by 153 / 365 before comparison, and its estimated side already reflects five months rather than twelve. A move-out is the mirror image: a tenant that vacated on June 30 carries roughly half the annual share and six estimates. The failure mode to guard against is comparing a prorated actual against a full year of estimates, which fabricates a large phantom credit; the models above prevent it by carrying occupancy days and months billed as first-class fields.
A cap that limits the true-up. When a controllable-expense cap clamps the actual share below the raw allocated amount, the true-up shrinks accordingly — sometimes to a credit even though the raw costs rose. Because the cap is applied upstream, inside , the true-up engine never sees the uncapped figure and cannot accidentally bill it. This is intentional: the cap protects the tenant regardless of what the estimates assumed, and the stateful mechanics of that ceiling live in managing expense caps and controllable limits.
Negative pools and credits. A recoverable pool can go negative when a vendor issues a refund or a prior over-accrual reverses — a snow-removal contractor credits an over-billed month, say. A negative pool produces a smaller or negative actual share and, correctly, a larger tenant credit. The engine must not floor the pool at zero; doing so would silently keep money the tenant is owed. Decimal arithmetic carries the sign through cleanly.
Prior-year adjustments landing in the current true-up. Sometimes a correction to a closed year — an invoice that arrived late and belongs to last period — must be recovered from tenants. It never mutates the closed period. Instead it lands as a discrete, forward-dated adjustment line in the current true-up, clearly labeled so the tenant sees it did not originate this year. That line becomes its own entry when tenant statement generation and dispute routing renders the settlement, keeping the current-year math and the prior-year correction visually and auditably separate.
Never mutating a closed period. This is the discipline that makes everything else defensible. Under FASB ASC 842, CAM recoveries are recognized in the period the service was delivered, and a closed period is an immutable snapshot. When a figure needs correcting after close, the engine writes a new adjustment entry against a live period — it never edits the historical allocation or its snapshot hash. A reconciliation that rewrites last year’s numbers to make this year’s true-up tidy has corrupted recognized revenue and invited a restatement.
from decimal import Decimal
class ClosedPeriodError(Exception):
"""Raised on any attempt to mutate a settled reconciliation period."""
def guard_closed_period(snapshot_status: str) -> None:
"""A closed period accepts new forward-dated adjustments only, never
edits to historical shares or their audit fingerprints."""
if snapshot_status == "closed":
raise ClosedPeriodError(
"period is closed; post a forward-dated adjustment instead"
)
Integration Points
The true-up is a hinge between the allocation engine that produces the actual share and the statement layer that communicates the result to the tenant. On the input side, the finalized arrives fully constrained: its cap ceiling is set by managing expense caps and controllable limits, and its base-year floor is subtracted according to modeling expense stops and base-year clauses. The true-up engine never reaches back into either — it consumes their combined output, which keeps the settlement logic small and the responsibility boundaries clean.
On the output side, every TrueUp becomes a line on the tenant statement. The statement reads the sign to choose “balance due” or “credit,” renders the actual share and the estimates paid side by side so the tenant can see the derivation, and attaches the reconciliation snapshot so an audit request resolves without a scramble. The clean separation matters operationally: because the true-up carries the snapshot reference, a dispute routed back from the statement layer lands on a reproducible number rather than a figure that has to be reconstructed from memory.
Testing & Verification
Because the true-up is where a year of billing is settled, its tests assert both direction and magnitude, not merely that a number came out.
from decimal import Decimal
def test_underestimate_bills_tenant() -> None:
est = EstimatedBilling("t-100", Decimal("1200.00"), months_billed=12)
act = ActualShare("t-100", Decimal("16800.00"), days_occupied=365, days_in_period=365)
tu = compute_true_up(est, act)
# actual 16,800 - estimated 14,400 = 2,400 owed
assert tu.balance == Decimal("2400.00")
assert tu.tenant_owes is True
def test_overestimate_credits_tenant() -> None:
est = EstimatedBilling("t-100", Decimal("1500.00"), months_billed=12)
act = ActualShare("t-100", Decimal("16200.00"), days_occupied=365, days_in_period=365)
tu = compute_true_up(est, act)
# actual 16,200 - estimated 18,000 = -1,800 credit
assert tu.balance == Decimal("-1800.00")
assert tu.tenant_owes is False
def test_mid_year_move_in_prorates_both_sides() -> None:
# Occupied 153 days (Aug 1 onward), billed 5 monthly estimates.
est = EstimatedBilling("t-205", Decimal("900.00"), months_billed=5)
act = ActualShare("t-205", Decimal("11000.00"), days_occupied=153, days_in_period=365)
tu = compute_true_up(est, act)
# prorated actual 11,000 * 153/365 = 4,610.96 ; estimates 4,500.00
assert tu.actual_share == Decimal("4610.96")
assert tu.balance == Decimal("110.96")
def test_cap_clamped_share_yields_smaller_true_up() -> None:
# Uncapped costs would have implied ~19,000; the cap held the share
# to 15,000, so the true-up is a credit despite rising raw expenses.
est = EstimatedBilling("t-310", Decimal("1350.00"), months_billed=12)
act = ActualShare("t-310", Decimal("15000.00"), days_occupied=365, days_in_period=365)
tu = compute_true_up(est, act)
assert tu.balance == Decimal("-1200.00")
assert tu.tenant_owes is False
Beyond per-tenant assertions, the run enforces a portfolio-level tie-out: for a given pool, the sum of every tenant’s true-up plus the sum of every tenant’s estimated payments must equal the total actual recoverable amount distributed. Formally, . This is the reconciliation’s closing invariant — it proves no dollar of recoverable cost was created or lost between estimate and settlement, and it fails the run loudly if a rounding residual or a dropped tenant breaks the balance.
from decimal import Decimal
def assert_period_ties_out(
true_ups: list[TrueUp], total_actual_recoverable: Decimal
) -> None:
"""Sum of true-ups plus estimates must equal actual recoverable."""
settled = sum((t.actual_share for t in true_ups), Decimal("0"))
expected = total_actual_recoverable.quantize(Decimal("0.01"))
if settled != expected:
raise AssertionError(
f"tie-out failed: settled {settled} != recoverable {expected}"
)
Frequently Asked Questions
Why does a true-up happen at all — why not just bill the actual cost each month? Because the actual recoverable cost of a fiscal year is not knowable until the year closes and the general ledger is final. Tax bills, insurance renewals, and variable maintenance land unevenly, and many are invoiced in arrears. The landlord bills a monthly estimate to keep cash flowing against the tenant’s obligation, then reconciles once the real pools are settled. The true-up is the honest settlement of an unavoidable forecast.
When does the true-up get issued, and how long does a tenant have to pay? Reconciliation runs after the fiscal year closes and the ledger is final — commonly within 90 to 120 days of year-end, though the exact window is a lease term. The statement then states a payment date for a balance owed or the mechanism for a credit, which is typically applied against the next estimate cycle or refunded. Because the actual share cites an immutable period snapshot, a tenant exercising an audit right can trace every figure regardless of when the request arrives.
What happens to the true-up if a tenant left partway through the year? Both sides of the subtraction are scaled to the occupancy window. The tenant was billed estimates only for the months it occupied, and its clamped annual share is multiplied by the fraction of days it was in the premises. The engine compares a partial-year actual against partial-year estimates, so a departed tenant is settled for exactly its period of responsibility and no more.
Can an expense cap create a shortfall the landlord has to absorb? Yes. When a controllable-expense cap clamps a tenant’s recoverable share below the cost actually allocated to that tenant, the capped increment is not recoverable from anyone — the landlord absorbs it. The true-up settles only against the capped share, so the tenant is never billed above its ceiling, and the unrecovered remainder is a known, contractual cost of the cap rather than a reconciliation error.
Where This Fits in the Reconciliation System
Year-end reconciliation is the moment the allocation engine’s precision becomes money changing hands. Every clamp the engine applied during the year — the exclusions, the gross-up, the area-weighted allocation, the expense stop, the cap — converges into a single finalized actual share, and the true-up settles that share against a year of estimates in one signed subtraction. The value is not in the arithmetic but in its defensibility: because the actual share is built by the pro-rata allocation algorithm and frozen against an immutable snapshot, and because the result flows into tenant statement generation and dispute routing with its derivation intact, a true-up survives the scrutiny that turns a routine bill into a paid one. Settle it carelessly and it invites a dispute; settle it against versioned, reproducible inputs and it closes the year cleanly.
Related
- Automating CAM True-Up Calculations in Python — the batch machinery that computes signed true-ups across a whole tenant roster and posts them.
- Reconciling Estimated vs Actual CAM Charges — the estimate-versus-actual comparison and tie-out that underpins every true-up figure.
- Tenant Statement Generation and Dispute Routing — how a settled true-up becomes a defensible statement line and how a challenged one is routed back.
- Managing Expense Caps and Controllable Limits — the ceiling that clamps the actual share before the true-up ever sees it.