State Filing Deadline Calendars: Computing Deterministic Due Dates Across Jurisdictions
A state filing deadline calendar is the component that converts a statutory rule — “an annual report is due on the anniversary of formation” or “the first day of the fourth month after fiscal year-end” — into an exact, business-day-adjusted, time-zoned date that a scheduler can act on without human interpretation. The hard part is not storing dates; it is computing them deterministically from heterogeneous statutory anchors so the same entity resolves to the same trigger on every run, across fifty jurisdictions that each define “due date” differently.
This guide is part of the Core Architecture & Regulatory Mapping framework. It sits one layer above Entity Taxonomy & Classification: once an entity has been resolved to a single filing obligation, the calendar engine attaches a dated, dispatchable execution window to it. Everything downstream in the Deadline Tracking & Routing Engines layer — priority scoring, batch orchestration, and notification pipelines — consumes the dates this engine produces, so a one-day computation error here silently propagates into late filings and penalty exposure across an entire portfolio.
Statutory and Regulatory Context
Filing deadlines are defined by statute, and the statutes do not agree on an anchor. Delaware franchise tax and the annual report for domestic corporations are due March 1 under Del. Code tit. 8 § 502(a), a fixed calendar date independent of when the entity formed. Delaware LLCs instead owe only an annual tax due June 1 under Del. Code tit. 6 § 18-1107, with no annual report at all. California, by contrast, anchors the Statement of Information to the anniversary month of registration under Cal. Corp. Code § 1502, giving the filer the entire month, while New York uses a biennial cadence keyed to the registration anniversary month. Texas folds the franchise tax and Public Information Report into a single May 15 annual deadline.
Three statutory primitives recur across these rules and must be modeled explicitly: the anchor (fixed date, fiscal-year offset, or formation/registration anniversary), the cadence (annual, biennial, or biennial-by-parity-of-year), and the business-day treatment (whether a due date that lands on a weekend or state holiday rolls forward, rolls back, or stays put). All date arithmetic is expressed in ISO 8601 and resolved against a jurisdiction-specific time zone, because a deadline is a civil date in the filing state, not in UTC. The holiday inputs are drawn from each Secretary of State’s published schedule, cross-referenced against the National Association of Secretaries of State directory.
Architecture and Design Model
The engine is a three-stage pipeline: ingest raw statutory anchors into a typed calendar entry, normalize the anchor into a concrete civil date with business-day and grace-period offsets applied, then dispatch that date as an idempotent, single-intent routing instruction. Each stage commits its state before the next begins, so a failure is always replayable from a durable checkpoint rather than re-derived from scratch.
The central design decisions are deliberate. Temporal normalization is pure and deterministic: given the same entry and the same holiday set, resolve_execution_date always returns the same date — no date.today(), no wall-clock reads inside the resolver. Dispatch is idempotent: a SHA-256 routing key over (entity, jurisdiction, filing_type, year) guarantees that a re-run, a retry, or a duplicate queue delivery cannot produce a second filing. State transitions commit before external I/O, so the system never performs a side effect it cannot account for in the audit log. These three properties — purity, idempotency, commit-before-effect — are what let the downstream priority scoring algorithms and multi-entity batch orchestration treat a calendar entry as a trustworthy fact rather than a guess.
Prerequisites and Dependencies
| Dependency | Minimum version | Role in the calendar engine |
|---|---|---|
| Python | 3.10+ | zoneinfo, match statements, `X |
zoneinfo (stdlib) |
3.9+ | IANA time zones for per-jurisdiction civil-date resolution |
| Pydantic | v2 | Validates ingested statutory anchors before normalization |
holidays |
0.40+ | Seeds federal and per-state holiday sets for rollover logic |
| Celery or ARQ | 5.x / 0.25+ | Schedules retries with exponential backoff on dispatch failure |
| PostgreSQL | 14+ | Transactional state store for routing keys (READ COMMITTED) |
Infrastructure assumptions: a distributed lock (Redis or a database advisory lock) backs the dispatcher in production — the in-process threading.Lock below is illustrative only — and the holiday datasets are version-controlled so an audit can reconstruct which calendar produced a given date.
Step-by-Step Implementation
Phase 1 — Model the statutory anchor
Every calendar entry begins as a typed, frozen record. The anchor type is explicit so the normalizer can branch on it deterministically rather than inferring intent from loosely-typed fields. This model is the temporal counterpart of the typed payloads defined in Compliance Metadata Schemas.
from __future__ import annotations
import logging
from dataclasses import dataclass
from datetime import date
from enum import Enum, auto
logger = logging.getLogger(__name__)
class FilingType(Enum):
ANNUAL_REPORT = auto()
FRANCHISE_TAX = auto()
BENEFICIAL_OWNERSHIP = auto()
class AnchorKind(Enum):
FIXED_CALENDAR = auto() # e.g. Delaware corp report: March 1
FISCAL_OFFSET = auto() # e.g. first day of 4th month after FY-end
FORMATION_ANNIVERSARY = auto() # e.g. California Statement of Information
class Cadence(Enum):
ANNUAL = auto()
BIENNIAL = auto() # e.g. New York DOS
@dataclass(frozen=True)
class StatutoryCalendarEntry:
jurisdiction_code: str # ISO 3166-2, e.g. "US-DE"
entity_classification: str
filing_type: FilingType
anchor_kind: AnchorKind
base_due_date: date # resolved nominal date for the target year
cadence: Cadence = Cadence.ANNUAL
grace_period_days: int = 0
weekend_rollover: bool = True
holiday_exclusion: bool = True
timezone: str = "America/New_York"
Phase 2 — Normalize the anchor into a civil date
The resolver applies the statutory offsets in a fixed order — weekend rollover, holiday skip, then grace period (with a second rollover so a grace date never lands on a weekend). It is pure: the holiday set is injected, never read from a global, so the function is fully testable and reproducible.
from __future__ import annotations
import logging
from datetime import date, timedelta
from typing import Protocol
from zoneinfo import ZoneInfo
logger = logging.getLogger(__name__)
class BusinessDayCalculator(Protocol):
def adjust_to_business_day(self, target: date, jurisdiction: str) -> date: ...
class TemporalNormalizer:
"""Deterministic statutory-deadline resolver. No wall-clock reads."""
def __init__(self, holiday_set: set[date]) -> None:
self._holidays = holiday_set
def resolve_execution_date(self, entry: StatutoryCalendarEntry) -> date:
execution_date = entry.base_due_date
if entry.weekend_rollover:
execution_date = self._roll_weekend(execution_date)
if entry.holiday_exclusion:
execution_date = self._skip_holidays(execution_date)
if entry.grace_period_days > 0:
execution_date += timedelta(days=entry.grace_period_days)
if entry.weekend_rollover:
execution_date = self._roll_weekend(execution_date)
# Civil date is anchored to the filing state's zone, not UTC.
tz = ZoneInfo(entry.timezone)
logger.info(
"deadline.resolved",
extra={
"jurisdiction": entry.jurisdiction_code,
"filing_type": entry.filing_type.name,
"base_due_date": entry.base_due_date.isoformat(),
"execution_date": execution_date.isoformat(),
"timezone": str(tz),
},
)
return execution_date
@staticmethod
def _roll_weekend(d: date) -> date:
if d.weekday() >= 5: # Saturday=5, Sunday=6 -> roll to Monday
return d + timedelta(days=7 - d.weekday())
return d
def _skip_holidays(self, d: date) -> date:
while d in self._holidays:
d += timedelta(days=1)
d = self._roll_weekend(d)
return d
Phase 3 — Dispatch with an idempotency guard
Dispatch transitions a routing instruction through an explicit state machine. The routing key is computed over persisted, deterministic fields only, so a replay produces the identical key and is suppressed. The state is written to DISPATCHED before the filing pipeline runs, which is the commit-before-effect rule that keeps the audit log honest.
from __future__ import annotations
import hashlib
import logging
import threading
from dataclasses import dataclass, field
from datetime import date
from enum import Enum
logger = logging.getLogger(__name__)
class ExecutionState(Enum):
PENDING = "PENDING"
DISPATCHED = "DISPATCHED"
COMPLETED = "COMPLETED"
FAILED = "FAILED"
@dataclass
class RoutingInstruction:
entity_id: str
jurisdiction: str
filing_type: FilingType
execution_date: date
target_year: int
routing_key: str = field(default="")
def __post_init__(self) -> None:
if not self.routing_key:
payload = (
f"{self.entity_id}:{self.jurisdiction}:"
f"{self.filing_type.name}:{self.target_year}"
)
self.routing_key = hashlib.sha256(payload.encode()).hexdigest()
class IdempotentDispatcher:
"""Single-intent dispatch with commit-before-effect state transitions."""
def __init__(self, state_store: dict[str, ExecutionState]) -> None:
self._state_store = state_store
self._lock = threading.Lock() # Production: Redis / advisory lock
def dispatch(self, instruction: RoutingInstruction) -> bool:
key = instruction.routing_key
with self._lock:
current = self._state_store.get(key, ExecutionState.PENDING)
if current in (ExecutionState.DISPATCHED, ExecutionState.COMPLETED):
logger.warning("dispatch.suppressed", extra={"routing_key": key})
return False
self._state_store[key] = ExecutionState.DISPATCHED # commit first
try:
self._execute_filing_pipeline(instruction)
self._state_store[key] = ExecutionState.COMPLETED
logger.info("dispatch.completed", extra={"routing_key": key})
return True
except Exception:
self._state_store[key] = ExecutionState.FAILED
logger.exception("dispatch.failed", extra={"routing_key": key})
raise
def _execute_filing_pipeline(self, instruction: RoutingInstruction) -> None:
# Hand off to the portal-submission layer; the dated, single-intent
# instruction is now an immutable fact downstream consumers can trust.
...
Phase 4 — Categorize failures and route fallbacks
Regulatory environments are volatile, so a dispatch failure must be classified before it is retried. Recoverable integration faults retry with exponential backoff; non-recoverable statutory ambiguities route straight to legal-ops without burning the grace window. This taxonomy is the same error model the registered agent assignment logic uses when it escalates a routing failure to a regional team.
from __future__ import annotations
import enum
import logging
from dataclasses import dataclass
logger = logging.getLogger(__name__)
class ComplianceErrorCategory(enum.Enum):
INTEGRATION_TIMEOUT = "integration_timeout"
IDEMPOTENCY_CONFLICT = "idempotency_conflict"
STATUTORY_AMBIGUITY = "statutory_ambiguity"
GRACE_PERIOD_EXHAUSTED = "grace_period_exhausted"
DATA_VALIDATION_FAILURE = "data_validation_failure"
@dataclass(frozen=True)
class ComplianceError(Exception):
category: ComplianceErrorCategory
entity_id: str
jurisdiction: str
message: str
retryable: bool = False
def __str__(self) -> str:
return f"[{self.category.value}] {self.jurisdiction}/{self.entity_id}: {self.message}"
class EscalationRouter:
MAX_RETRIES = 3
BACKOFF_BASE = 2.0
def handle_failure(self, error: ComplianceError, attempt: int) -> None:
if not error.retryable:
logger.critical("escalation.legal_ops", extra={"error": str(error)})
return # freeze entity routing, page on-call, attach audit log
if attempt >= self.MAX_RETRIES:
logger.warning("escalation.manual_review", extra={"error": str(error)})
return # pause automation, open ticket with statutory reference
delay = self.BACKOFF_BASE ** attempt
logger.info(
"escalation.retry_scheduled",
extra={"entity_id": error.entity_id, "delay_s": delay, "attempt": attempt + 1},
)
Edge Cases and Jurisdiction-Specific Gotchas
The anchor, cadence, and business-day treatment differ enough across states that hard-coding any single rule guarantees wrong dates somewhere. Model each jurisdiction as data:
| Jurisdiction | Anchor | Cadence | Business-day treatment | Gotcha |
|---|---|---|---|---|
| Delaware (corp) | Fixed: March 1 | Annual | No rollover — statute fixes the date | Franchise tax and report share the March 1 deadline; large-filer estimated payments fall earlier on June 1 |
| Delaware (LLC) | Fixed: June 1 | Annual | No rollover | LLC owes a flat tax and no annual report — do not emit a report obligation |
| California | Registration anniversary month | Annual (stock) / biennial (LLC) | Filing window is the whole month; late = entire month elapses | Stock corporations file annually; LLCs file biennially — cadence depends on entity type |
| New York | Registration anniversary month | Biennial | Window is the anniversary month | Biennial parity must be derived from the registration year, not the current year |
| Texas | Fixed: May 15 | Annual | Rolls forward to next business day | Combined franchise tax + Public Information Report; a “no tax due” entity still owes the report |
| Florida | Fixed: May 1 | Annual | No rollover; steep penalty after the date | A flat $400 late fee applies the day after the deadline with no proportional grace |
Two cross-cutting traps: DST drift corrupts dates when arithmetic is done on naive datetimes — always resolve civil dates with zoneinfo and never add timedelta(hours=...) across a DST boundary. Anniversary anchors for Feb-29 formations must clamp to Feb 28 in non-leap years; an unclamped date(year, 2, 29) raises ValueError and crashes the whole batch.
Verification and Testing
Determinism is the property worth testing hardest: feed the resolver a frozen holiday set and assert idempotence across repeated calls, then assert the documented jurisdiction-specific outcomes.
import unittest
from datetime import date
class TestTemporalNormalizer(unittest.TestCase):
def setUp(self) -> None:
# 2026-07-03 is the observed federal Independence Day holiday.
self.normalizer = TemporalNormalizer(holiday_set={date(2026, 7, 3)})
def test_resolution_is_idempotent(self) -> None:
entry = StatutoryCalendarEntry(
jurisdiction_code="US-TX",
entity_classification="domestic_llc",
filing_type=FilingType.FRANCHISE_TAX,
anchor_kind=AnchorKind.FIXED_CALENDAR,
base_due_date=date(2026, 5, 15),
)
first = self.normalizer.resolve_execution_date(entry)
second = self.normalizer.resolve_execution_date(entry)
self.assertEqual(first, second)
def test_saturday_deadline_rolls_to_monday(self) -> None:
entry = StatutoryCalendarEntry(
jurisdiction_code="US-TX",
entity_classification="domestic_corp",
filing_type=FilingType.ANNUAL_REPORT,
anchor_kind=AnchorKind.FIXED_CALENDAR,
base_due_date=date(2026, 5, 16), # a Saturday
)
self.assertEqual(self.normalizer.resolve_execution_date(entry), date(2026, 5, 18))
def test_holiday_then_weekend_skips_both(self) -> None:
entry = StatutoryCalendarEntry(
jurisdiction_code="US-DE",
entity_classification="domestic_corp",
filing_type=FilingType.FRANCHISE_TAX,
anchor_kind=AnchorKind.FIXED_CALENDAR,
base_due_date=date(2026, 7, 3), # holiday -> skip -> lands Sat -> Monday
)
self.assertEqual(self.normalizer.resolve_execution_date(entry), date(2026, 7, 6))
For dispatch, assert that a duplicate routing key is suppressed and that a thrown exception leaves the key in FAILED rather than DISPATCHED, so the retry path can pick it up cleanly.
Troubleshooting
Delaware LLCs are being assigned a March 1 annual report deadline
The anchor table is applying the domestic-corporation rule to LLCs. Delaware LLCs owe only the flat tax due June 1 and file no annual report. Branch the calendar entry on entity type during ingestion so an LLC never receives a FilingType.ANNUAL_REPORT obligation — this is a classification boundary that belongs in Entity Taxonomy & Classification, upstream of date computation.
The same entity resolves to a different date on a re-run
The resolver is reading a wall clock or an unsorted/mutable holiday source. resolve_execution_date must be pure: inject the holiday set, never call date.today() inside it, and version the holiday dataset so the same input always reproduces the same output under audit.
A deadline computed an hour off, then missed by a day during March or November
Date arithmetic is being done on naive datetimes across a daylight-saving boundary. Resolve civil dates with zoneinfo and operate on date objects, not datetime + timedelta(hours=...); a filing deadline is a civil date in the filing state, and adding hours across a DST transition shifts it.
The batch crashes every leap year on February anniversaries
An anniversary anchor is constructing date(year, 2, 29) in a non-leap year, which raises ValueError. Clamp February anniversaries to the 28th in non-leap years before passing the date to the normalizer.
Two identical filings were submitted to the same portal
The dispatcher’s idempotency guard is keyed on a non-deterministic field or the lock is process-local under horizontal scaling. Compute the SHA-256 routing key over persisted fields only and back the guard with a distributed lock so a duplicate queue delivery across workers is suppressed.