Tracking Contingency Utilization Against a Drawdown Schedule
A completion guarantor does not approve a lump-sum contingency and look away; it approves a drawdown schedule that phases the contingency across periods, and it measures the production against that schedule at every reporting point. The exact problem this page solves is narrow and unforgiving: given the authorized per-period allowance for each cost category and a list of approved draws already applied, reconcile cumulative utilization against the schedule, decide for each category and period whether the draws are on schedule or ahead of it, and emit an audit record whose numbers a guarantor can reproduce to the cent months later. A running total in a spreadsheet answers “how much is left” but not “are we drawing faster than we said we would” — and it is the second question that predicts a covenant test failing. This walkthrough builds the reconciliation as a pure function over a validated draw list with Decimal money throughout, as one focused procedure inside Contingency Drawdown Tracking.
Prerequisites and Context
This page assumes the draws have already been authorized, deduplicated, and cost-coded by the engine documented in Contingency Drawdown Tracking; it focuses entirely on the period-by-period reconciliation and the audit trail around it. It targets Python 3.11+ for standard-library zoneinfo, and uses the same deliberate dependency set as the rest of Completion Bond Reporting & Guarantor Analytics: decimal for every monetary value, zoneinfo for timezone-aware timestamps (never a bare UTC offset), hashlib for the payload fingerprint, and Pydantic v2 (model_validate, field_validator) for the boundary schema. Draws reach this function already parsed by the upstream Cost Ingestion & Data Parsing Workflows subsystem, and each draw keys to exactly one budget category through Cost Code Standardization. The parameter that matters is the drawdown schedule itself: because the phasing and the covenant thresholds are set in the specific bond agreement, treat the per-period allowance and the utilization ceiling as configurable inputs rather than hardcoded constants.
Step-by-Step Implementation
Reconciliation begins at the boundary. A Pydantic v2 model rejects any draw whose amount is a float or non-positive, because a contaminated amount is the single largest source of silent utilization drift. The reconciliation is then a pure function that folds the approved draws into cumulative totals keyed by (category, period), compares each running total against the scheduled allowance, and classifies the result — all offset-invariant arithmetic that a guarantor can recompute from the archived draws alone.
from __future__ import annotations
import hashlib
import json
from collections import defaultdict
from datetime import datetime, timezone
from decimal import Decimal, ROUND_HALF_UP
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
# Utilization band thresholds, as a fraction of the period allowance.
ON_SCHEDULE = Decimal("1.00")
AHEAD_WARNING = Decimal("1.15") # >15% over the period's allowance
def _quantize(amount: Decimal) -> Decimal:
return amount.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
class ApprovedDraw(BaseModel):
"""One authorized, deduplicated contingency draw."""
model_config = ConfigDict(frozen=True)
draw_id: str
category: str
period: str # schedule period, e.g. "2026-W29"
amount: Decimal = Field(gt=Decimal("0"))
@field_validator("amount", mode="before")
@classmethod
def _reject_float(cls, v: Any) -> Decimal:
if isinstance(v, float):
raise ValueError("amount must be str/Decimal, never float")
return Decimal(str(v))
def reconcile(
draws: list[ApprovedDraw],
schedule: dict[tuple[str, str], Decimal],
) -> dict[str, Any]:
"""Reconcile approved draws against the per-period scheduled allowance."""
drawn: dict[tuple[str, str], Decimal] = defaultdict(lambda: Decimal("0"))
# Deterministic order: identical inputs always fold to identical totals.
for draw in sorted(draws, key=lambda d: (d.category, d.period, d.draw_id)):
drawn[(draw.category, draw.period)] += draw.amount
lines = []
for key in sorted(schedule.keys() | drawn.keys()):
category, period = key
used = drawn.get(key, Decimal("0"))
allowance = schedule.get(key, Decimal("0"))
# Ratio only means something when an allowance was actually scheduled.
if allowance > 0:
ratio = (used / allowance).quantize(
Decimal("0.0001"), rounding=ROUND_HALF_UP
)
else:
ratio = None
if allowance == 0 and used > 0:
status = "unscheduled_draw"
elif ratio is None or ratio <= ON_SCHEDULE:
status = "on_schedule"
elif ratio <= AHEAD_WARNING:
status = "ahead_of_schedule"
else:
status = "over_period_allowance"
lines.append({
"category": category,
"period": period,
"drawn": str(_quantize(used)),
"allowance": str(_quantize(allowance)),
"utilization_ratio": None if ratio is None else str(ratio),
"status": status,
})
payload = {
"reconciled_at": datetime.now(tz=timezone.utc).isoformat(),
"total_drawn": str(_quantize(sum(drawn.values(), Decimal("0")))),
"total_scheduled": str(_quantize(sum(schedule.values(), Decimal("0")))),
"lines": lines,
}
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
payload["payload_sha256"] = hashlib.sha256(canonical).hexdigest()
return payload
Because reconcile sorts its inputs before folding and never touches the network, identical draws always produce an identical payload and an identical hash. That determinism is what lets a guarantor’s auditor recompute a disputed period from the archived draws alone. The status per line is the actionable output: on_schedule needs no attention, ahead_of_schedule is an early caution, and over_period_allowance or unscheduled_draw is the exception a production accountant surfaces before the guarantor finds it independently.
The reconciliation flow validates each draw, folds the approved draws into cumulative totals by category and period, and compares each total against the scheduled allowance before classifying the line.
Audit Trail Requirements
Every reconciliation — including a clean run where every category is on schedule — must be serialized to a write-once store so a guarantor’s examiner sees an unbroken record rather than only the exceptions. At minimum the persisted payload must carry: the reconciled_at UTC timestamp; the total_drawn and total_scheduled figures as strings; and, per line, the category, period, cumulative drawn, scheduled allowance, the utilization_ratio, and the resolved status; closed by the payload_sha256 fingerprint of the canonical payload. Persist the record to append-only, write-once storage — object storage with an immutability lock, or an event-sourced table with no in-place update — before any downstream report is published, so a crash mid-run leaves a replayable record of intent rather than a silent gap. A later correction is a new reconciliation record, never an edit of the posted one, so the figures a covenant certificate was built on stay recoverable. This is the same write-once discipline specified for the underlying draws in Contingency Drawdown Tracking and the ledger design in Audit Logging & Immutability, which is why the hashing belongs in one shared module rather than reimplemented per report.
Gotchas and Production Edge Cases
Idempotent draw application. The reconciliation folds a list of draws, so the same draw appearing twice — from a retried batch, a resubmitted memo, or an at-least-once queue — inflates utilization silently. Deduplicate on draw_id (or the payload hash) before folding, and treat the reconciliation as a pure function of a deduplicated set, so re-running it after a partial failure produces the identical payload rather than a double-counted total. Folding in sorted order, as the code does, is what makes that re-run bit-for-bit reproducible.
Period-boundary assignment. A draw approved at the tail of one reporting week but recorded in the next distorts both periods: the first looks under-utilized, the second over. The period a draw belongs to is the one the guarantor’s schedule uses, not the wall-clock date the record happened to be entered, and that assignment must be made once, upstream, and carried on the draw — never inferred at reconciliation time from a timestamp. When periods are calendar weeks, pin them to an explicit ISO week key like 2026-W29 rather than a raw date, so a shoot straddling a week boundary lands every draw in exactly one bucket.
Unscheduled categories. A draw against a category and period with no scheduled allowance is not automatically an error — a mid-shoot reallocation can legitimately open one — but it must never be silently rolled into a neighbouring bucket. The reconciliation surfaces it as a distinct unscheduled_draw status so a production accountant confirms the reallocation with the guarantor rather than letting the schedule and the ledger quietly diverge.
Float contamination. Never build a Decimal from a float literal — Decimal(0.1) imports the binary rounding error, and a single fractional cent compounded across a season of draws becomes the variance a guarantor asks you to explain. Read every amount through its string form, as Decimal(str(...)) above, and quantize to cents only at the point a figure becomes reportable.
Ratio on a zero allowance. Dividing drawn by a scheduled allowance of zero raises or, worse, silently yields a misleading infinity in a naive implementation. Guard the denominator explicitly, as the code does, and represent “no allowance scheduled” as a None ratio with an explicit status rather than a number the report cannot interpret. The forward-looking projection that turns these ratios into a cutback warning is developed in Flagging Over-Budget Categories Before a Bond Cutback.
Related Guides
- Contingency Drawdown Tracking — the parent engine that authorizes, deduplicates, and ledgers the draws this reconciliation consumes.
- Flagging Over-Budget Categories Before a Bond Cutback — the projection that turns a utilization ratio into an early cutback warning.
- Cost-to-Complete Projections — the estimate-at-completion view that decides whether the remaining contingency is actually sufficient.
- Guarantor Variance Reporting — where these utilization lines roll up into the variance report a guarantor reviews.
- Audit Logging & Immutability — the write-once, hash-stamped storage discipline this reconciliation persists into.
Up one level: Contingency Drawdown Tracking, part of Completion Bond Reporting & Guarantor Analytics.