# バッチ設計書 21-Campaigns::TriggerOneoffCampaignJob

## 概要

本ドキュメントは、Chatwootシステムにおけるワンオフキャンペーン実行ジョブ（Campaigns::TriggerOneoffCampaignJob）の設計仕様を記載したものである。

### 本バッチの処理概要

本バッチは、スケジュールされたワンオフ（一回限り）キャンペーンを実行し、対象オーディエンスにメッセージを配信する処理を行う。

**業務上の目的・背景**：企業がマーケティングや顧客エンゲージメントの一環として、SMS（Twilio/一般SMS）やWhatsAppを通じて一斉にメッセージを配信する必要がある。ワンオフキャンペーンは、特定の日時にスケジュールされた単発のマーケティング活動を自動化し、人手を介さずに大量の顧客にリーチすることを可能にする。これにより、マーケティング担当者は事前にキャンペーンを設定し、指定時刻に自動配信することで業務効率を向上させることができる。

**バッチの実行タイミング**：TriggerScheduledItemsJobから5分毎にトリガーされる。過去3日以内にスケジュールされたアクティブなワンオフキャンペーンが対象となる。

**主要な処理内容**：
1. 親ジョブ（TriggerScheduledItemsJob）から渡されたキャンペーンオブジェクトを受け取る
2. キャンペーンのtrigger!メソッドを呼び出す
3. インボックスの種類に応じて適切なサービスを選択（Twilio SMS、SMS、WhatsApp）
4. 対象オーディエンス（ラベルでタグ付けされたコンタクト）にメッセージを配信
5. キャンペーンステータスを「completed」に更新

**前後の処理との関連**：本バッチはTriggerScheduledItemsJob（5分毎実行）から呼び出される子ジョブである。TriggerScheduledItemsJobがアクティブなワンオフキャンペーンを検索し、条件に合致するキャンペーンごとに本ジョブをキューイングする。

**影響範囲**：campaignsテーブルのステータス更新、外部SMS/WhatsAppプロバイダへのAPI呼び出し、対象コンタクトへのメッセージ配信

## バッチ種別

通知配信 / マーケティングキャンペーン実行

## 実行スケジュール

| 項目 | 内容 |
|-----|------|
| 実行頻度 | 5分毎（親ジョブ経由） |
| 実行時刻 | 毎時 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55分 |
| 実行曜日 | 毎日 |
| 実行日 | 毎日 |
| トリガー | TriggerScheduledItemsJob（cron: */5 * * * *） |

## 実行条件

### 前提条件

| 条件 | 説明 |
|-----|------|
| キャンペーンタイプ | one_off（ワンオフ）であること |
| キャンペーンステータス | active（アクティブ）であること |
| スケジュール日時 | scheduled_atが過去3日以内かつ現在時刻以前 |
| インボックス種別 | Twilio SMS、SMS、またはWhatsAppであること |
| WhatsAppの場合 | whatsapp_campaign機能が有効であること |

### 実行可否判定

```ruby
# TriggerScheduledItemsJobでの対象抽出
Campaign.where(
  campaign_type: :one_off,
  campaign_status: :active
).where(scheduled_at: 3.days.ago..Time.current)

# trigger!メソッド内での判定
return unless one_off?      # ワンオフキャンペーンのみ
return if completed?        # 完了済みは処理しない
```

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | デフォルト値 | 説明 |
|-------------|-----|-----|-------------|------|
| campaign | Campaign | Yes | - | 実行対象のキャンペーンオブジェクト |

### 入力データソース

| データソース | 形式 | 説明 |
|-------------|------|------|
| campaigns | DB | キャンペーン情報（message、audience、template_params等） |
| contacts | DB | オーディエンス対象のコンタクト情報 |
| labels | DB | オーディエンス抽出用のラベル情報 |
| inboxes | DB | インボックス情報（種別判定用） |

## 出力仕様

### 出力データ

| 出力先 | 形式 | 説明 |
|-------|------|------|
| campaigns | DB | campaign_statusをcompletedに更新 |
| 外部API | HTTP | SMS/WhatsAppメッセージの送信 |

### 出力ファイル仕様

ファイル出力なし

## 処理フロー

### 処理シーケンス

```
1. performメソッド実行
   └─ campaign.trigger!を呼び出し

2. trigger!メソッド内での判定
   └─ one_off?かつ!completed?の場合のみ続行

3. execute_campaign呼び出し
   └─ インボックス種別に応じてサービスを選択

4. 各キャンペーンサービスでの処理
   ├─ Twilio SMS: Twilio::OneoffSmsCampaignService
   ├─ SMS: Sms::OneoffSmsCampaignService
   └─ WhatsApp: Whatsapp::OneoffCampaignService

5. キャンペーンサービス内処理
   ├─ バリデーション実行
   ├─ campaign.completed!でステータス更新
   ├─ オーディエンスラベル抽出
   ├─ ラベル付きコンタクト取得
   └─ 各コンタクトへメッセージ送信
```

