# 機能設計書 38-キャンペーン管理

## 概要

本ドキュメントは、Medusaのプロモーションモジュールにおけるキャンペーン管理機能の設計を記載する。

### 本機能の処理概要

キャンペーン管理機能は、複数のプロモーションをまとめて管理するためのキャンペーンの作成・更新・削除を行う機能である。キャンペーンには開始日・終了日、予算、識別子（campaign_identifier）を設定でき、関連するプロモーションの一括管理が可能。

**業務上の目的・背景**：季節セールやイベントなど、特定の期間に複数のプロモーションを展開する際に、キャンペーンとしてまとめて管理することで、予算管理や効果測定が容易になる。

**機能の利用シーン**：
1. 新しいキャンペーンを作成する場面
2. キャンペーンの期間や名前を更新する場面
3. キャンペーンにプロモーションを追加・削除する場面
4. キャンペーンを終了・削除する場面

**主要な処理内容**：
1. キャンペーンの一覧取得・検索
2. キャンペーンの新規作成（予算の同時作成含む）
3. キャンペーンの更新
4. キャンペーンの削除
5. プロモーションのキャンペーンへの追加・削除

**関連システム・外部連携**：特になし

**権限による制御**：管理者（Admin）権限を持つユーザーのみがキャンペーンの作成・更新・削除が可能。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 62 | キャンペーン追加画面 | 主機能 | プロモーションをキャンペーンに追加 |
| 64 | キャンペーン一覧画面 | 主機能 | キャンペーン一覧の取得・表示 |
| 65 | キャンペーン作成画面 | 主機能 | 新規キャンペーンの作成 |
| 66 | キャンペーン詳細画面 | 主機能 | キャンペーン詳細の表示 |
| 67 | キャンペーン編集画面 | 主機能 | キャンペーン情報の編集 |
| 68 | キャンペーン設定画面 | 主機能 | キャンペーン設定の編集 |

## 機能種別

CRUD操作 / 関連管理

## 入力仕様

### 入力パラメータ

#### キャンペーン作成

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes | キャンペーン名 | 空文字不可、searchable |
| campaign_identifier | string | Yes | キャンペーン識別子 | 一意制約 |
| description | string | No | 説明 | searchable |
| starts_at | datetime | No | 開始日時 | - |
| ends_at | datetime | No | 終了日時 | - |
| budget | BudgetDTO | No | 予算設定 | 別表参照 |

#### 予算設定（BudgetDTO）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| type | enum | Yes | 予算タイプ | spend, usage |
| limit | number | No | 予算上限 | - |
| currency_code | string | 条件付き必須 | 通貨コード | typeがspendの場合必須 |

#### キャンペーン更新

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | string | Yes | キャンペーンID | 存在するIDであること |
| name | string | No | 更新後の名前 | - |
| description | string | No | 更新後の説明 | - |
| starts_at | datetime | No | 更新後の開始日時 | - |
| ends_at | datetime | No | 更新後の終了日時 | - |
| budget | BudgetUpdateDTO | No | 予算更新 | type, currency_codeは変更不可 |

#### プロモーション追加・削除

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | string | Yes | キャンペーンID | 存在するIDであること |
| add | string[] | No | 追加するプロモーションID | 他キャンペーンに未所属であること |
| remove | string[] | No | 削除するプロモーションID | - |

### 入力データソース

- 画面入力：管理画面からの入力
- DBテーブル：promotion_campaign, promotion_campaign_budget

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| campaign | CampaignDTO | キャンペーン情報 |
| campaign.id | string | キャンペーンID（prefix: procamp） |
| campaign.name | string | キャンペーン名 |
| campaign.description | string | 説明 |
| campaign.campaign_identifier | string | 識別子 |
| campaign.starts_at | datetime | 開始日時 |
| campaign.ends_at | datetime | 終了日時 |
| campaign.budget | CampaignBudgetDTO | 予算情報 |
| campaign.promotions | PromotionDTO[] | 関連プロモーション |

### 出力先

- 画面表示：管理画面のキャンペーン一覧・詳細
- DBテーブル：promotion_campaign, promotion_campaign_budget, promotion

## 処理フロー

### 処理シーケンス

