Tuning Concession Eligibility Expiry Windows

A concession entitlement — senior, student, or disability — is never permanent: it carries a valid_from date, a valid_until date, and, in most agency policies, a grace window that keeps the discount alive for a bounded period after expiry so a rider is not hard-cut to full fare the moment their re-verification lapses. This page implements the exact decision a validator makes at tap time T: is this concession still in force, is it inside the grace window, or has it expired past grace and must the fare fall back to the full adult Decimal amount? It is a leaf of the Discount Eligibility Engines component inside Fare Rule Validation & Calculation Engines, and it owns only the temporal eligibility test that the demographic rules in Automating Senior and Student Fare Validation Rules hand off to it.

Concession expiry decision flow

The evaluator applies three ordered date comparisons against a single tap instant, and each path ends in exactly one fare outcome. Order matters: a tap before valid_from is not-yet-eligible and must be caught before any expiry test, and a tap inside the concession window must short-circuit before the grace logic ever runs. Only a tap strictly after valid_until reaches the grace comparison, where the configurable grace period decides between a flagged-but-honored discount and a full-fare downgrade.

Ordered date comparisons of the concession expiry decision A tap at time T carrying a concession is tested in order. If T is before the concession valid-from date the result is NOT_YET_VALID at full fare. Otherwise, if T is on or before valid-until the concession is ACTIVE and the discount applies. Otherwise T is past valid-until and the overage is measured: if it is within the grace period the result is IN_GRACE with the discount still applied but flagged for re-verification; if it is beyond grace the result is EXPIRED and the fare is downgraded to full adult fare. Each comparison short-circuits before the next. no no within grace yes yes beyond grace Tap at time T + concession T ≥ valid_from? T ≤ valid_until? Overage within grace? NOT_YET_VALID full fare ACTIVE concession discount applies IN_GRACE discount, flag re-verify EXPIRED downgrade to full fare

Step 1 — Model the concession as timezone-safe dates

Expiry is a calendar concept, not a wall-clock one: an agency’s student pass is valid through the end of 2026-08-31 in the network’s local timezone, regardless of where the validator’s clock sits. Comparing a tap instant against a naive date invites two failure modes — a tap just before local midnight can read as a day early in UTC, and a naive/aware comparison raises TypeError. Model the concession with explicit date boundaries plus the agency timezone, and resolve every tap to a local calendar day before any comparison runs.

import logging
from dataclasses import dataclass
from datetime import date, datetime, timezone
from decimal import Decimal
from enum import Enum
from zoneinfo import ZoneInfo

logger = logging.getLogger("afc.concession_expiry")


class ConcessionType(str, Enum):
    SENIOR = "senior"
    STUDENT = "student"
    DISABILITY = "disability"


class EligibilityStatus(str, Enum):
    ACTIVE = "active"
    IN_GRACE = "in_grace"
    EXPIRED = "expired"
    NOT_YET_VALID = "not_yet_valid"


@dataclass(frozen=True)
class Concession:
    """A rider's concession entitlement with inclusive calendar boundaries."""
    concession_type: ConcessionType
    valid_from: date
    valid_until: date          # inclusive: the pass works through this whole day
    agency_tz: str = "America/New_York"
    grace_days: int = 14       # policy grace window after valid_until

    def local_tap_day(self, tap_utc: datetime) -> date:
        """Resolve a UTC tap instant to the agency-local calendar day."""
        if tap_utc.tzinfo is None:
            raise ValueError("tap timestamp must be timezone-aware UTC")
        return tap_utc.astimezone(ZoneInfo(self.agency_tz)).date()

Making valid_until inclusive is a deliberate policy encoding: riders and auditors both read “valid until Aug 31” as covering the 31st, so the comparison below uses <=, not <. The grace_days default belongs to configuration in production — it is loaded per program from the Threshold Tuning Frameworks layer rather than hardcoded — but a sane in-code default keeps the evaluator testable in isolation.

Step 2 — Evaluate eligibility and downgrade the fare

