# Recipient Resolution

AshDispatch automatically resolves recipients using **DSL-based configuration** - define once in config, works everywhere!

## Quick Start (Zero Configuration!)

### 1. Configure Your App Structure

Tell AshDispatch about your User module and audience filters:

```elixir
# config/config.exs
config :ash_dispatch,
  user_module: MyApp.Accounts.User,
  recipient_filters: [
    audiences: [
      # Bare atom = extract from relationship with same name
      :user,                      # Extract from :user relationship
      :creator,                   # Extract from :creator relationship

      # Relationship path + filter: query users matching filter
      admin: [:user, admin: true],
      partner: [:user, role: :partner],

      # Relationship chain: follow multiple relationships
      sellers: [:user, :associated_seller],

      # Template filters with dynamic values
      regional_admin: [:user, admin: true, region: {:resource, [:user, :region]}]
    ]
  ]
```

### 2. That's It!

AshDispatch now automatically:
- ✅ Resolves recipients based on DSL-configured filters
- ✅ Queries admins/partners/custom audiences using your filters
- ✅ Extracts emails (even CiString types!)
- ✅ Handles user preferences automatically
- ✅ Works with ANY Ash resource structure
- ✅ Supports custom audiences with zero code changes

## How It Works

### Auto-Inference Pattern

AshDispatch supports **multiple audience configuration formats** with intelligent auto-inference:

#### Format 1: Bare Atom (Auto-Infer)

The simplest pattern - just list the audience, and AshDispatch extracts from the relationship with the same name:

```elixir
config :ash_dispatch,
  recipient_filters: [
    audiences: [
      :user,      # Extract from :user relationship
      :creator,   # Extract from :creator relationship
      :assignee   # Extract from :assignee relationship
    ]
  ]

# For an Order with belongs_to :user relationship:
%{order: %Order{user: %User{}}}

# AshDispatch automatically:
# 1. Sees :user in bare atom list
# 2. Looks for :user relationship on Order
# 3. Extracts the user
```

**When to use:** Relationship name matches audience name (most common case!)

---

#### Format 2: Relationship Path + Filter

Query users matching a filter via a specified relationship:

```elixir
config :ash_dispatch,
  recipient_filters: [
    audiences: [
      admin: [:user, admin: true],
      partner: [:user, role: :partner, active: true],
      support: [:user, role: :support]
    ]
  ]

# AshDispatch queries:
MyApp.Accounts.User
|> Ash.Query.filter(admin == true)
|> Ash.read!()
```

**When to use:** Notifying groups of users (admins, partners, etc.)

---

#### Format 3: Relationship Chain

Follow multiple relationships to reach recipients:

```elixir
config :ash_dispatch,
  recipient_filters: [
    audiences: [
      # Follow user.associated_seller
      sellers: [:user, :associated_seller],

      # Follow user.team.lead
      team_lead: [:user, :team, :lead]
    ]
  ]

# For an Order with user.associated_seller:
%{order: %Order{user: %User{associated_seller: %Seller{}}}}

# AshDispatch:
# 1. Extracts user from order
# 2. Follows associated_seller relationship
# 3. Returns the seller as recipient
```

**When to use:** Recipients accessed via relationship chains

---

#### Format 4: Template Filters (Dynamic Values)

Filters with values extracted from event context:

```elixir
config :ash_dispatch,
  recipient_filters: [
    audiences: [
      # Admins in the same region as the event's user
      regional_admin: [:user, admin: true, region: {:resource, [:user, :region]}]
    ]
  ]

# For event with user in region "EU":
%{order: %Order{user: %User{region: "EU"}}}

# AshDispatch resolves:
# 1. Extracts [:user, :region] from context → "EU"
# 2. Queries: User |> filter(admin == true and region == "EU")
# 3. Returns all EU admins
```

**When to use:** Dynamic filters based on event context (regional routing, team assignment)

---

#### Format 5: Function/MFA (Full Dynamic Logic)

Call a function for complex recipient resolution:

```elixir
config :ash_dispatch,
  recipient_filters: [
    audiences: [
      on_duty_admin: {MyApp.AdminResolver, :get_on_duty, [:region]}
    ]
  ]

# Your resolver module:
defmodule MyApp.AdminResolver do
  def get_on_duty(region) do
    # Complex logic: check rotation schedule, time zones, etc.
    [admin: true, on_duty: true, region: region]
  end
end
```

**When to use:** Complex logic like on-call rotations, escalation chains

---

