Packages

Usher provides framework-agnostic invitation link management for any Elixir application with Ecto.

Current section

Files

Jump to
usher lib usher invitation_usage.ex
Raw

lib/usher/invitation_usage.ex

defmodule Usher.InvitationUsage do
@moduledoc """
Schema for tracking invitation usage by entities.
An invitation usage record represents when an entity (user, company, device, etc.)
interacts with an invitation. This could be visiting a signup page, completing
registration, or any other trackable action.
"""
use Ecto.Schema
import Ecto.Changeset
alias Usher.Config
alias Usher.Invitation
@type t :: %__MODULE__{
id: Ecto.UUID.t(),
invitation_id: Ecto.UUID.t(),
invitation: Invitation.t() | Ecto.Association.NotLoaded.t(),
entity_type: atom(),
entity_id: String.t(),
action: atom(),
metadata: map(),
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "usher_invitation_usages" do
belongs_to(:invitation, Invitation)
field(:entity_type, Usher.Types.Atom)
field(:entity_id, :string)
field(:action, Usher.Types.Atom)
field(:metadata, :map, default: %{})
timestamps(type: :utc_datetime_usec)
end
@doc """
Changeset for creating invitation usage records.
## Required fields
- `invitation_id`: The invitation being used
- `entity_type`: A string describing what kind of entity (e.g., :user, :company, :device)
- `entity_id`: ID of the entity
- `action`: What action was performed (e.g., :visited, :registered, :activated)
## Optional fields
- `metadata`: Additional context (user agent, IP, custom data, etc.)
"""
def changeset(usage, attrs) do
usage
|> cast(attrs, [:invitation_id, :entity_type, :entity_id, :action, :metadata])
|> validate_required([:invitation_id, :entity_type, :entity_id, :action])
|> validate_entity_type()
|> validate_action()
|> foreign_key_constraint(:invitation_id)
end
defp validate_entity_type(changeset) do
validate_change(changeset, :entity_type, fn :entity_type, entity_type ->
valid_types = Config.valid_usage_entity_types()
if entity_type in valid_types do
[]
else
[entity_type: "must be one of: #{Enum.join(valid_types, ", ")}"]
end
end)
rescue
# If configuration is not set, skip validation to allow tests without config
RuntimeError -> changeset
end
defp validate_action(changeset) do
validate_change(changeset, :action, fn :action, action ->
valid_usage_actions = Config.valid_usage_actions()
if action in valid_usage_actions do
[]
else
[action: "must be one of: #{Enum.join(valid_usage_actions, ", ")}"]
end
end)
rescue
# If configuration is not set, skip validation to allow tests without config
RuntimeError -> changeset
end
end