Modeling Anniversary vs. Fiscal-Year Filing Deadlines
This guide is part of the State Filing Deadline Calendars area within the Core Architecture & Regulatory Mapping framework. It isolates the single decision that sits under every dated obligation: which of the two dominant due-date derivation rules a jurisdiction uses — the date keyed to an entity’s own formation anniversary, or the fixed calendar date the statute pins for everyone — and how to turn either rule plus an entity’s formation date into the concrete next due date the rest of the pipeline can schedule against.
Scope of This Page
This page covers the derivation basis and nothing downstream of it. It models the anniversary-of-formation regime and the fixed calendar/fiscal-year regime as data, then computes the next occurrence of the deadline at or after a supplied reference date, including biennial cadence and February-29 clamping. It deliberately excludes two neighbours documented elsewhere: the business-day adjustment that shifts a raw due date off a weekend or holiday, which belongs to Rolling Deadlines Forward Across Weekends and State Holidays, and the penalty timeline that governs what happens once that date passes, which belongs to Penalty Avoidance & Grace-Period Mapping. Here the output is a clean, unadjusted civil date; the roll-forward and penalty layers consume it.
The Statutory Split That Forces Two Bases
Jurisdictions do not agree on what a due date is anchored to, and the disagreement is not cosmetic — it changes which field of the entity record drives the arithmetic. Delaware pins the domestic-corporation franchise tax and annual report to a fixed March 1 under 8 Del. C. § 502, a date identical for a company formed in 1970 and one formed last week. Texas folds the franchise tax and Public Information Report into a fixed May 15 under Tex. Tax Code § 171. California instead keys the Statement of Information to the entity’s registration anniversary month under Cal. Corp. Code § 1502, so two California corporations formed a month apart owe on different months forever. New York’s biennial statement under N.Y. BCL § 408 is also anniversary-anchored, but on a two-year cadence tied to the parity of the registration year. A calendar engine that hard-codes one of these shapes computes wrong dates in every state that uses the other, which is why the basis has to be a typed field the resolver branches on rather than an assumption baked into code.
Prerequisites
- Python 3.10+ — for
X | Yunions,match-friendly enums, and modern typing on the rule model. - Standard library only:
datetime,calendar(formonthrange, which drives leap-year and month-length clamping),json,logging, anddataclasses. - A classified obligation — the entity’s formation/registration date and its filing type must already be resolved upstream; this layer derives when, not what.
- A reference date supplied by the caller (usually today’s civil date in the jurisdiction’s zone), never read from a global clock inside the resolver, so the derivation is reproducible under audit.
- A versioned rule registry: each jurisdiction’s basis, cadence, and any fixed month/day live in data that an examiner can diff against the statute.
Implementation: A Rules-as-Data Deadline Deriver
The module below models both regimes with one frozen DeadlineRule and one pure derive_next_due_date. Anniversary rules read the entity’s own formation date; fixed rules ignore it and read the statutory fixed_month/fixed_day. Biennial obligations are aligned to the parity of the registration year, and anniversary-month regimes such as California collapse the whole window to its final day. The reference date is an argument, so the same inputs always resolve to the same output. Every derivation emits one structured JSON record. The business-day adjustment is intentionally not here — a raw civil date leaves this module and the roll-forward layer takes over.
from __future__ import annotations
import calendar
import json
import logging
from dataclasses import dataclass
from datetime import date
from enum import Enum
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger("calendar.basis")
class Basis(str, Enum):
ANNIVERSARY = "anniversary" # due date derived from the entity's formation/registration date
FIXED = "fixed" # due date is a statutory month/day, formation-independent
class Cadence(str, Enum):
ANNUAL = "annual"
BIENNIAL = "biennial" # NY BCL 408: every other year, keyed to registration-year parity
@dataclass(frozen=True)
class DeadlineRule:
jurisdiction: str # ISO 3166-2, e.g. "US-CA"
citation: str
basis: Basis
cadence: Cadence = Cadence.ANNUAL
fixed_month: int | None = None # required for FIXED, ignored for ANNIVERSARY
fixed_day: int | None = None
window_to_month_end: bool = False # CA/NY: filing window spans the whole month
def _clamp_day(year: int, month: int, day: int) -> date:
"""Build a valid date, clamping Feb-29 anchors (and any overflow) to month length."""
last_valid = calendar.monthrange(year, month)[1] # 28/29 for Feb, 30/31 otherwise
return date(year, month, min(day, last_valid))
def _next_on_or_after(reference: date, month: int, day: int) -> date:
"""Smallest date carrying (month, day) that is >= reference, leap-safe."""
candidate = _clamp_day(reference.year, month, day)
if candidate < reference: # this year's occurrence already passed
candidate = _clamp_day(reference.year + 1, month, day)
return candidate
def derive_next_due_date(rule: DeadlineRule, formation: date, reference: date) -> date:
"""Return the concrete next due date for `rule` at or after `reference`.
Pure: no wall-clock reads. `reference` is injected so the result is reproducible
under audit. Business-day adjustment happens downstream, not here.
"""
if rule.basis is Basis.ANNIVERSARY:
month, day = formation.month, formation.day # anchor is the entity's own date
elif rule.basis is Basis.FIXED:
if rule.fixed_month is None or rule.fixed_day is None:
raise ValueError(f"{rule.jurisdiction}: FIXED basis requires fixed_month/fixed_day")
month, day = rule.fixed_month, rule.fixed_day # anchor is the statute, not the entity
else: # defensive: unreachable while Basis has two members
raise ValueError(f"unknown basis {rule.basis!r}")
due = _next_on_or_after(reference, month, day)
if rule.cadence is Cadence.BIENNIAL:
# BCL 408: the statement is owed in years matching the registration-year parity.
if (due.year - formation.year) % 2 != 0:
due = _clamp_day(due.year + 1, month, day)
if rule.window_to_month_end:
# Corp. Code 1502: the window is the whole anniversary month; the hard
# deadline is its last day. Re-clamp to month length for the resolved year.
last_valid = calendar.monthrange(due.year, due.month)[1]
due = date(due.year, due.month, last_valid)
logger.info(json.dumps({
"event": "due_date_derived",
"jurisdiction": rule.jurisdiction,
"citation": rule.citation,
"basis": rule.basis.value,
"cadence": rule.cadence.value,
"formation": formation.isoformat(),
"reference": reference.isoformat(),
"next_due_date": due.isoformat(),
}))
return due
REGISTRY: dict[str, DeadlineRule] = {
"US-DE": DeadlineRule("US-DE", "8 Del. C. 502", Basis.FIXED, fixed_month=3, fixed_day=1),
"US-TX": DeadlineRule("US-TX", "Tex. Tax Code 171", Basis.FIXED, fixed_month=5, fixed_day=15),
"US-CA": DeadlineRule("US-CA", "Cal. Corp. Code 1502", Basis.ANNIVERSARY,
window_to_month_end=True),
"US-NY": DeadlineRule("US-NY", "N.Y. BCL 408", Basis.ANNIVERSARY, Cadence.BIENNIAL,
window_to_month_end=True),
}
if __name__ == "__main__":
formed = date(2019, 8, 14) # a Wednesday; parity year is odd
today = date(2026, 7, 16) # supplied reference, not read from the clock
for code, rule in REGISTRY.items():
derive_next_due_date(rule, formation=formed, reference=today)
Run against the reference 2026-07-16, an entity formed 2019-08-14 resolves to 2027-03-01 in Delaware and 2027-05-15 in Texas (both fixed, formation ignored), 2026-08-31 in California (the last day of the August anniversary window), and 2027-08-31 in New York — because the 2026 occurrence fails the biennial parity check against the odd 2019 registration year, so the resolver advances one cycle. The same four calls are what the priority scoring algorithms later measure proximity against; a basis error here silently mis-ranks every obligation that depends on the date.
Configuration Reference
Each field is data because it is dictated by statute, not by control flow. Encoding the basis as a branch in code rather than a value in a table is the single most common way these engines drift out of alignment with the law.
| Field | Example | Statutory / operational justification |
|---|---|---|
basis |
FIXED (DE, TX) / ANNIVERSARY (CA, NY) |
Selects whether the anchor is the statute’s date or the entity’s formation date; branching the resolver on a typed value keeps both regimes in one code path. |
cadence |
ANNUAL / BIENNIAL (NY) |
N.Y. BCL § 408 owes the statement every two years; annual states advance by one. Cadence is orthogonal to basis, so it is a separate field. |
fixed_month / fixed_day |
3 / 1 (DE) · 5 / 15 (TX) |
The statutory calendar date for fixed regimes (8 Del. C. § 502, Tex. Tax Code § 171); None for anniversary regimes where the entity supplies it. |
window_to_month_end |
True (CA, NY) |
Cal. Corp. Code § 1502 grants the whole anniversary month; the hard deadline is its last day, so the anchor day is collapsed to month-end. |
formation (argument) |
2019-08-14 |
The anniversary anchor; ignored entirely by fixed regimes, load-bearing for anniversary regimes. |
reference (argument) |
2026-07-16 |
Injected, never read from a global clock, so the derivation is pure and an audit replays to the identical date. |
Failure Modes and Fallback Routing
Each derivation fault maps onto the parent area’s error taxonomy — the STATUTORY_AMBIGUITY, DATA_VALIDATION_FAILURE, and INTEGRATION_TIMEOUT categories the State Filing Deadline Calendars engine escalates on — and the basis resolver responds to each distinctly.
- A February-29 formation resolves in a non-leap year (data-validation, recoverable in-line). Constructing
date(2027, 2, 29)raisesValueErrorand would crash the batch._clamp_dayfolds the anniversary to February 28 viacalendar.monthrange, so a leap-day entity resolves cleanly every year instead of failing three years out of four. This is handled, not escalated. - A fixed rule arrives with no
fixed_month/fixed_day(data-validation, terminal). The resolver raises immediately rather than silently defaulting to a date, because a guessed statutory date is worse than a loud failure. The obligation routes to manual review with itsjurisdictionandcitationattached. - Biennial parity is derived from the current year instead of the registration year (statutory ambiguity). Keying the cadence off “this year” rather than
formation.yearputs a New York statement in the wrong biennium and misses it by up to a year. The parity test is anchored to the registration year in data; if the registration year itself is missing upstream, the case escalates to legal-ops without a fabricated date. - The reference date is read from a wall clock instead of injected (system, non-reproducible). A resolver that calls
date.today()internally cannot be replayed, so two runs near midnight in different zones can disagree. The reference is always an argument; the caller resolves the civil date in the jurisdiction’s own zone before the derivation, which is precisely the concern the Rolling Deadlines Forward Across Weekends and State Holidays layer already handles for the adjacent business-day step.
Frequently Asked Questions
Why not just store one due date per entity instead of deriving it from a basis?
Because a stored date cannot be re-derived when a statute, formation record, or cadence changes, and it carries no trace back to the rule that produced it. Modeling the basis as data and deriving the date on every run means a California anniversary shift or a corrected registration date recomputes automatically, and an auditor can point at the exact DeadlineRule and formation inputs that yield a given date. Storage is a cache of the derivation, never the source of truth.
How does the biennial parity rule for New York actually work?
N.Y. BCL § 408 owes the statement every two years in the calendar month of the registration anniversary, and the biennium is fixed by the parity of the registration year. The resolver first finds the next anniversary occurrence at or after the reference date, then checks whether (due.year - formation.year) is even; if it is odd, that year is the off-year and the derivation advances one more year. An entity registered in the odd year 2019 therefore owes in odd years, so a 2026 reference resolves to 2027, not 2026.
California gives a whole anniversary month — what date do you actually schedule?
The last day of that month. Cal. Corp. Code § 1502 makes the filing window the entire anniversary month rather than a single day, so the enforceable deadline is the final calendar day. The resolver sets window_to_month_end for California and New York and collapses the anchor to month-end via calendar.monthrange, which yields 2026-08-31 for an August anniversary. Scheduling any earlier day is safe but understates the available window; scheduling the anchor day itself would misreport the true statutory deadline.
Where does the weekend and holiday adjustment happen if not here?
Deliberately downstream. This module returns an unadjusted civil date so the basis logic stays pure and independently testable; the shift off a Saturday, Sunday, or state holiday is applied by Rolling Deadlines Forward Across Weekends and State Holidays, which owns each jurisdiction’s holiday calendar and observation rules. Keeping derivation and adjustment in separate layers means a holiday-table update never risks a regression in the basis arithmetic, and vice versa.