#### Format 6: System Audience (Static Recipients)

Static email addresses for system notifications:

```elixir
config :ash_dispatch,
  system_recipients: [
    %{email: "ops@myapp.com", name: "Operations"},
    %{email: "monitoring@pagerduty.com", name: "PagerDuty"}
  ]
```

**When to use:** External services, ops team emails

### Future-Proof Design

Adding new resources? **Zero code changes needed!**

```elixir
# Add a new Invoice resource
defmodule MyApp.Invoices.Invoice do
  # ...
  belongs_to :user, MyApp.Accounts.User
end

# Dispatch event with invoice
AshDispatch.Dispatcher.dispatch(
  "invoices.created",
  %{invoice: invoice}  # <-- User auto-extracted via relationship!
)
```

AshDispatch introspects the `Invoice` resource, finds the `:user` relationship, and extracts the user automatically.

## Real-World Example

Here's a complete event from production (Magasin):

```elixir
defmodule MyApp.Events.NewResellerRequest do
  use AshDispatch.Event

  @impl true
  def id, do: "requests.new_reseller_request"

  @impl true
  def channels(_context) do
    [
      # Send in-app notification to all admins
      %Channel{transport: :in_app, audience: :admin, time: {:in, 0}},

      # Send email to all admins
      %Channel{transport: :email, audience: :admin, time: {:in, 0}}
    ]
  end

  # NO recipients/2 override needed!
  # AshDispatch automatically:
  # 1. Queries MyApp.Accounts.User
  # 2. Filters by [super_admin: true]
  # 3. Extracts emails (handles CiString!)
  # 4. Sends to all admins
end
```

**Configuration:**
```elixir
config :ash_dispatch,
  user_module: MyApp.Accounts.User,
  admin_filter: [super_admin: true]
```

**That's it!** No resolver module, no hardcoded patterns, no maintenance.

## Audience Types

### `:user` - Extracted Automatically

AshDispatch finds the user via Ash introspection:

```elixir
# Works with direct user
%{user: %User{}}

# Works with nested user in any resource
%{order: %Order{user: %User{}}}
%{ticket: %Ticket{user: %User{}}}
%{invoice: %Invoice{user: %User{}}}
# ... any resource with a user relationship!
```

**How it works:**
1. Check if any data value IS the configured `user_module`
2. Use `Ash.Resource.Info.relationships/1` to find relationships to `user_module`
3. Extract user from that relationship

**Use when:**
- Notifying the person who triggered the action
- User-specific notifications
- Order confirmations, ticket updates, etc.

### `:admin` - Queried Automatically

AshDispatch queries admins using your configured filter:

```elixir
config :ash_dispatch,
  user_module: MyApp.Accounts.User,
  admin_filter: [super_admin: true]  # Your admin logic
```

**Examples:**
```elixir
# Simple boolean flag
admin_filter: [admin: true]

# Role-based
admin_filter: [role: :admin]

# Multiple conditions (keyword list becomes AND filter)
admin_filter: [active: true, role: :admin]
```

**Use when:**
- High-priority notifications
- Security alerts
- New user registrations
- System-wide announcements

### `:system` - Configured Recipients

Optional static recipients for system notifications:

```elixir
config :ash_dispatch,
  system_recipients: [
    %{email: "ops@myapp.com", name: "Operations"},
    %{email: "monitoring@pagerduty.com", name: "PagerDuty"}
  ]
```

**Use when:**
- Internal monitoring
- Ops team alerts
- External service integrations

## CiString Email Handling

AshDispatch automatically handles case-insensitive email types:

```elixir
# Your User resource
attribute :email, :ci_string  # or Ash.CiString, or CiString

# AshDispatch introspects attributes and handles:
%{string: "user@example.com"}  # CiString struct
%{data: "user@example.com"}    # Cldr.LanguageTag.CiString
"user@example.com"             # Plain string

# Zero configuration needed!
```

Uses `Ash.Resource.Info.attributes/1` to detect email type and extract correctly.

## Advanced: Custom Recipients

For the rare cases where you need custom logic (< 1% of events):

```elixir
defmodule MyApp.Events.EscalationAlert do
  use AshDispatch.Event

  @impl true
  def recipients(context, %Channel{audience: :admin}) do
    severity = context.data.alert.severity

    case severity do
      :critical ->
        # Critical: All admins + CTO
        MyApp.Accounts.User
        |> Ash.Query.filter_input(admin: true)
        |> Ash.Query.filter(role == :cto)
        |> Ash.read!()
        |> Enum.map(&normalize_user/1)

      _ ->
        # Default: Use smart defaults
        AshDispatch.Event.Helpers.resolve_recipients_for_audience(context, channel)
    end
  end

  defp normalize_user(user) do
    %{
      id: user.id,
      email: extract_email(user),
      display_name: user.display_name || user.email
    }
  end
end
```

