> ## Documentation Index
> Fetch the complete documentation index at: https://docs.evomarketing.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Services

> Business logic service objects in EVO Dialed backend

Business logic is organized into service objects under `app/services/`. This keeps controllers thin and models focused on data.

## Service organization

```
app/services/
  brands/           # Brand lifecycle
  campaigns/        # Campaign operations
  creators/         # Creator management
  metrics/          # Data ingestion and sync
  chat/             # Chat system
  notifications/    # Multi-channel notifications
  integrations/     # External services
  ...
```

## Key service categories

### Brands (`/brands/`)

| Service                | Purpose                            |
| ---------------------- | ---------------------------------- |
| `PipelineStatusChange` | Single write path for brand status |
| `PipelineAutomation`   | Automated stage transitions        |
| `HubPasscode`          | Client hub PIN management          |
| `StrategySlackUpdate`  | Slack strategy notifications       |

**Critical:** Always use `Brands::PipelineStatusChange` to change brand status:

```ruby theme={null}
Brands::PipelineStatusChange.new(
  brand: brand,
  new_status: "active",
  actor: current_user
).perform!
```

### Campaigns (`/campaigns/`)

| Service                   | Purpose                       |
| ------------------------- | ----------------------------- |
| `PlacementAllocator`      | Attributes posts to campaigns |
| `SubmissionLifecycle`     | Handles first-post triggers   |
| `PlacementCompletion`     | Contract completion logic     |
| `RolloverActiveContracts` | Overflow post handling        |

### Creators (`/creators/`)

| Service              | Purpose                       |
| -------------------- | ----------------------------- |
| `AuthAccountManager` | Account linking               |
| `TierPromoter`       | Tier advancement              |
| `BadgeEvaluator`     | Badge calculations            |
| `BenchmarkLadder`    | Performance benchmarking      |
| `Rejector`           | 60-day ban + rejection letter |
| `FlagAutomation`     | Automated flagging            |

**Rejection flow:**

```ruby theme={null}
Creators::Rejector.new(
  user: creator,
  reason: "performance",
  notes: "Consistently missed cadence"
).perform!
```

### Metrics (`/metrics/`)

| Service                    | Purpose                           |
| -------------------------- | --------------------------------- |
| `Ingestor`                 | Main post ingestion (\~800 lines) |
| `SyncFailureReport`        | Health monitoring                 |
| `ScrapeEligibility`        | Determines scrape permissions     |
| `UnattributedPostResolver` | Handles orphaned posts            |

**Ingestion flow:**

```ruby theme={null}
Metrics::Ingestor.new.ingest_for_social(social, posts)
```

### Chat (`/chat/`)

| Service          | Purpose                       |
| ---------------- | ----------------------------- |
| `BrandSync`      | Creates/syncs brand channels  |
| `TierChannels`   | Rank-based chat rooms         |
| `VoiceModerator` | LiveKit voice moderation      |
| `Broadcaster`    | ActionCable real-time updates |

### Notifications (`/notifications/`)

| Service                   | Purpose                         |
| ------------------------- | ------------------------------- |
| `Kinds`                   | All notification type constants |
| `EarlyViralAlertNotifier` | 100K viral alerts               |
| `PushDirect`              | Push notification delivery      |
| `Outbound`                | Multi-channel dispatch          |

**Notification dispatch:**

```ruby theme={null}
Mc::NotificationDispatcher.notify!(
  recipient_user_id: user_id,
  kind: Notifications::Kinds::EARLY_VIRAL_ALERT,
  title: "Going viral!",
  body: "Your post hit 100K views",
  target_url: "/brands/123"
)
```

### Integrations (`/integrations/`)

| Service                 | Purpose               |
| ----------------------- | --------------------- |
| `ScrapeCreatorsClient`  | Social media scraping |
| `SocialOauthClient`     | Official API access   |
| `ZendeskClient`         | Support tickets       |
| `SlackClient`           | Team notifications    |
| `TremendousClient`      | Gift card rewards     |
| `TwilioClient`          | SMS delivery          |
| `ContentAnalyzerClient` | AI content analysis   |

## Service patterns

### Single responsibility

Each service handles one operation:

```ruby theme={null}
# Good - single purpose
class Creators::TierPromoter
  def initialize(user:)
    @user = user
  end

  def perform!
    return unless eligible?
    @user.update!(tier: next_tier)
  end
end
```

### Transaction safety

Services wrap mutations in transactions:

```ruby theme={null}
def perform!
  ActiveRecord::Base.transaction do
    create_records!
    send_notifications!
    update_status!
  end
end
```

### Return values

Services return meaningful results:

```ruby theme={null}
def perform!
  # Returns the created record or result object
  submission = CampaignSubmission.create!(attributes)
  Result.success(submission)
rescue ActiveRecord::RecordInvalid => e
  Result.failure(e.message)
end
```

## Calling services

From controllers:

```ruby theme={null}
def create
  result = Campaigns::PlacementAllocator.new(
    social: @social,
    posts: post_params
  ).perform!

  if result.success?
    render json: result.data
  else
    render json: { error: result.error }, status: :unprocessable_entity
  end
end
```

From jobs:

```ruby theme={null}
class Metrics::SyncJob < ApplicationJob
  def perform(social_id:)
    social = CreatorSocial.find(social_id)
    posts = fetch_posts(social)
    Metrics::Ingestor.new.ingest_for_social(social, posts)
  end
end
```

## Testing services

Services have dedicated tests in `test/services/`:

```ruby theme={null}
class Creators::TierPromoterTest < ActiveSupport::TestCase
  test "promotes user to next tier when eligible" do
    user = create_user(tier: "rocket", total_views: 15_000_000)
    
    Creators::TierPromoter.new(user: user).perform!
    
    assert_equal "moonwalker", user.reload.tier
  end
end
```
