defmodule AshDispatch.Setup do @moduledoc """ Automatically sets up DeliveryReceipt resource in your consuming app. Similar to ash_paper_trail, this creates the necessary resources for you with zero manual module creation needed. ## Usage In your Deliveries domain module: defmodule MyApp.Deliveries do use AshDispatch.Setup, repo: MyApp.Repo, user_resource: MyApp.Accounts.User # Your domain definition continues here... use Ash.Domain # ... rest of domain config end This will automatically create `MyApp.Deliveries.DeliveryReceipt` with the user relationship - completely frictionless! """ defmacro __using__(opts) do repo = Keyword.fetch!(opts, :repo) user_resource = Keyword.fetch!(opts, :user_resource) # Build the complete resource module AST delivery_receipt_ast = build_delivery_receipt_ast(repo, user_resource) quote do # Get the calling module to namespace the DeliveryReceipt caller_module = __MODULE__ receipt_module = Module.concat(caller_module, DeliveryReceipt) # Dynamically create the DeliveryReceipt resource Module.create(receipt_module, unquote(delivery_receipt_ast), Macro.Env.location(__ENV__)) end end defp build_delivery_receipt_ast(repo, user_resource) do quote do @moduledoc """ Delivery receipt tracking resource. Auto-generated by AshDispatch.Setup - tracks all message deliveries (email, in-app, Discord, SMS, etc.) with full audit trail. """ use Ash.Resource, data_layer: AshPostgres.DataLayer, authorizers: [Ash.Policy.Authorizer], extensions: [ AshStateMachine, AshTypescript.Resource ] postgres do table "delivery_receipts" repo(unquote(repo)) references do reference(:notification, on_delete: :nilify) reference(:user, on_delete: :nilify) end end state_machine do initial_states([:pending]) default_initial_state(:pending) state_attribute(:status) extra_states([:sending, :sent, :failed_permanent, :skipped, :scheduled, :failed]) transitions do transition(:schedule, from: :pending, to: :scheduled) transition(:mark_sending, from: [:scheduled, :pending], to: :sending) transition(:mark_sent, from: [:sending, :scheduled, :pending], to: :sent) transition(:mark_failed, from: [:sending, :scheduled, :pending], to: :failed) transition(:mark_failed_permanent, from: [:sending, :scheduled, :failed], to: :failed_permanent ) transition(:skip, from: [:pending, :scheduled, :sending], to: :skipped) transition(:retry, from: :failed, to: :scheduled) end end relationships do belongs_to :notification, AshDispatch.Resources.Notification do source_attribute :notification_id destination_attribute :id allow_nil? true public? true define_attribute? false end # User relationship - works because we're compiling in the consuming app! belongs_to :user, unquote(user_resource) do source_attribute :user_id destination_attribute :id allow_nil? true public? true define_attribute? false end end attributes do uuid_primary_key :id attribute :event_id, :string, allow_nil?: false, public?: true attribute :transport, :atom, allow_nil?: false, public?: true, constraints: [one_of: [:email, :in_app, :discord, :sms, :webhook]] attribute :user_id, :uuid, allow_nil?: true, public?: true attribute :notification_id, :uuid, allow_nil?: true, public?: true attribute :audience, :atom, allow_nil?: false, public?: true, constraints: [one_of: [:user, :admin, :system]] attribute :status, :atom, default: :pending, allow_nil?: false, public?: true, constraints: [ one_of: [:pending, :scheduled, :sending, :sent, :failed, :failed_permanent, :skipped] ] attribute :recipient, :string, allow_nil?: false, public?: true attribute :provider_id, :string, allow_nil?: true, public?: true attribute :provider_response, :map, default: %{}, allow_nil?: false, public?: true attribute :subject, :string, allow_nil?: true, public?: true attribute :body_text, :string, allow_nil?: true, public?: true attribute :body_html, :string, allow_nil?: true, public?: true attribute :content, :map, default: %{}, allow_nil?: false, public?: true attribute :oban_job_id, :integer, allow_nil?: true, public?: true attribute :error_message, :string, allow_nil?: true, public?: true attribute :retry_count, :integer, default: 0, allow_nil?: false, public?: true attribute :last_retry_at, :utc_datetime_usec, allow_nil?: true, public?: true attribute :sent_at, :utc_datetime_usec, allow_nil?: true, public?: true attribute :delivered_at, :utc_datetime_usec, allow_nil?: true, public?: true attribute :delivery_delayed_at, :utc_datetime_usec, allow_nil?: true, public?: true attribute :failed_at, :utc_datetime_usec, allow_nil?: true, public?: true attribute :opened_at, :utc_datetime_usec, allow_nil?: true, public?: true attribute :clicked_at, :utc_datetime_usec, allow_nil?: true, public?: true attribute :bounced_at, :utc_datetime_usec, allow_nil?: true, public?: true attribute :complained_at, :utc_datetime_usec, allow_nil?: true, public?: true create_timestamp :inserted_at update_timestamp :updated_at end calculations do calculate :oban_job, :map, {AshDispatch.Calculations.ObanJob, []} do public? true end end actions do default_accept :* defaults [:read, :destroy] create :create do accept [ :event_id, :transport, :user_id, :notification_id, :audience, :recipient, :subject, :body_text, :body_html, :content, :oban_job_id, :provider_id, :provider_response ] end read :list_all do pagination offset?: true, keyset?: true, required?: false end read :list_for_user do argument :user_id, :uuid, allow_nil?: false filter expr(user_id == ^arg(:user_id)) pagination offset?: true, keyset?: true, required?: false end read :get do argument :id, :uuid, allow_nil?: false get? true filter expr(id == ^arg(:id)) end update :schedule do accept [:oban_job_id] change transition_state(:scheduled) end update :mark_sending do change transition_state(:sending) end update :mark_sent do accept [:sent_at] change transition_state(:sent) end update :mark_failed do accept [:error_message] change transition_state(:failed) change increment(:retry_count, amount: 1) end update :mark_failed_permanent do accept [:error_message] change transition_state(:failed_permanent) end update :skip do change transition_state(:skipped) end update :retry do accept [:oban_job_id] change transition_state(:scheduled) end end policies do policy always() do authorize_if always() end end identities do identity :oban_job, [:oban_job_id] identity :notification, [:notification_id] end end end end