AuroraMeter. Period behaviour
(Aurora Meter v1.0.0)
View Source
Resolves the billing period a usage counter belongs to.
A period is a half-open UTC interval [start, end): start belongs to the
period, end does not. The end of period N is the start of period N+1, so
no instant belongs to two periods and no instant belongs to none. See
docs/periods.md for the full contract, the boundary rule and two custom
source recipes.
current/2 delegates to the configured :period_source (ADR 0003), which
defaults to AuroraMeter.Period.Calendar (calendar month, UTC). Pro overrides
it with subscription-aligned bounds, without any change to core call sites.
current!/2 is the same read with the contract enforced, and it is what every
core call site uses. current/2 is kept unvalidated for compatibility with
hosts that call it directly.
Instants come from AuroraMeter.Clock, so a test can freeze time with
AuroraMeter.Test.with_clock/2.
Summary
Callbacks
Returns the period that held instant, which may be in the past.
Returns the active period for tenant at now.
Functions
Returns the period that held instant, which may be in the past.
Returns the active billing period for tenant, unvalidated.
Returns the active billing period for tenant and checks the contract.
Types
@type t() :: %{start: DateTime.t(), end: DateTime.t(), source: atom()}
A resolved billing window: the half-open UTC interval [start, end).
Callbacks
@callback containing(tenant :: term(), instant :: DateTime.t()) :: t()
Returns the period that held instant, which may be in the past.
Optional. When a source does not export it, AuroraMeter.Period.containing/2
falls back to current/2 with the past instant, which is correct for any
source that is a pure function of the instant (the calendar month is).
@callback current(tenant :: term(), now :: DateTime.t()) :: t()
Returns the active period for tenant at now.
Functions
@spec containing(term(), DateTime.t()) :: t()
Returns the period that held instant, which may be in the past.
Uses the source's containing/2 when it exports one and
current/2 with instant otherwise, then checks the same contract with
instant in place of "now".
Raises AuroraMeter.Period.InvalidPeriodError with reason: :not_containing
when the source cannot place the instant. That raise is the contract: a
caller that must degrade rather than fail (the legacy event backfill, event
attribution for an old occurrence) rescues it and records the event with an
unresolved attribution instead of guessing a period.
try do
AuroraMeter.Period.containing(tenant, occurred_at)
rescue
AuroraMeter.Period.InvalidPeriodError -> :unresolved
endExamples
iex> p = AuroraMeter.Period.containing("org_1", ~U[2026-02-10 12:00:00Z])
iex> {p.start, p.end}
{~U[2026-02-01 00:00:00Z], ~U[2026-03-01 00:00:00Z]}
@spec current(term(), DateTime.t()) :: t()
Returns the active billing period for tenant, unvalidated.
Kept for hosts that call it directly. Core call sites use current!/2; a
source that breaks the contract is caught there rather than here.
Examples
iex> p = AuroraMeter.Period.current("org_1")
iex> match?(%{start: %DateTime{}, end: %DateTime{}, source: _}, p)
true
@spec current!(term(), DateTime.t()) :: t()
Returns the active billing period for tenant and checks the contract.
The returned map must have :start, :end and :source; :start and :end
must be DateTime structs in Etc/UTC; :start must be before :end; and
the interval must contain now (start <= now < end). Any violation raises
AuroraMeter.Period.InvalidPeriodError naming the source module.
Examples
iex> p = AuroraMeter.Period.current!("org_1")
iex> DateTime.compare(p.start, p.end)
:lt