Skip to main content

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 SQL date_trunc('month', now() AT TIME ZONE 'UTC') behavior exactly.
  • "rolling_30d": resets every 30 days from the anchor (plan-assignment time). Falls back to calendar_month when 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 to calendar_month when 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). None falls back to calendar_month behavior 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.