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

Types

t()

A resolved billing window: the half-open UTC interval [start, end).

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

t()

@type t() :: %{start: DateTime.t(), end: DateTime.t(), source: atom()}

A resolved billing window: the half-open UTC interval [start, end).

Callbacks

containing(tenant, instant)

(optional)
@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).

current(tenant, now)

@callback current(tenant :: term(), now :: DateTime.t()) :: t()

Returns the active period for tenant at now.

Functions

containing(tenant, instant)

@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
end

Examples

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]}

current(tenant, now \\ Clock.now())

@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

current!(tenant, now \\ Clock.now())

@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