The evaluator returns both a status and the fare to charge, so the caller never has to re-derive the money from the status. When the concession is ACTIVE or IN_GRACE the discounted fare applies; when it is EXPIRED or NOT_YET_VALID the fare downgrades to the full adult amount. All money is Decimal in whole currency units quantized to the cent, and the discount is computed from a Decimal rate so no float ever touches a fare.

from decimal import ROUND_HALF_UP


@dataclass(frozen=True)
class FareDecision:
    status: EligibilityStatus
    charged_fare: Decimal
    discount_applied: Decimal
    overage_days: int
    rationale: str


def evaluate_concession(
    concession: Concession,
    tap_utc: datetime,
    full_fare: Decimal,
    discount_rate: Decimal,
) -> FareDecision:
    """Decide the fare for a tap given a concession's expiry and grace window.

    discount_rate is a fraction, e.g. Decimal("0.50") for half fare.
    Returns the full fare on any non-eligible status; never raises on expiry.
    """
    if not (Decimal("0") <= discount_rate <= Decimal("1")):
        raise ValueError(f"discount_rate out of range: {discount_rate}")
    if full_fare <= Decimal("0.00"):
        raise ValueError("full_fare must be positive")

    tap_day = concession.local_tap_day(tap_utc)
    discounted = (full_fare * (Decimal("1") - discount_rate)).quantize(
        Decimal("0.01"), rounding=ROUND_HALF_UP
    )

    if tap_day < concession.valid_from:
        return FareDecision(
            EligibilityStatus.NOT_YET_VALID, full_fare, Decimal("0.00"),
            0, f"tap day {tap_day} precedes valid_from {concession.valid_from}",
        )

    if tap_day <= concession.valid_until:
        return FareDecision(
            EligibilityStatus.ACTIVE, discounted, full_fare - discounted,
            0, "within concession validity window",
        )

    overage = (tap_day - concession.valid_until).days
    if overage <= concession.grace_days:
        logger.info(
            "concession %s in grace: %d day(s) past expiry",
            concession.concession_type.value, overage,
        )
        return FareDecision(
            EligibilityStatus.IN_GRACE, discounted, full_fare - discounted,
            overage, f"{overage}d past expiry, within {concession.grace_days}d grace",
        )

    logger.warning(
        "concession %s expired past grace: %d day(s) over",
        concession.concession_type.value, overage,
    )
    return FareDecision(
        EligibilityStatus.EXPIRED, full_fare, Decimal("0.00"),
        overage, f"{overage}d past expiry exceeds {concession.grace_days}d grace",
    )

Two properties make this safe to run at the gate. First, the grace branch keeps the rider on the discounted fare but stamps the decision IN_GRACE so a downstream re-verification workflow can nudge the rider before the hard cut — the same policy-lag protection the parent engine documents for expired-but-grace-period credentials. Second, the EXPIRED path never raises: an expired concession is an expected steady state, not an error, so it returns a full-fare decision the ledger can settle immediately.

Validation & Test Cases

Exercise every terminal path with concrete dates. The normal case is a tap comfortably inside the window; the edge cases cover a tap one day inside the grace window and a tap one day past it, which must flip the fare from discounted to full.

from datetime import datetime, timezone
from decimal import Decimal

pass_ = Concession(
    concession_type=ConcessionType.STUDENT,
    valid_from=date(2026, 1, 1),
    valid_until=date(2026, 8, 31),
    agency_tz="America/New_York",
    grace_days=14,
)
FULL = Decimal("2.90")
RATE = Decimal("0.50")  # student half fare

# Normal case: mid-term tap -> ACTIVE, half fare
d = evaluate_concession(pass_, datetime(2026, 3, 15, 13, 0, tzinfo=timezone.utc), FULL, RATE)
assert d.status is EligibilityStatus.ACTIVE
assert d.charged_fare == Decimal("1.45")

# Edge: 10 days past expiry, inside 14-day grace -> IN_GRACE, still half fare
d = evaluate_concession(pass_, datetime(2026, 9, 10, 13, 0, tzinfo=timezone.utc), FULL, RATE)
assert d.status is EligibilityStatus.IN_GRACE
assert d.charged_fare == Decimal("1.45")
assert d.overage_days == 10

