AuroraMeter.PlanVersionConflictError exception (Aurora Meter v1.0.0)

View Source

Raised by AuroraMeter.Plans.register!/0 when a compiled plan version's commercial content differs from the snapshot already stored for it.

This is the whole of invariant I17 in one exception. A plan version is immutable: editing price 2_000 to price 3_000 inside plan :pro changes what every tenant on :pro version "1" is charged, retroactively and silently, and no amount of testing downstream can recover the fact that they were on the old contract. The registry refuses the boot instead.

The remedy is always the same and the message says it: publish the change as a new version.

plan :pro, version: "2", effective_at: ~U[2026-10-01 00:00:00Z] do
  price 3_000
end

conflicts is a list of maps with :plan_id, :version, :stored_fingerprint and :compiled_fingerprint. Rescue it in a release task if a host wants to report rather than crash; do not retry it, because a retry compares the same two values and reaches the same answer.

Configuration plan_version_conflict: :warn logs the same message through Logger.error/1 and lets the boot continue. That is the default in the 0.5.x transition release so an upgrading host sees the problem before it becomes a failed deploy, and :raise is the default from 1.0.

Summary

Functions

The operator-facing message for a list of conflicts.

Types

conflict()

@type conflict() :: %{
  plan_id: String.t(),
  version: String.t(),
  stored_fingerprint: binary() | nil,
  compiled_fingerprint: binary() | nil
}

t()

@type t() :: %AuroraMeter.PlanVersionConflictError{
  __exception__: term(),
  conflicts: [conflict()],
  message: String.t()
}

Functions

format(conflicts)

@spec format([conflict()]) :: String.t()

The operator-facing message for a list of conflicts.

Public because plan_version_conflict: :warn logs exactly this string rather than a second, drifting copy of it.

It is format/1 and not message/1 because message/1 is the Exception callback defexception generates. Defining a one-argument message/1 over a list here shadows it, and Exception.message/1 on the struct then raises a Protocol.UndefinedError instead of rendering the exception, which is a failure that only shows up at the moment somebody needs to read the error.