lib/incident/command_handler.ex

defmodule Incident.CommandHandler do
  @moduledoc """
  Defines a Command Handler that receives a command to:
  - ensure that the command data is valid based on the command validations;
  - executes the command using the aggregate;
  """

  defmacro __using__(opts) do
    aggregate = Keyword.get(opts, :aggregate)
    event_handler = Keyword.get(opts, :event_handler)

    quote do
      import Incident.CommandHandler

      alias Incident.EventStore

      @doc """
      Receives the command struct, validates it and executes it through the aggregate.

      After receiving the new event and aggregate state from the aggregate it attemps to:
      - the event is persisted in the Event Store;
      - the persisted event is broadcasted to the Event Handler;

      Returns an error if:
      - an invalid command is sent;
      - event can't be persisted in the Event Store;
      - event can't be broadcasted to the Event Handler;

      """
      @spec receive(struct) :: {:ok, struct} | {:error, atom | struct | String.t()}
      def receive(command) do
        command_module = command.__struct__

        with true <- command_module.valid?(command),
             :ok <- EventStore.acquire_lock(command.aggregate_id, self()),
             {:ok, new_event, state} <- unquote(aggregate).execute(command),
             {:ok, persisted_event} <- EventStore.append(new_event),
             :ok <- EventStore.release_lock(command.aggregate_id, self()),
             {:ok, _projected_event} <- unquote(event_handler).listen(persisted_event, state) do
          {:ok, persisted_event}
        else
          false -> {:error, :invalid_command}
          {:error, _reason} = error -> error
        end
      end
    end
  end
end