```
1. キャンペーン一覧取得
   └─ remoteQueryObjectFromStringで取得

2. キャンペーン作成
   ├─ createCampaignsWorkflowの実行
   ├─ Campaignレコード作成
   ├─ 予算データがある場合
   │   ├─ validateCampaignBudgetDataでバリデーション
   │   └─ CampaignBudgetレコード作成
   └─ 作成したキャンペーンを返却

3. キャンペーン更新
   ├─ updateCampaignsWorkflowの実行
   ├─ 既存キャンペーン取得
   ├─ budget.type/currency_codeの変更チェック（不変）
   ├─ Campaignレコード更新
   ├─ 予算の更新または作成
   └─ 更新したキャンペーンを返却

4. キャンペーン削除
   ├─ deleteCampaignsWorkflowの実行
   └─ Campaignレコード削除（budgetはカスケード削除）

5. プロモーション追加
   ├─ addOrRemoveCampaignPromotionsWorkflowの実行
   ├─ プロモーションの所属チェック
   ├─ 通貨コード整合性チェック（SPEND予算の場合）
   └─ promotion.campaign_idを更新

6. プロモーション削除
   ├─ addOrRemoveCampaignPromotionsWorkflowの実行
   └─ promotion.campaign_idをnullに更新
```

### フローチャート