# Edge: 15 days past expiry, beyond grace -> EXPIRED, downgrade to full fare
d = evaluate_concession(pass_, datetime(2026, 9, 15, 13, 0, tzinfo=timezone.utc), FULL, RATE)
assert d.status is EligibilityStatus.EXPIRED
assert d.charged_fare == Decimal("2.90")
assert d.discount_applied == Decimal("0.00")

# Edge: tap before the pass starts -> NOT_YET_VALID at full fare
d = evaluate_concession(pass_, datetime(2025, 12, 31, 13, 0, tzinfo=timezone.utc), FULL, RATE)
assert d.status is EligibilityStatus.NOT_YET_VALID
assert d.charged_fare == Decimal("2.90")

The grace-boundary pair is the assertion that protects revenue: at day 14 the rider still pays 1.45, at day 15 they pay the full 2.90, and the transition is exact because the overage is a whole-day integer difference, not a floating-point duration. A tap at 2026-09-10T13:00Z resolves to September 10 in America/New_York (09:00 local), so daylight-saving offsets never shift the comparison across a day boundary.

Edge Cases

  • Local-midnight taps. A tap at 2026-08-31T23:30 local is still the 31st and still valid; the same instant compared naively in UTC (2026-09-01T03:30Z) would read as expired. Always convert to the agency-local day first, which is exactly what local_tap_day enforces.
  • Grace of zero. Some programs (short-term visitor passes) set grace_days=0, collapsing IN_GRACE out of the flow entirely — the first day past valid_until is immediately EXPIRED. The code handles this without a special case because overage <= 0 is only true on the expiry day itself, which the <= window check already claimed.
  • Renewed mid-journey. A rider whose pass renews between two taps of one journey should not be re-priced mid-trip. Pin the concession version at journey start, the same immutability discipline the parent engine applies to discount matrices.
  • Backdated corrections. If a registry backfills a corrected valid_until, re-evaluate the original tap by its idempotency key so the correction re-prices the same tap rather than minting a second fare.

Integration Note

This evaluator is the temporal gate that Discount Eligibility Engines invokes once a rider’s demographic entitlement has been resolved: the engine confirms which concession a rider holds, then this code confirms whether that concession is still in force at time T. Its closest sibling is Automating Senior and Student Fare Validation Rules, which supplies the Concession record — the program code, the valid_from/valid_until boundaries, and the verification token — that this page consumes. Keeping the two concerns separate means an agency can change its grace policy without touching the demographic-matching logic, and vice versa.

FAQ

Why keep a grace window instead of hard-cutting to full fare on the expiry date?
Because re-verification lag is usually the agency's fault, not the rider's. Student enrolment feeds and municipal disability registries refresh on their own schedule, so a pass can lapse in the system days before the rider could possibly have renewed. A bounded grace window keeps the concession honored while the tap is flagged IN_GRACE for re-verification, which protects a protected rider from an over-charge without leaving the discount open indefinitely.
Should I compare the tap timestamp as UTC or in the agency's local timezone?
Convert to the agency-local calendar day first. Expiry is a calendar concept — "valid through Aug 31" means the whole of the 31st in the network's timezone. A tap at 23:30 local on the last valid day is 2026-09-01T03:30Z in UTC, which a naive UTC comparison would wrongly read as expired. The local_tap_day helper resolves the instant to the agency day so daylight-saving offsets never move a tap across a day boundary.
Is valid_until inclusive or exclusive?
Inclusive. Riders and auditors both read "valid until Aug 31" as covering the 31st, so the evaluator uses tap_day <= valid_until. Encoding it as exclusive would silently strip the last day of every pass and generate a wave of full-fare charges that reconciliation would have to unwind. If a data source hands you an exclusive end date, normalize it to inclusive at ingestion rather than flipping the comparison operator.
Can the discount rate be a float if I round the final fare?
No. Binary float cannot represent most cent fractions, so a 50% discount on an odd fare can land a cent off and that drift compounds across millions of concession taps into an audit discrepancy. Keep the rate as a Decimal fraction, compute the discounted fare in Decimal space, and quantize once with ROUND_HALF_UP so every validator produces the identical charged fare.

Part of Discount Eligibility Engines, within Fare Rule Validation & Calculation Engines.