### フローチャート

```mermaid
flowchart TD
    A[バッチ開始] --> B[campaign.trigger!]
    B --> C{one_off?}
    C -->|No| Z[処理終了]
    C -->|Yes| D{completed?}
    D -->|Yes| Z
    D -->|No| E[execute_campaign]
    E --> F{inbox_type判定}
    F -->|Twilio SMS| G[Twilio::OneoffSmsCampaignService]
    F -->|SMS| H[Sms::OneoffSmsCampaignService]
    F -->|WhatsApp| I{whatsapp_campaign有効?}
    I -->|No| Z
    I -->|Yes| J[Whatsapp::OneoffCampaignService]
    G --> K[campaign.completed!]
    H --> K
    J --> K
    K --> L[オーディエンスラベル抽出]
    L --> M[コンタクト取得]
    M --> N[各コンタクトへメッセージ送信]
    N --> Z
```

## データベース操作仕様

### 操作別データベース影響一覧

| 処理 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| ステータス更新 | campaigns | UPDATE | campaign_statusをcompletedに更新 |
| オーディエンス取得 | labels | SELECT | オーディエンスラベル情報の取得 |
| コンタクト取得 | contacts | SELECT | ラベル付きコンタクトの取得 |
| タグ取得 | taggings | SELECT | コンタクトのタグ情報取得 |

### テーブル別操作詳細

#### campaigns

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | campaign_status | completed (1) | 処理開始時に即座に更新 |
| UPDATE | updated_at | 現在時刻 | 自動更新 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | RuntimeError | 無効なキャンペーン | ログ出力、処理中断 |
| - | RuntimeError | 完了済みキャンペーン | ログ出力、処理中断 |
| - | RuntimeError | WhatsApp Cloud以外のプロバイダ | ログ出力、処理中断 |
| - | Twilio::REST::TwilioError | Twilio API エラー | ログ出力、次のコンタクトへ継続 |
| - | StandardError | SMS/WhatsApp送信エラー | ログ出力、次のコンタクトへ継続 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | Sidekiqデフォルト設定に依存 |
| リトライ間隔 | Sidekiqデフォルト設定に依存 |
| リトライ対象エラー | ジョブ実行時の一時的エラー |

### 障害時対応

- キャンペーンステータスが処理開始時に即座にcompletedに更新されるため、同一キャンペーンの重複実行は発生しない
- 個別コンタクトへの送信エラーは次のコンタクト処理に継続（フェイルセーフ設計）
- エラー発生時はRails.loggerにエラーログを出力

## トランザクション仕様

| 項目 | 内容 |
|-----|------|
| トランザクション範囲 | キャンペーンステータス更新は個別、コンタクト送信は非トランザクション |
| コミットタイミング | ステータス更新時に即座にコミット |
| ロールバック条件 | なし（ステータス更新後は継続処理） |

## パフォーマンス要件

| 項目 | 内容 |
|-----|------|
| 想定処理件数 | キャンペーンあたり数百〜数千コンタクト |
| 目標処理時間 | コンタクト数に依存 |
| メモリ使用量上限 | 特に制限なし |

## 排他制御

- campaign.completed!により処理開始時にステータスを更新し、同一キャンペーンの重複実行を防止
- 5分毎のスケジューラー実行間隔により、処理完了前の再実行を抑制

## ログ出力

| ログ種別 | 出力タイミング | 出力内容 |
|---------|--------------|---------|
| 進捗ログ | コンタクト処理時 | Processing contact: {name} ({phone}) |
| 進捗ログ | スキップ時 | Skipping contact {name} - no phone number |
| 進捗ログ | キャンペーン完了時 | Campaign {id} processing completed |
| エラーログ | 送信失敗時 | Failed to send to {phone}: {message} |

## 監視・アラート

| 監視項目 | 閾値 | アラート先 |
|---------|-----|----------|
| ジョブ実行エラー | 発生時 | Sidekiq監視 |
| 外部API障害 | 連続エラー発生時 | 運用担当 |

## 備考

- WhatsAppキャンペーンはwhatsapp_campaign機能フラグが有効なアカウントのみ利用可能
- WhatsAppキャンペーンはWhatsApp Cloudプロバイダのみ対応
- オーディエンスはラベルベースで指定され、指定ラベルが付与されたコンタクトが対象となる
- メッセージ内容はLiquidテンプレートで動的に生成可能