**When to override:**
- Escalation chains based on severity
- Organization-scoped recipients
- On-call rotations
- Dynamic team assignment

**Otherwise:** Trust the smart defaults!

## Configuration Reference

### Complete Example

```elixir
config :ash_dispatch,
  # REQUIRED for user extraction and queries
  user_module: MyApp.Accounts.User,

  # Audience configuration supports multiple formats:
  recipient_filters: [
    audiences: [
      # Format 1: Bare atoms (extract from relationship with same name)
      :user,                      # Extract from :user relationship
      :creator,                   # Extract from :creator relationship

      # Format 2: Relationship path + filter (query users matching filter)
      admin: [:user, admin: true],
      partner: [:user, role: :partner, active: true],

      # Format 3: Relationship chain (follow multiple relationships)
      sellers: [:user, :associated_seller],
      team_lead: [:user, :team, :lead],

      # Format 4: Template filters (dynamic values from context)
      regional_admin: [:user, admin: true, region: {:resource, [:user, :region]}],

      # Format 5: Function/MFA (complex logic)
      on_duty: {MyApp.AdminResolver, :get_on_duty, []}

      # Format 6: System audience (configured separately below)
    ]
  ],

  # Static recipients for :system audience
  system_recipients: [
    %{email: "ops@example.com", name: "Operations"},
    %{email: "monitoring@pagerduty.com", name: "PagerDuty"}
  ]
```

### Format Quick Reference

| Format | Example | Use Case |
|--------|---------|----------|
| **Bare Atom** | `:user` | Extract from relationship with same name |
| **Relationship + Filter** | `admin: [:user, admin: true]` | Query users by attributes |
| **Relationship Chain** | `sellers: [:user, :associated_seller]` | Follow multiple relationships |
| **Template Filter** | `regional_admin: [:user, admin: true, region: {:resource, [...]}]` | Dynamic filters from context |
| **Function/MFA** | `on_duty: {Mod, :fun, []}` | Complex resolution logic |
| **System** | `system_recipients: [...]` | Static email addresses |

### Filter Query Examples

```elixir
# Simple boolean
admin: [:user, admin: true]

# Role-based
partner: [:user, role: :partner]

# Multiple conditions (AND)
active_admin: [:user, admin: true, active: true]

# Multiple values (OR - uses list)
manager: [:user, role: [:admin, :manager]]
```

### Template Filter Examples

```elixir
# Regional routing - admins in same region as user
regional_admin: [:user, admin: true, region: {:resource, [:user, :region]}]

# Team-based - users in same team
team_lead: [:user, role: :lead, team_id: {:resource, [:order, :team_id]}]

# Nested extraction
sales_manager: [:user, role: :manager, department: {:resource, [:user, :profile, :department]}]
```

## Extending DeliveryReceipt with User Relationship

AshDispatch provides a **Base module pattern** for creating DeliveryReceipt resources in your app with proper user relationships and TypeScript integration.

### Pattern 1: Base Module with Manual Relationship (Recommended)

The cleanest approach - use the Base module for all DSL, add your own relationships:

```elixir
defmodule MyApp.Deliveries.DeliveryReceipt do
  @moduledoc """
  Delivery receipt tracking - extends base from ash_dispatch with user relationship.
  """

  # Use base from ash_dispatch with ALL the DSL (attributes, actions, etc.)
  use AshDispatch.Resources.DeliveryReceipt.Base,
    repo: MyApp.Repo,
    domain: MyApp.Deliveries

  # TypeScript configuration (helps verifier detect AshTypescript.Resource extension)
  typescript do
    type_name("DeliveryReceipt")
  end

  # Add user relationship (only thing you need to define!)
  relationships do
    belongs_to :user, MyApp.Accounts.User do
      source_attribute :user_id
      destination_attribute :id
      allow_nil? true
      public? true
    end
  end
end
```

**What you get:**
- ✅ All attributes, actions, state machine from Base
- ✅ Explicit user relationship (clear ownership)
- ✅ TypeScript types with nested user support
- ✅ Full control over customization
- ✅ No magic - just clear module extension

---