```mermaid
flowchart TD
    A[開始: キャンペーンCRUD] --> B{操作種別}
    B -->|一覧取得| C[remoteQuery]
    B -->|作成| D[createCampaignsWorkflow]
    B -->|更新| E[updateCampaignsWorkflow]
    B -->|削除| F[deleteCampaignsWorkflow]
    B -->|プロモーション管理| G[addOrRemoveCampaignPromotionsWorkflow]

    C --> H[結果返却]

    D --> D1[Campaignレコード作成]
    D1 --> D2{予算あり?}
    D2 -->|Yes| D3[validateCampaignBudgetData]
    D3 --> D4[CampaignBudgetレコード作成]
    D2 -->|No| H
    D4 --> H

    E --> E1[既存キャンペーン取得]
    E1 --> E2{type/currency_code変更?}
    E2 -->|Yes| E3[MedusaError: immutable]
    E2 -->|No| E4[Campaignレコード更新]
    E4 --> E5{予算更新?}
    E5 -->|既存あり| E6[CampaignBudget更新]
    E5 -->|新規| E7[CampaignBudget作成]
    E5 -->|なし| H
    E6 --> H
    E7 --> H

    F --> F1[キャンペーン削除（cascade: budget）]
    F1 --> H

    G --> G1{add処理}
    G1 --> G2[所属チェック]
    G2 --> G3{SPEND予算?}
    G3 -->|Yes| G4[通貨コード整合性チェック]
    G3 -->|No| G5[promotion.campaign_id更新]
    G4 --> G5
    G1 --> G6{remove処理}
    G6 --> G7[promotion.campaign_id = null]
    G5 --> H
    G7 --> H

    H --> I[終了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-38-001 | 識別子一意性 | campaign_identifierはシステム全体で一意 | 作成時 |
| BR-38-002 | 予算タイプ不変 | 作成後のbudget.typeは変更不可 | 更新時 |
| BR-38-003 | 通貨コード不変 | 作成後のbudget.currency_codeは変更不可 | 更新時 |
| BR-38-004 | SPEND予算通貨必須 | typeがSPENDの場合、currency_codeは必須 | 予算作成時 |
| BR-38-005 | プロモーション所属制限 | 1つのプロモーションは1つのキャンペーンにのみ所属 | プロモーション追加時 |
| BR-38-006 | 通貨コード整合性 | SPEND予算のキャンペーンには同じ通貨のプロモーションのみ追加可能 | プロモーション追加時 |
| BR-38-007 | カスケード削除 | キャンペーン削除時は予算も自動削除 | 削除時 |

### 計算ロジック

特に複雑な計算ロジックはなし。

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 一覧取得 | promotion_campaign | SELECT | キャンペーン取得 |
| 作成 | promotion_campaign | INSERT | キャンペーン作成 |
| 作成 | promotion_campaign_budget | INSERT | 予算作成（オプション） |
| 更新 | promotion_campaign | UPDATE | キャンペーン更新 |
| 更新 | promotion_campaign_budget | UPDATE/INSERT | 予算更新または新規作成 |
| 削除 | promotion_campaign | DELETE | キャンペーン削除（カスケード） |
| プロモーション追加 | promotion | UPDATE | campaign_id更新 |
| プロモーション削除 | promotion | UPDATE | campaign_id = null |

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

#### promotion_campaign

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | 自動生成（prefix: procamp） | - |
| INSERT | name | キャンペーン名 | searchable |
| INSERT | description | 説明文字列 | searchable、nullable |
| INSERT | campaign_identifier | 識別子 | 一意制約 |
| INSERT | starts_at | 開始日時 | nullable |
| INSERT | ends_at | 終了日時 | nullable |
| UPDATE | name | 更新後の名前 | - |
| UPDATE | description | 更新後の説明 | - |
| UPDATE | starts_at | 更新後の開始日時 | - |
| UPDATE | ends_at | 更新後の終了日時 | - |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| DUPLICATE_KEY | DBError | campaign_identifierが重複 | 別の識別子を指定 |
| INVALID_DATA | MedusaError | SPEND予算でcurrency_code未指定 | currency_codeを指定 |
| INVALID_DATA | MedusaError | 予算のtype/currency_codeを変更しようとした | type/currency_codeは変更不可 |
| NOT_FOUND | MedusaError | 存在しないキャンペーンの更新・削除 | 正しいIDを指定 |
| NOT_FOUND | MedusaError | プロモーションが別キャンペーンに所属または未存在 | 正しいプロモーションIDを指定 |
| INVALID_DATA | MedusaError | SPEND予算キャンペーンへの通貨不一致プロモーション追加 | 同じ通貨のプロモーションを使用 |

### リトライ仕様

- 通常のCRUD操作のため、特別なリトライ仕様なし

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

- ワークフローによるトランザクション管理
- キャンペーンと予算の作成は同一トランザクション
- @InjectTransactionManagerで保護

## パフォーマンス要件

- 一覧取得：100ms以内
- 作成・更新・削除：200ms以内
- インデックス：campaign_identifier（一意制約）

## セキュリティ考慮事項

- 管理者認証必須（AuthenticatedMedusaRequest）
- 予算情報は機密性が高いため、適切な権限管理が必要

## 備考

- キャンペーン識別子（campaign_identifier）は外部システムとの連携用（例：GA-123456）
- 予算のtype/currency_codeは不変であり、変更する場合は新しい予算を作成する必要がある
- プロモーションは1つのキャンペーンにのみ所属可能（N:1関係）

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | campaign.ts | `packages/modules/promotion/src/models/campaign.ts` | Campaignエンティティの構造 |

**読解のコツ**:
- **9行目**: idプレフィックスは`procamp`
- **10-11行目**: name、descriptionはsearchable
- **12行目**: campaign_identifierは一意識別子
- **13-14行目**: starts_at、ends_atは期間管理
- **15-19行目**: budgetとのhasOne関連（nullable）
- **20-22行目**: promotionsとのhasMany関連
- **25-27行目**: budgetへのカスケード削除
- **28-34行目**: campaign_identifierの一意制約インデックス

#### Step 2: APIルートを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | route.ts | `packages/medusa/src/api/admin/campaigns/route.ts` | 一覧取得・作成エンドポイント |
| 2-2 | route.ts | `packages/medusa/src/api/admin/campaigns/[id]/route.ts` | 詳細取得・更新・削除エンドポイント |
| 2-3 | route.ts | `packages/medusa/src/api/admin/campaigns/[id]/promotions/route.ts` | プロモーション追加・削除 |

**主要処理フロー**:
- **route.ts 13-36行目**: GET（一覧取得）- remoteQueryObjectFromString使用
- **route.ts 38-63行目**: POST（作成）- createCampaignsWorkflow使用
- **[id]/route.ts 14-32行目**: GET（詳細取得）
- **[id]/route.ts 34-70行目**: POST（更新）- updateCampaignsWorkflow使用
- **[id]/route.ts 72-88行目**: DELETE（削除）- deleteCampaignsWorkflow使用
- **[id]/promotions/route.ts 10-30行目**: POST（プロモーション追加・削除）- addOrRemoveCampaignPromotionsWorkflow使用

#### Step 3: ワークフローを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | create-campaigns.ts | `packages/core/core-flows/src/promotion/workflows/create-campaigns.ts` | 作成ワークフロー |

**読解のコツ**:
- **60-73行目**: createCampaignsWorkflow
  - **63行目**: createCampaignsStepでキャンペーン作成
  - **64-67行目**: campaignsCreatedフックで追加処理が可能

#### Step 4: モジュールサービスを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | promotion-module.ts | `packages/modules/promotion/src/services/promotion-module.ts` | キャンペーン関連メソッド |

**主要メソッド**:
- **1722-1741行目**: createCampaigns（キャンペーン作成）
- **1744-1798行目**: createCampaigns_（作成実装）
  - **1755-1768行目**: キャンペーンと予算データの分離
  - **1770-1773行目**: キャンペーンレコード作成
  - **1781行目**: validateCampaignBudgetDataで予算バリデーション
  - **1790-1795行目**: 予算レコード作成
- **1800-1820行目**: validateCampaignBudgetData（予算バリデーション）
  - **1804-1809行目**: type必須チェック
  - **1811-1819行目**: SPEND時のcurrency_code必須チェック
- **1837-1853行目**: updateCampaigns（キャンペーン更新）
- **1856-1926行目**: updateCampaigns_（更新実装）
  - **1884-1895行目**: type/currency_codeの不変チェック
  - **1897-1909行目**: 予算の更新または新規作成
- **1930-1994行目**: addPromotionsToCampaign（プロモーション追加）
  - **1951-1955行目**: 未所属プロモーションのみ対象
  - **1962-1969行目**: 所属チェックエラー
  - **1971-1983行目**: SPEND予算の通貨コード整合性チェック
  - **1985-1991行目**: campaign_idの更新
- **1998-2039行目**: removePromotionsFromCampaign（プロモーション削除）

### プログラム呼び出し階層図

```
POST /admin/campaigns (route.ts)
    │
    └─ createCampaignsWorkflow
           │
           └─ createCampaignsStep
                  │
                  └─ PromotionModuleService.createCampaigns()
                         │
                         └─ createCampaigns_()
                                ├─ campaignService_.create()
                                ├─ validateCampaignBudgetData()
                                └─ campaignBudgetService_.create()

