defmodule WorkloadService.Aggregates.Task do @moduledoc """ Behaviour and __using__ macro for task aggregates. Each task type can override validation and add specific behavior. Usage: defmodule WorkloadService.Aggregates.QuoteTask do use WorkloadService.Aggregates.Task, task_type: "quote" end ## With custom submission type defmodule QuoteTaskWithCustomSubmission do defmodule CustomSubmission do @type t :: %{...custom_fields: term()} end use WorkloadService.Aggregates.Task, task_type: "quote", submission_type: CustomSubmission.t() end """ @callback validate_submission(map()) :: :ok | {:error, term()} defmacro __using__(opts) do task_type = Keyword.fetch!(opts, :task_type) commands_module = Keyword.get(opts, :commands, WorkloadService.Commands.Task) submission_type = Keyword.get(opts, :submission_type, quote(do: map())) quote do @type t :: %__MODULE__{ id: WorkloadService.Aggregates.TaskId.t() | nil, application_id: WorkloadService.Aggregates.ApplicationId.t() | nil, task_info: map() | nil, submission: unquote(submission_type) | nil, attachments: [String.t()], status: String.t() | nil } @behaviour Commanded.Aggregates.Aggregate @task_type unquote(task_type) alias unquote(commands_module).CreateTask alias unquote(commands_module).SubmitResponse alias unquote(commands_module).RequestApproval alias unquote(commands_module).ApproveSubmission alias unquote(commands_module).CompleteTask alias Commanded.Aggregates.Aggregate @derive Jason.Encoder defstruct [ :id, :application_id, :task_info, :submission, :attachments, :status ] @impl Aggregate def execute(%__MODULE__{status: nil}, %CreateTask{} = cmd) do %WorkloadService.Events.TaskCreated{ id: cmd.id, application_id: cmd.application_id, task_info: cmd.task_info, attachments: cmd.attachments || [] } end @impl Aggregate def execute(%__MODULE__{}, %CreateTask{}) do {:error, :task_already_exists} end @impl Aggregate def execute(%__MODULE__{status: status}, %SubmitResponse{} = cmd) when status in [nil, "created", "draft", "approved"] do with :ok <- validate_submission(cmd.submission) do %WorkloadService.Events.SubmissionUpdated{ id: cmd.id, submission: cmd.submission, attachments: cmd.attachments } end end @impl Aggregate def execute(%__MODULE__{status: "draft"}, %RequestApproval{} = cmd) do %WorkloadService.Events.ApprovalRequested{ id: cmd.id } end @impl Aggregate def execute(%__MODULE__{status: status}, %RequestApproval{}) do {:error, {:invalid_state, "cannot request approval in state: #{status}"}} end @impl Aggregate def execute(%__MODULE__{id: id, status: "draft"}, %ApproveSubmission{}) do %WorkloadService.Events.SubmissionApproved{ id: id } end @impl Aggregate def execute(%__MODULE__{status: status}, %ApproveSubmission{}) do {:error, {:invalid_state, "cannot approve in state: #{status}"}} end @impl Aggregate def execute(%__MODULE__{status: "approved", id: id}, %CompleteTask{} = cmd) do %WorkloadService.Events.TaskCompleted{ id: id, completed_by: cmd.completed_by } end @impl Aggregate def execute(%__MODULE__{status: status}, %CompleteTask{}) do {:error, {:invalid_state, "cannot complete in state: #{status}"}} end @impl Aggregate def apply(%__MODULE__{} = agg, %WorkloadService.Events.TaskCreated{} = e) do %{ agg | id: e.id, application_id: e.application_id, task_info: e.task_info, attachments: e.attachments, status: "created" } end @impl Aggregate def apply(%__MODULE__{} = agg, %WorkloadService.Events.SubmissionUpdated{} = e) do %{ agg | submission: e.submission, attachments: e.attachments || [], status: "draft" } end @impl Aggregate def apply(%__MODULE__{} = agg, %WorkloadService.Events.SubmissionApproved{}) do %{ agg | status: "approved" } end @impl Aggregate def apply(%__MODULE__{} = agg, %WorkloadService.Events.ApprovalRequested{}) do %{ agg | status: "approval_requested" } end @impl Aggregate def apply(%__MODULE__{} = agg, %WorkloadService.Events.TaskCompleted{}) do %{ agg | status: "completed" } end defoverridable execute: 2, apply: 2 end end end