defmodule DripDrop do
@moduledoc """
Public entry point for DripDrop.
DripDrop is a backend-first, database-driven messaging sequence engine. The
foundation currently exposes boot-time validation while the domain APIs are
implemented capability by capability.
"""
alias DripDrop.{
AdapterHealth,
AdapterPools,
AdapterSequenceBudgets,
Channel,
ChannelAdapters,
Dispatch,
Enrollments,
HttpHooks,
Inbound,
SequenceAuthoring,
StartupCheck,
Suppressions,
Web
}
@doc """
Validates the host application's DripDrop runtime configuration.
This check is intended for host `Application.start/2` callbacks after the
host Repo, PgFlow supervisor, and custom channel registrations have been
configured.
"""
@spec startup_check() :: :ok | {:error, [StartupCheck.error()]}
defdelegate startup_check, to: StartupCheck, as: :run
@doc "Creates a sequence."
@spec create_sequence(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
defdelegate create_sequence(attrs), to: SequenceAuthoring
@doc "Creates a sequence version."
@spec create_sequence_version(Ecto.UUID.t(), map()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
defdelegate create_sequence_version(sequence_id, attrs), to: SequenceAuthoring
@doc "Activates a sequence version and archives the previously active version."
@spec activate_sequence_version(Ecto.UUID.t()) :: {:ok, Ecto.Schema.t()} | {:error, term()}
defdelegate activate_sequence_version(version_id), to: SequenceAuthoring
@doc "Creates a step in a sequence version."
@spec create_step(Ecto.UUID.t(), map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
defdelegate create_step(version_id, attrs), to: SequenceAuthoring
@doc "Creates a transition in a sequence version."
@spec create_step_transition(Ecto.UUID.t(), map()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
defdelegate create_step_transition(version_id, attrs), to: SequenceAuthoring
@doc "Creates a condition attached to a step by default, or to a transition when `transition_id` is provided."
@spec create_condition(Ecto.UUID.t(), map()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
defdelegate create_condition(owner_id, attrs), to: SequenceAuthoring
@doc "Validates a sequence version before activation."
@spec validate_sequence_version(Ecto.UUID.t()) :: {:ok, Ecto.Schema.t()} | {:error, list()}
defdelegate validate_sequence_version(version_id), to: SequenceAuthoring
@doc "Creates a channel adapter."
@spec create_channel_adapter(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
defdelegate create_channel_adapter(attrs), to: ChannelAdapters
@doc "Updates a channel adapter."
@spec update_channel_adapter(Ecto.Schema.t(), map()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
defdelegate update_channel_adapter(adapter, attrs), to: ChannelAdapters
@doc "Lists channel adapters."
@spec list_channel_adapters(map()) :: [Ecto.Schema.t()]
defdelegate list_channel_adapters(filters \\ %{}), to: ChannelAdapters
@doc "Gets the active default adapter for a channel and tenant, falling back to the global default."
@spec get_default_adapter(binary() | atom(), binary() | nil) :: Ecto.Schema.t() | nil
defdelegate get_default_adapter(channel, tenant_key), to: ChannelAdapters
@doc "Creates an outbound adapter pool."
@spec create_adapter_pool(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
defdelegate create_adapter_pool(attrs), to: AdapterPools
@doc "Updates an outbound adapter pool."
@spec update_adapter_pool(Ecto.Schema.t() | Ecto.UUID.t(), map()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
defdelegate update_adapter_pool(pool_or_id, attrs), to: AdapterPools
@doc "Deletes an outbound adapter pool."
@spec delete_adapter_pool(Ecto.Schema.t() | Ecto.UUID.t(), map() | keyword()) ::
{:ok, Ecto.Schema.t()} | {:error, map()}
defdelegate delete_adapter_pool(pool_or_id, opts), to: AdapterPools
@doc "Lists outbound adapter pools."
@spec list_adapter_pools(map()) :: [Ecto.Schema.t()]
defdelegate list_adapter_pools(filters), to: AdapterPools
@doc "Adds an adapter to an outbound pool."
@spec add_pool_member(Ecto.Schema.t() | Ecto.UUID.t(), map()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
defdelegate add_pool_member(pool_or_id, attrs), to: AdapterPools
@doc "Removes an adapter from an outbound pool."
@spec remove_pool_member(Ecto.Schema.t() | Ecto.UUID.t(), map()) ::
{:ok, Ecto.Schema.t()} | {:error, :not_found}
defdelegate remove_pool_member(pool_or_id, attrs), to: AdapterPools
@doc "Lists outbound pool members."
@spec list_pool_members(Ecto.Schema.t() | Ecto.UUID.t() | map()) :: [Ecto.Schema.t()]
defdelegate list_pool_members(pool_or_filters), to: AdapterPools
@doc "Sets an adapter health state from a host-supplied external signal."
@spec set_adapter_health(Ecto.UUID.t(), map()) :: {:ok, Ecto.Schema.t()} | {:error, term()}
defdelegate set_adapter_health(adapter_id, attrs), to: AdapterHealth, as: :set_external_signal
@doc "Creates or updates an outbound adapter sequence budget."
@spec set_adapter_sequence_budget(Ecto.UUID.t(), Ecto.UUID.t(), map()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
defdelegate set_adapter_sequence_budget(adapter_id, sequence_version_id, attrs \\ %{}),
to: AdapterSequenceBudgets
@doc "Creates an HTTP hook for a sequence."
@spec create_http_hook(Ecto.UUID.t(), map()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
defdelegate create_http_hook(sequence_id, attrs), to: HttpHooks
@doc "Updates an HTTP hook."
@spec update_http_hook(Ecto.Schema.t(), map()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
defdelegate update_http_hook(hook, attrs), to: HttpHooks
@doc "Runs an HTTP hook out of band and stores its redacted test result."
@spec test_http_hook(Ecto.UUID.t(), map()) :: {:ok, term()} | {:error, term()}
defdelegate test_http_hook(hook_id, test_data), to: HttpHooks
@doc """
Ingests a normalized inbound email from host-owned inbox infrastructure.
Example:
DripDrop.ingest_inbound_message(adapter.id, %{
message_id: "reply@gmail.com",
in_reply_to: "0197...@example.com",
references: ["0197...@example.com"],
from: "prospect@example.org",
to: "sales@example.com",
body_text: "Sure, let's talk.",
received_at: DateTime.utc_now(),
intent: :reply
})
Hosts can feed this from IMAP, Gmail API watches, or Microsoft Graph
subscriptions after normalizing provider-specific payloads into this map.
"""
@spec ingest_inbound_message(Ecto.UUID.t() | map(), map()) :: :ok | {:error, term()}
defdelegate ingest_inbound_message(adapter_id_or_scope, normalized_message), to: Inbound
@doc "Deprecated unscoped HTTP hook listing. Use `list_http_hooks/2`."
@spec list_http_hooks(Ecto.UUID.t()) :: no_return()
defdelegate list_http_hooks(sequence_id), to: HttpHooks
@doc "Lists HTTP hooks for a sequence and explicit tenant scope."
@spec list_http_hooks(Ecto.UUID.t(), binary() | nil) :: [Ecto.Schema.t()]
defdelegate list_http_hooks(sequence_id, tenant_key), to: HttpHooks
@doc "Creates or updates a suppression for a channel recipient."
@spec suppress(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
defdelegate suppress(attrs), to: Suppressions
@doc "Enrolls a subscriber into the active version of a sequence."
@spec enroll(map()) :: {:ok, Ecto.Schema.t()} | {:error, term()}
defdelegate enroll(attrs), to: Enrollments
@doc "Deprecated unscoped cancel. Use `unenroll/2`."
@spec unenroll(Ecto.UUID.t()) :: no_return()
defdelegate unenroll(enrollment_id), to: Enrollments
@doc "Cancels an enrollment scoped by tenant."
@spec unenroll(Ecto.UUID.t(), binary() | nil) ::
{:ok, Ecto.Schema.t()} | {:error, term()}
defdelegate unenroll(enrollment_id, tenant_key), to: Enrollments
@doc "Deprecated unscoped pause. Use `pause_enrollment/2`."
@spec pause_enrollment(Ecto.UUID.t()) :: no_return()
defdelegate pause_enrollment(enrollment_id), to: Enrollments
@doc "Pauses an enrollment scoped by tenant."
@spec pause_enrollment(Ecto.UUID.t(), binary() | nil) ::
{:ok, Ecto.Schema.t()} | {:error, term()}
defdelegate pause_enrollment(enrollment_id, tenant_key), to: Enrollments
@doc "Deprecated unscoped resume. Use `resume_enrollment/2`."
@spec resume_enrollment(Ecto.UUID.t()) :: no_return()
defdelegate resume_enrollment(enrollment_id), to: Enrollments
@doc "Resumes an enrollment scoped by tenant."
@spec resume_enrollment(Ecto.UUID.t(), binary() | nil) ::
{:ok, Ecto.Schema.t()} | {:error, term()}
defdelegate resume_enrollment(enrollment_id, tenant_key), to: Enrollments
@doc "Reassigns an enrollment to a different outbound adapter."
@spec repin_enrollment(Ecto.UUID.t(), Ecto.UUID.t(), keyword() | map()) ::
{:ok, Ecto.Schema.t()} | {:error, term()}
defdelegate repin_enrollment(enrollment_id, new_adapter_id, opts \\ []), to: Enrollments
@doc "Tracks an event by subscriber identity (tenant_key in the map) or deprecated unscoped enrollment id."
@spec track_event(Ecto.UUID.t() | map(), binary(), map()) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | no_return()
defdelegate track_event(identity, event_key, event_data), to: Enrollments
@doc "Tracks an event for a tenant-scoped enrollment id."
@spec track_event(Ecto.UUID.t(), binary(), map(), binary() | nil) ::
{:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
defdelegate track_event(enrollment_id, event_key, event_data, tenant_key), to: Enrollments
@doc "Lists active enrollments."
@spec list_active_enrollments(map()) :: [Ecto.Schema.t()]
defdelegate list_active_enrollments(filters \\ %{}), to: Enrollments
@doc "Deprecated unscoped enrollment lookup. Use `get_enrollment/4`."
@spec get_enrollment(Ecto.UUID.t(), binary(), binary()) :: no_return()
defdelegate get_enrollment(sequence_id, subscriber_type, subscriber_id), to: Enrollments
@doc "Gets an enrollment by sequence, subscriber identity, and explicit tenant scope."
@spec get_enrollment(Ecto.UUID.t(), binary(), binary(), binary() | nil) :: Ecto.Schema.t() | nil
defdelegate get_enrollment(sequence_id, subscriber_type, subscriber_id, tenant_key),
to: Enrollments
@doc "Replays a failed step execution with a fresh idempotency key."
@spec replay(Ecto.UUID.t()) :: {:ok, Ecto.Schema.t()} | {:error, term()}
defdelegate replay(step_execution_id), to: Dispatch
@doc "Returns webhook routes declared by active channel adapters."
@spec webhook_routes() :: [Channel.webhook_route()]
defdelegate webhook_routes, to: Web
end