bursar.allowance module
Pure resolver for configurable free-allowance reset windows (WS9).
resolve_allowance_window computes the [period_start, period_end) window
that a user’s plan allowance resets on, given the allowance period mode and an
anchor timestamp (when the user was assigned the plan). All computation is
UTC-only, date granularity (time-of-day is discarded) — there is no local-time
conversion anywhere, so the resolver is unaffected by DST in any zone.
Supported period values (mirrors PlanDefinition.allowance_period):
"calendar_month"(default): resets on the 1st of each UTC calendar month. This is the regression-safety baseline — it must match the pre-WS9 SQLdate_trunc('month', now() AT TIME ZONE 'UTC')behavior exactly."rolling_30d": resets every 30 days from the anchor (plan-assignment time). Falls back tocalendar_monthwhen no anchor is available."anniversary": resets monthly on the anchor’s day-of-month, clamped to the target month’s actual length (e.g. an anchor of the 31st resets on the 28th/29th in February, then returns to the 31st in a 31-day month). Falls back tocalendar_monthwhen no anchor is available.
resolve_calendar_window is a separate, anchor-free resolver used by
per-feature invocation-count limits (PlanDefinition.feature_limits). It
supports "daily"/"weekly"/"monthly"/"yearly" cadences, all
calendar-aligned (every user on a plan resets at the same instant) rather than
anchored to a per-user timestamp — a deliberately simpler model than the
allowance windows above, since feature-limit cadences have no signup-anchor
requirement in the current design.
bursar.allowance.resolve_allowance_window(now: datetime, period: str, anchor: datetime | None) → tuple[date, date]
Resolve the [period_start, period_end) allowance window (UTC, date-only).
Args:
: now: Current instant. Only the UTC date part is used (time-of-day is
: discarded); callers should pass a UTC-aware (or UTC-naive)
datetime — this function does not perform any timezone conversion.
period: One of "calendar_month", "rolling_30d", "anniversary".
anchor: When the user was assigned the plan (used by rolling_30d and
anniversary).Nonefalls back tocalendar_monthbehavior for those modes.
Returns:
: (period_start, period_end) as date objects, both UTC, with
period_end EXCLUSIVE (i.e. the window is [period_start, period_end)).
Raises:
: ValueError: If period is not a recognized allowance-period mode.
bursar.allowance.resolve_calendar_window(now: datetime, period: str) → tuple[date, date]
Resolve the calendar-aligned [period_start, period_end) window for a cadence.
Used by per-feature invocation-count limits (PlanDefinition.feature_limits).
Unlike resolve_allowance_window(), this resolver takes no anchor: every
user resets at the same UTC instant, which keeps the cadence easy to reason
about and avoids needing a per-feature signup anchor.
Args:
: now: Current instant. Only the UTC date part is used (time-of-day is
: discarded); callers should pass a UTC-aware (or UTC-naive) datetime.
period: One of "daily", "weekly", "monthly", "yearly".
Returns:
: (period_start, period_end) as date objects, both UTC, with
period_end EXCLUSIVE.
Raises:
: ValueError: If period is not a recognized cadence.