### Pattern 2: Base Module with Auto Relationship

If you want the Base to create the relationship automatically:

```elixir
defmodule MyApp.Deliveries.DeliveryReceipt do
  use AshDispatch.Resources.DeliveryReceipt.Base,
    repo: MyApp.Repo,
    domain: MyApp.Deliveries,
    user_resource: MyApp.Accounts.User  # ← Auto-creates belongs_to :user

  # TypeScript types are automatically configured
  # User relationship is automatically added
end
```

**What happens:**
1. Base module injects all DSL (attributes, actions, policies, state machine)
2. If `user_resource` is provided, adds `belongs_to :user` relationship
3. Includes TypeScript extension automatically

---

### Base Module Benefits

**Compared to manual resource definition:**
- ❌ Manual: ~500 lines of duplicated DSL
- ✅ Base: ~20 lines for extension + relationships

**Compared to direct `use AshDispatch.Resources.DeliveryReceipt`:**
- ❌ Direct: No user relationship (library can't reference app modules)
- ✅ Base: Proper `belongs_to` relationship with your User module

**Features inherited from Base:**
- All attributes (event_id, transport, status, provider_response, etc.)
- State machine (pending → scheduled → sending → sent/failed)
- All actions (create, mark_sent, mark_failed, retry, etc.)
- Policies (workers bypass, admins can read)
- Calculations (oban_job)

---

### Usage in Your App

Once extended, use like any Ash resource:

```elixir
# Query receipts with user loaded
receipts = MyApp.Deliveries.DeliveryReceipt
|> Ash.Query.load(:user)
|> Ash.Query.filter(status == :sent)
|> Ash.read!()

# Create receipts (usually done by AshDispatch internally)
MyApp.Deliveries.DeliveryReceipt
|> Ash.Changeset.for_create(:create, %{
  event_id: "orders.created",
  transport: :email,
  user_id: user.id,
  recipient: user.email,
  status: :pending
})
|> Ash.create!()
```

---

### TypeScript Integration

With Pattern 1 (manual relationship), TypeScript types include the user:

```typescript
// Auto-generated from AshTypescript
interface DeliveryReceipt {
  id: string;
  event_id: string;
  transport: "email" | "in_app" | "discord";
  status: "pending" | "scheduled" | "sending" | "sent" | "failed";
  user_id: string;
  user?: User;  // ← Available for nested loading!
  recipient: string;
  subject?: string;
  body_html?: string;
  // ... all other fields
}

// Query with nested user
const receipts = await ashClient.query(DeliveryReceipt, {
  load: ["user"]
});
// receipts[0].user.email is available!
```

---

### Customization Examples

**Add custom policies:**

```elixir
defmodule MyApp.Deliveries.DeliveryReceipt do
  use AshDispatch.Resources.DeliveryReceipt.Base,
    repo: MyApp.Repo,
    domain: MyApp.Deliveries

  relationships do
    belongs_to :user, MyApp.Accounts.User do
      source_attribute :user_id
      destination_attribute :id
      allow_nil? true
      public? true
    end
  end

  # Add custom policies
  policies do
    # Users can read their own receipts
    policy action(:list_for_user) do
      authorize_if actor_attribute_equals(:id, arg(:user_id))
    end
  end
end
```

**Add custom actions:**

```elixir
defmodule MyApp.Deliveries.DeliveryReceipt do
  use AshDispatch.Resources.DeliveryReceipt.Base,
    repo: MyApp.Repo,
    domain: MyApp.Deliveries

  relationships do
    belongs_to :user, MyApp.Accounts.User do
      source_attribute :user_id
      destination_attribute :id
      allow_nil? true
      public? true
    end
  end

  # Add custom action
  actions do
    read :list_failed_for_retry do
      description "Find failed receipts ready for retry"

      filter expr(
        status == :failed and
        retry_count < 5 and
        (is_nil(last_retry_at) or last_retry_at < ago(15, :minute))
      )
    end
  end
end
```

---

### Migration from Direct Usage

**Before (if you were using the library resource directly):**
```elixir
# You couldn't - library doesn't know your User module
alias AshDispatch.Resources.DeliveryReceipt
```

**After (with Base pattern):**
```elixir
# In your app:
defmodule MyApp.Deliveries.DeliveryReceipt do
  use AshDispatch.Resources.DeliveryReceipt.Base,
    repo: MyApp.Repo,
    domain: MyApp.Deliveries

  relationships do
    belongs_to :user, MyApp.Accounts.User do
      source_attribute :user_id
      destination_attribute :id
      allow_nil? true
      public? true
    end
  end
end

# Now use your resource:
alias MyApp.Deliveries.DeliveryReceipt
```

This pattern allows AshDispatch to be **library-friendly** while providing **first-class relationships** in consuming apps!

---

## Migration from RecipientResolver

If you're migrating from the old `RecipientResolver` behaviour:

**Before (old pattern):**
```elixir
# lib/my_app/recipient_resolver.ex (130 lines of cond chains!)
defmodule MyApp.RecipientResolver do
  @behaviour AshDispatch.RecipientResolver

  def extract_user_id(%{data: data}) do
    cond do
      user = Map.get(data, :user) -> get_id(user)
      order = Map.get(data, :order) ->
        case Map.get(order, :user) do
          nil -> :error
          user -> get_id(user)
        end
      # ... 40+ more lines
    end
  end

  def resolve_admins(_context) do
    # Custom query logic
  end
end
```

**After (DSL-based pattern):**
```elixir
# config/config.exs (just configuration!)
config :ash_dispatch,
  user_module: MyApp.Accounts.User,
  recipient_filters: [
    audiences: [
      :user,
      admin: [:user, admin: true],
      partner: [:user, role: :partner]
    ]
  ]

# Delete lib/my_app/recipient_resolver.ex entirely
```

**Benefits:**
- ❌ Delete 130+ lines of brittle code
- ✅ Future-proof for new resources and audiences
- ✅ Zero maintenance - just update config
- ✅ Automatic CiString handling
- ✅ Works with ANY Ash resource structure
- ✅ Add new audiences without code changes

## Troubleshooting

### "No :user_module configured"

**Problem:** Warning logged when dispatching events.

**Cause:** Missing `user_module` configuration.

**Solution:**
```elixir
config :ash_dispatch,
  user_module: MyApp.Accounts.User
```

### "Failed to query admin recipients"

**Problem:** Error when dispatching to `:admin` audience.

**Cause:** Invalid `admin_filter` or User resource not queryable.

**Solution:**
1. Check `admin_filter` syntax matches your User attributes
2. Verify User resource has matching attribute
3. Check Ash read action is accessible

### "No user found in context"

**Problem:** Warning for `:user` audience events.

**Cause:** Event data doesn't contain user or resource with user relationship.

**Solution:**
1. Ensure event data includes user or related resource
2. Check resource has `belongs_to :user, YourUserModule`
3. Verify relationship is loaded if using nested extraction

### User Preferences Not Working

**Problem:** Users still receiving notifications they opted out of.

**Cause:** Preference checking requires additional setup.

**Solution:** See [User Preferences](user-preferences.md) guide.

## Performance Considerations

### Admin Query Caching

For frequently-dispatched admin events, consider caching:

```elixir
defmodule MyApp.AdminCache do
  use GenServer

  # Cache admin list, invalidate on user changes
  def get_admins do
    GenServer.call(__MODULE__, :get_admins)
  end

  def invalidate do
    GenServer.cast(__MODULE__, :invalidate)
  end
end

# In User resource
change fn changeset, _context ->
  if Ash.Changeset.changing_attribute?(changeset, :admin) do
    MyApp.AdminCache.invalidate()
  end
  changeset
end
```

### Relationship Preloading

If recipient resolution needs related data, preload in events:

```elixir
@impl true
def recipients(context, channel) do
  # Load relationships before normalizing
  AshDispatch.Event.Helpers.resolve_recipients_for_audience(context, channel)
  |> Ash.load!([:profile, :preferences])
end
```

## Testing

### Test Configuration

Use simpler filters in test environment:

```elixir
# config/test.exs
config :ash_dispatch,
  user_module: MyApp.Accounts.User,
  admin_filter: [test_admin: true]  # Use test-specific flag
```

### Factory Integration

Works seamlessly with test factories:

```elixir
test "dispatches to admins" do
  admin = build(:user, %{admin: true})
  insert!(admin)

  AshDispatch.Dispatcher.dispatch("events.admin_alert", %{data: %{}})

  # Verify admin received notification
  assert_email_sent(to: admin.email)
end
```

## Next Steps

- [Getting Started](../tutorials/getting-started.md) - Define your first events
- [User Preferences](user-preferences.md) - Let users control notifications
- [Counter Broadcasting](counter-broadcasting.md) - Real-time updates
- [Phoenix Integration](phoenix-integration.md) - Channel and frontend setup
