Pension & Health Fund Calculations: Deterministic Fringe Automation for Union Payroll
Pension and health (P&H) contributions are among the most legally sensitive and financially volatile line items in any film or television budget. They are not flat percentages of gross pay: they scale with compensation thresholds, jurisdictional mandates, and work classification, and each guild’s trust fund enforces a contribution schedule that changes with every collective bargaining cycle. A miscalculated fringe accrual does not stay hidden — it surfaces as an audit flag from a union trust fund, a held payment, or a completion guarantor refusing to release a bond until the remittance discrepancy is reconciled. Spreadsheet-driven approximation cannot meet that bar, because a floating-point cent that drifts across thousands of contribution lines becomes exactly the variance an auditor asks you to explain. This page specifies a deterministic, code-enforced P&H calculation engine — its inputs, its architecture, its production Python, the collective bargaining agreements it must honor, and the quarantine and verification mechanics that make every number defensible — as one subsystem of the broader Guild Compliance & Rule Validation Automation reference architecture.
Prerequisites and Expected Inputs
The implementation targets Python 3.11+, both for zoneinfo in the standard library and for modern union-type syntax. It leans on a deliberately small stack: Pydantic v2 for boundary schema validation through model_validate and field_validator; the standard-library decimal, hashlib, and zoneinfo modules for currency-safe arithmetic, deterministic audit hashing, and timezone-aware work dates; and, in production, a signature library such as cryptography to verify that a cached trust schedule was ratified before it is applied to a contribution. Never use float for monetary values — as the official Python decimal module documentation makes explicit, only an explicit precision and rounding context guarantees the deterministic output a trust fund’s remittance specification demands.
The engine assumes its inputs are already clean. P&H records do not originate here: heterogeneous timecards, deal-memo parameters, and payroll exports are normalized upstream by the Cost Ingestion & Data Parsing Workflows subsystem, where Async Batch Processing absorbs vendor API rate limits and Schema Validation & Error Handling quarantines malformed payloads before they ever reach a rule engine. Each hour is keyed against the taxonomy defined in Cost Code Standardization, so a validated line maps to exactly one fund code, one department, and one budget line. What the pension and health engine adds on top of that contract is a single guarantee: the contribution applied to any validated line is always resolvable to a version-stamped trust schedule and a reproducible payload hash, even under a volatile multi-jurisdiction shooting schedule.
The primary inputs the engine consumes are a payroll transaction — employee, classification, jurisdiction, gross compensation, and the timezone-aware date the work was performed — and a trust schedule keyed by guild classification and agreement version. The output is a resolved contribution pair (pension and health) plus a provenance record: the pensionable base actually used, the agreement version that produced the rates, and the SHA-256 hash of the canonical payload.
Architecture: Normalize, Determine the Pensionable Base, Then Apply the Schedule
The engine is a strict pipeline, not an ad-hoc formula. A validated transaction is first canonicalized and hashed so its provenance is fixed before any arithmetic runs. The classification then resolves a version-stamped trust schedule; the schedule’s pensionable ceiling caps the contributory base, because most guild agreements stop accruing pension above a per-engagement earnings cap. Only then are the pension and health rates applied to that capped base, each result quantized to whole cents under a deterministic rounding context. Every successful computation writes an immutable audit entry; every failure that reflects a bad record — an unknown classification, a negative gross, an unmapped jurisdiction — diverts to a quarantine queue rather than silently accruing zero. The distinction matters to a completion guarantor: a resolved contribution and a quarantined exception are different states, and conflating them is how a payroll ledger quietly goes wrong.
The diagram below shows the contribution path from gross wages through schedule lookup to an immutable audit entry, including the fallback queue branch that fires when validation fails.
Where the resolution of a missing schedule is concerned — a rate table that simply is not present when the close runs — the tiered routing that keeps payroll moving is specified separately in Compliance Fallback Chains. This engine assumes a schedule either resolves cleanly or is a validity failure to quarantine; it delegates availability failures to that chain.
Core Implementation
The implementation proceeds in three steps: define the validated boundary models, canonicalize and hash each transaction, then compute the capped contribution against a version-stamped schedule.
Step 1 — Model the trust schedule and the transaction as validated Pydantic v2 types. Rates are constrained to the [0, 1] interval at the boundary, gross compensation may not be negative, and the work date must be timezone-aware — an offset-naive datetime is rejected outright, because a P&H rate that changes on a ratification date cannot be resolved from an ambiguous local timestamp.
Step 2 — Fix provenance before arithmetic. The transaction is serialized to a canonical, key-sorted JSON form and hashed with SHA-256. That hash is computed from the same bytes every time, so re-running a disputed week yields an identical fingerprint — the idempotency property that lets an accountant replay the close deterministically.
Step 3 — Cap the base, apply the schedule, quantize to cents. The pensionable base is the lesser of gross compensation and the agreement’s pensionable ceiling; each contribution is base × rate quantized to 0.01 under ROUND_HALF_UP.
from __future__ import annotations
import hashlib
import json
from datetime import datetime
from decimal import Decimal, ROUND_HALF_UP, getcontext
from enum import Enum
from zoneinfo import ZoneInfo
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
# Deterministic context for all fringe arithmetic — never float for money.
getcontext().prec = 34
CENTS = Decimal("0.01")
PRODUCTION_TZ = ZoneInfo("America/Los_Angeles")
class Classification(str, Enum):
SAG_PERFORMER = "SAG_PERFORMER"
DGA_DIRECTOR = "DGA_DIRECTOR"
WGA_WRITER = "WGA_WRITER"
class TrustSchedule(BaseModel):
"""One guild trust-fund contribution schedule, version-stamped for audit."""
model_config = ConfigDict(frozen=True)
classification: Classification
pension_rate: Decimal
health_rate: Decimal
pensionable_ceiling: Decimal # per-engagement cap on the contributory base
agreement_version: str # e.g. "SAG-AFTRA-2023-Codified-Basic"
@field_validator("pension_rate", "health_rate")
@classmethod
def _rate_is_fraction(cls, v: Decimal) -> Decimal:
if not (Decimal("0") <= v <= Decimal("1")):
raise ValueError("contribution rate must be a fraction between 0 and 1")
return v
class PayrollTransaction(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
employee_id: str = Field(min_length=1)
classification: Classification
jurisdiction_code: str = Field(min_length=2, max_length=2)
gross_compensation: Decimal
worked_at: datetime
@field_validator("gross_compensation")
@classmethod
def _non_negative(cls, v: Decimal) -> Decimal:
if v < 0:
raise ValueError("gross compensation cannot be negative")
return v
@field_validator("worked_at")
@classmethod
def _tz_aware(cls, v: datetime) -> datetime:
if v.tzinfo is None:
raise ValueError("worked_at must be timezone-aware (use an IANA zone)")
return v
def audit_hash(self) -> str:
canonical = json.dumps(
{
"employee_id": self.employee_id,
"classification": self.classification.value,
"jurisdiction_code": self.jurisdiction_code,
"gross_compensation": str(self.gross_compensation),
"worked_at": self.worked_at.astimezone(PRODUCTION_TZ).isoformat(),
},
sort_keys=True,
separators=(",", ":"),
)
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
class ContributionResult(BaseModel):
audit_hash: str
agreement_version: str
pensionable_base: Decimal
pension: Decimal
health: Decimal
status: str
computed_at: datetime
class PensionHealthCalculator:
def __init__(self, schedules: dict[Classification, TrustSchedule]):
self._schedules = schedules
self.quarantine: list[dict] = []
def compute(self, txn: PayrollTransaction) -> ContributionResult:
schedule = self._schedules.get(txn.classification)
if schedule is None:
return self._quarantine(txn, "no_trust_schedule_for_classification")
# Pension stops accruing above the agreement's per-engagement ceiling.
pensionable_base = min(txn.gross_compensation, schedule.pensionable_ceiling)
pension = (pensionable_base * schedule.pension_rate).quantize(CENTS, ROUND_HALF_UP)
health = (pensionable_base * schedule.health_rate).quantize(CENTS, ROUND_HALF_UP)
return ContributionResult(
audit_hash=txn.audit_hash(),
agreement_version=schedule.agreement_version,
pensionable_base=pensionable_base,
pension=pension,
health=health,
status="calculated",
computed_at=datetime.now(PRODUCTION_TZ),
)
def _quarantine(self, txn: PayrollTransaction, reason: str) -> ContributionResult:
self.quarantine.append({
"audit_hash": txn.audit_hash(),
"reason": reason,
"payload": txn.model_dump(mode="json"),
"quarantined_at": datetime.now(PRODUCTION_TZ).isoformat(),
"requires_manual_override": True,
})
return ContributionResult(
audit_hash=txn.audit_hash(),
agreement_version="UNRESOLVED",
pensionable_base=Decimal("0.00"),
pension=Decimal("0.00"),
health=Decimal("0.00"),
status="quarantined",
computed_at=datetime.now(PRODUCTION_TZ),
)
def ingest(raw: dict) -> PayrollTransaction | None:
"""Boundary parse: malformed records are logged, never silently coerced."""
try:
return PayrollTransaction.model_validate(raw)
except ValidationError as exc:
# In production, push exc.errors() to the reconciliation queue.
print("rejected at boundary:", exc.error_count(), "issue(s)")
return None
Two properties of this code are load-bearing for audit defensibility. First, gross_compensation and every rate are Decimal, and min(...) on Decimal values preserves that type — no float ever touches a monetary quantity. Second, worked_at is normalized to a single production timezone via zoneinfo before it enters the hash, so the same shoot day always canonicalizes identically regardless of the offset the source system emitted.
Guild and Contract Specifics
The three classifications above stand in for the trust funds a scripted production touches most often, and each carries its own contribution schedule negotiated in a distinct collective bargaining agreement (CBA). For performers, the Screen Actors Guild-American Federation of Television and Radio Artists (SAG-AFTRA) sets pension and health rates as a percentage of covered earnings up to a contract ceiling; the details of segregating pensionable from non-pensionable compensation before remittance are worked through in Automating SAG-AFTRA Pension Contributions Tracking. For directors and unit production managers, the Directors Guild of America (DGA) ties pensionable earnings to hours worked relative to standard turnaround windows, so overtime premiums can push gross pay across a contribution tier boundary — which is exactly why the base must be computed after the penalties defined in DGA Overtime & Turnaround Rules are applied, not before. For writing staff, the Writers Guild of America (WGA) validates tiered compensation against current guild minimums, and secondary payments interact with reuse: whether pension applies to a residual at all is resolved by the reuse-window logic in SAG-AFTRA Residuals Logic before this engine ever sees the base. Crew health-and-welfare caps under the International Alliance of Theatrical Stage Employees (IATSE) follow the same modeling: a per-hour or per-engagement ceiling that bounds the contributory base.
The rate values in the code are illustrative placeholders. In production, the schedule is loaded from a version-controlled, signed source — never hardcoded — because contribution rates and ceilings change with every bargaining cycle, and a stale rate applied to a live remittance is itself an audit finding. The agreement_version field exists precisely so that the schedule a contribution was computed against is recoverable months later, when a trust fund questions a specific line.
Error Handling and Quarantine
Not every failure is the same kind of failure, and the engine must not blur them. A malformed inbound record — a non-numeric gross, a two-letter jurisdiction code that is actually four characters, an offset-naive worked_at — is rejected at the boundary by model_validate before it can become a transaction at all; ingest surfaces the structured ValidationError for reconciliation rather than coercing a guess. A structurally valid transaction whose classification has no schedule is a different case: it is a real record that cannot yet be priced, so it is quarantined, not dropped and not zero-accrued.
Every quarantine event serializes the original payload verbatim via model_dump(mode="json"), attaches the SHA-256 hash of the canonical transaction, records a machine-readable reason code, and flags the record for manual override. The failing record never enters the ledger as a resolved contribution — it enters a separate exception store, so the payroll ledger stays clean while the discrepancy is preserved for triage. This mirrors the boundary discipline of Schema Validation & Error Handling: a rejected record always carries its reason code, its original bytes, and its hash, so a production accountant can reconcile a single line without re-ingesting the whole weekly batch. The engine never silently defaults to zero, because a silent zero is indistinguishable, at audit time, from a legitimately zero contribution — and that ambiguity is what a completion guarantor will not accept.
Verification
A contribution engine is only trustworthy if you can prove, after the fact, how every number was produced. Verification checks three artifacts.
First, the ledger entry. Every calculated result must carry a pensionable_base that equals min(gross, ceiling), a pension and health that are exact Decimal values quantized to two places, and an agreement_version that names the schedule applied. Re-running compute against the same transaction must yield an identical audit_hash — the idempotency check that lets a disputed week be replayed deterministically.
Second, the audit log fields. Each computation should log, at minimum: employee_id, classification, jurisdiction_code, agreement_version, pensionable_base, pension, health, audit_hash, and a timezone-aware computed_at. A trust-fund examiner reads this row as the provenance of the remittance, so no field is optional.
Third, the reconciliation report shape. A weekly report should surface, per fund code: total pensionable base, total pension and health accrued, the count of records that hit the pensionable ceiling (a signal of high-earner concentration), and every quarantined record awaiting triage. A healthy run is almost entirely calculated with a short, explained quarantine tail; a run with a growing quarantine list is a signal that a classification mapping or a schedule needs attention before the remittance deadline rather than after a trust fund flags it.
A minimal harness confirms the invariants:
def verify(result: ContributionResult, txn: PayrollTransaction,
schedule: TrustSchedule) -> None:
expected_base = min(txn.gross_compensation, schedule.pensionable_ceiling)
assert result.pensionable_base == expected_base, "base must respect the ceiling"
assert result.pension == (expected_base * schedule.pension_rate).quantize(CENTS, ROUND_HALF_UP)
assert result.audit_hash == txn.audit_hash(), "resolution must be idempotent"
assert result.computed_at.tzinfo is not None, "timestamps must be timezone-aware"
Enforced this way — Decimal fringe math, a version-stamped schedule, a capped pensionable base, and a reproducible payload hash on every line — a P&H calculation engine turns a volatile, audit-prone line item into a bond-ready, defensible pipeline that scales across multi-jurisdiction shoots, mixed union rosters, and compressed close schedules.
Related
- Guild Compliance & Rule Validation Automation — the parent architecture that routes normalized timecards through every compliance engine, including this one.
- Automating SAG-AFTRA Pension Contributions Tracking — the step-by-step build for segregating pensionable performer compensation this page frames.
- Compliance Fallback Chains — the tiered routing that resolves a rate when a primary trust schedule is unavailable at close.
- SAG-AFTRA Residuals Logic — where reuse windows decide whether a secondary payment carries a pension contribution at all.
- DGA Overtime & Turnaround Rules — the penalty and turnaround structures that fix the pensionable base before this engine applies a rate.