POST /admin/campaigns/:id (route.ts)
    │
    └─ updateCampaignsWorkflow
           │
           └─ updateCampaignsStep
                  │
                  └─ PromotionModuleService.updateCampaigns()
                         │
                         └─ updateCampaigns_()
                                ├─ campaignService_.list()
                                ├─ campaignService_.update()
                                ├─ campaignBudgetService_.update()
                                └─ campaignBudgetService_.create()

POST /admin/campaigns/:id/promotions (route.ts)
    │
    └─ addOrRemoveCampaignPromotionsWorkflow
           │
           ├─ addPromotionsToCampaignStep
           │      │
           │      └─ addPromotionsToCampaign_()
           │             ├─ campaignService_.retrieve()
           │             ├─ promotionService_.list()
           │             └─ promotionService_.update()
           │
           └─ removePromotionsFromCampaignStep
                  │
                  └─ removePromotionsFromCampaign_()
                         └─ promotionService_.update()
```

### データフロー図

```
[入力]                         [処理]                              [出力]

name ─────────────────────▶ createCampaignsWorkflow
campaign_identifier                 │
description                         │
starts_at/ends_at                   ▼
budget ─────────────────▶ ┌───────────────────────────┐
                          │ promotion_campaign        │
                          │ (INSERT)                  │
                          └───────────┬───────────────┘
                                      │
                          ┌───────────▼───────────────┐
                          │ promotion_campaign_budget │
                          │ (INSERT - if budget)      │
                          └───────────┬───────────────┘
                                      │
                                      ▼
                                 CampaignDTO
                                 (with budget, promotions)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| campaign.ts | `packages/modules/promotion/src/models/campaign.ts` | モデル | Campaignエンティティ定義 |
| route.ts | `packages/medusa/src/api/admin/campaigns/route.ts` | APIルート | 一覧取得・作成エンドポイント |
| route.ts | `packages/medusa/src/api/admin/campaigns/[id]/route.ts` | APIルート | 詳細・更新・削除エンドポイント |
| route.ts | `packages/medusa/src/api/admin/campaigns/[id]/promotions/route.ts` | APIルート | プロモーション追加・削除 |
| create-campaigns.ts | `packages/core/core-flows/src/promotion/workflows/create-campaigns.ts` | ワークフロー | 作成ワークフロー |
| update-campaigns.ts | `packages/core/core-flows/src/promotion/workflows/update-campaigns.ts` | ワークフロー | 更新ワークフロー |
| delete-campaigns.ts | `packages/core/core-flows/src/promotion/workflows/delete-campaigns.ts` | ワークフロー | 削除ワークフロー |
| promotion-module.ts | `packages/modules/promotion/src/services/promotion-module.ts` | サービス | キャンペーンCRUDメソッド |
