# 機能設計書 40-プロモーション計算

## 概要

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

### 本機能の処理概要

プロモーション計算機能は、カート内の商品やシッピング方法に対してプロモーションを適用し、割引額を計算する機能である。プロモーションコードの適用、自動プロモーションの検出、ルール評価、予算チェックを行い、最終的な割引アクション（ComputeActions）を返す。

**業務上の目的・背景**：カートの合計金額を計算する際に、適用可能なプロモーションを自動的に検出・計算し、正確な割引額を顧客に提示する。複数のプロモーションが存在する場合の優先順位や排他制御も行う。

**機能の利用シーン**：
1. カートにプロモーションコードを適用する場面
2. 自動適用プロモーションを検出する場面
3. チェックアウト時に割引額を確定する場面
4. 注文確定時に予算を消費する場面

**主要な処理内容**：
1. プロモーションコードの検証と取得
2. 自動プロモーションの検出とフィルタリング
3. ルール条件の評価
4. 割引額の計算（FIXED/PERCENTAGE、EACH/ACROSS/ONCE）
5. 予算・使用制限のチェック
6. ComputeActionsの生成

**関連システム・外部連携**：カートモジュール、注文モジュール

**権限による制御**：計算自体は認証不要。予算消費は内部システムが実行。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 83 | カート画面 | 主機能 | プロモーション適用・割引表示 |
| 84 | チェックアウト画面 | 補助機能 | 割引額の確認 |

## 機能種別

計算処理 / ビジネスロジック

## 入力仕様

### 入力パラメータ

#### computeActions

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| promotionCodes | string[] | Yes | 適用するプロモーションコード | - |
| applicationContext | ComputeActionContext | Yes | 適用コンテキスト | - |
| options | ComputeActionOptions | No | オプション設定 | - |

#### ComputeActionContext

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| items | ComputeActionItemLine[] | No | カート内アイテム | - |
| shipping_methods | ComputeActionShippingLine[] | No | シッピング方法 | - |
| currency_code | string | No | 通貨コード | - |
| customer_id | string | No | 顧客ID | USE_BY_ATTRIBUTE用 |
| customer_email | string | No | 顧客メール | USE_BY_ATTRIBUTE用 |

#### ComputeActionItemLine

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | string | Yes | アイテムID | - |
| quantity | number | Yes | 数量 | - |
| subtotal | bigNumber | Yes | 小計（税抜） | - |
| original_total | bigNumber | No | 税込合計 | - |
| adjustments | AdjustmentLine[] | No | 既存の調整 | - |

#### ComputeActionOptions

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| prevent_auto_promotions | boolean | No | 自動プロモーション無効 | デフォルト: false |
| skip_usage_limit_checks | boolean | No | 使用制限チェック無効 | デフォルト: false |

### 入力データソース

- API入力：カートモジュールからのコンテキスト
- DBテーブル：promotion, promotion_rule, promotion_rule_value, campaign, campaign_budget

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| computeActions | ComputeActions[] | 計算結果のアクション配列 |

#### ComputeActions（ADD_ITEM_ADJUSTMENT）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| action | string | "addItemAdjustment" |
| item_id | string | 対象アイテムID |
| amount | bigNumber | 割引額 |
| code | string | プロモーションコード |

#### ComputeActions（ADD_SHIPPING_METHOD_ADJUSTMENT）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| action | string | "addShippingMethodAdjustment" |
| shipping_method_id | string | 対象シッピング方法ID |
| amount | bigNumber | 割引額 |
| code | string | プロモーションコード |

#### ComputeActions（REMOVE_*_ADJUSTMENT）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| action | string | "removeItemAdjustment" / "removeShippingMethodAdjustment" |
| adjustment_id | string | 削除する調整ID |
| code | string | プロモーションコード |

#### ComputeActions（*_EXCEEDED）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| action | string | "campaignBudgetExceeded" / "promotionLimitExceeded" |
| code | string | プロモーションコード |

### 出力先

- API応答：カートモジュールへの応答
- 内部処理：registerUsageへの入力

## 処理フロー

### 処理シーケンス

```
1. 初期化
   ├─ 既存の調整を収集
   └─ 削除アクション（REMOVE_*）を生成

2. プロモーション取得
   ├─ コード指定プロモーションの取得
   └─ 自動プロモーションの取得（prevent_auto_promotionsがfalseの場合）
       └─ ルールプリフィルタリングで候補を絞り込み

3. プロモーションごとにループ
   ├─ 予算チェック（USE_BY_ATTRIBUTE）
   │   └─ 属性別使用量が上限に達していればCampaignBudgetExceeded
   ├─ 使用制限チェック（limit vs used）
   │   └─ 上限に達していればPromotionLimitExceeded
   ├─ 通貨コード整合性チェック
   ├─ ルール評価（areRulesValidForContext）
   └─ タイプ別割引計算
       ├─ BUYGET: getComputedActionsForBuyGet
       └─ STANDARD:
           ├─ ORDER/ITEMS: getComputedActionsForItems
           └─ SHIPPING_METHODS: getComputedActionsForShippingMethods

4. 結果整形
   └─ transformPropertiesToBigNumber（amount変換）
```

### フローチャート

```mermaid
flowchart TD
    A[開始: computeActions] --> B[既存調整収集]
    B --> C[REMOVE_*アクション生成]
    C --> D{自動プロモーション許可?}
    D -->|Yes| E[ルールプリフィルタリング]
    E --> F[自動プロモーション取得]
    D -->|No| G[コード指定のみ取得]
    F --> H[listActivePromotions_]
    G --> H

    H --> I[プロモーションループ開始]
    I --> J{USE_BY_ATTRIBUTE予算?}
    J -->|Yes| K[属性別使用量チェック]
    K --> L{上限超過?}
    L -->|Yes| M[CAMPAIGN_BUDGET_EXCEEDED追加]
    L -->|No| N[次のチェックへ]
    J -->|No| N

    N --> O{使用制限あり?}
    O -->|Yes| P{used >= limit?}
    P -->|Yes| Q[PROMOTION_LIMIT_EXCEEDED追加]
    P -->|No| R[通貨コードチェック]
    O -->|No| R

    R --> S{通貨コード一致?}
    S -->|No| T[スキップ]
    S -->|Yes| U[ルール評価]

    U --> V{ルール条件満たす?}
    V -->|No| T
    V -->|Yes| W{プロモーションタイプ}

    W -->|BUYGET| X[getComputedActionsForBuyGet]
    W -->|STANDARD| Y{target_type}
    Y -->|ORDER/ITEMS| Z[getComputedActionsForItems]
    Y -->|SHIPPING_METHODS| AA[getComputedActionsForShippingMethods]

    X --> AB[アクション追加]
    Z --> AB
    AA --> AB

    AB --> AC{次のプロモーション?}
    M --> AC
    Q --> AC
    T --> AC
    AC -->|Yes| I
    AC -->|No| AD[transformPropertiesToBigNumber]
    AD --> AE[終了: computeActions[]返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-40-001 | 通貨コード整合性 | プロモーションの通貨コードとカートの通貨コードが一致している必要がある | 常時 |
| BR-40-002 | ルール評価 | プロモーションのルール条件を満たすコンテキストでのみ適用 | 常時 |
| BR-40-003 | 使用制限 | プロモーションのlimitを超過した場合は適用不可 | limit設定時 |
| BR-40-004 | 予算制限 | キャンペーン予算を超過した場合は適用不可 | 予算設定時 |
| BR-40-005 | 自動適用優先 | is_automatic=trueのプロモーションは自動的に適用候補となる | prevent_auto_promotionsがfalse |
| BR-40-006 | BUYGETソート | BUYGETタイプは先に処理される | 複数プロモーション時 |
| BR-40-007 | 値降順ソート | 同タイプ内では割引値が大きい順に処理 | 複数プロモーション時 |

### 計算ロジック

#### FIXED（固定額割引）
```
割引額 = application_method.value
```

#### PERCENTAGE（割合割引）
```
割引額 = (対象金額 * application_method.value) / 100
```

#### Allocation別処理

| Allocation | 説明 | 計算方法 |
|-----------|------|---------|
| EACH | アイテムごと | 各アイテムに対してvalue分の割引 |
| ACROSS | 全体に分散 | 全アイテム合計に対してvalue分の割引を按分 |
| ONCE | 1回のみ | 最安アイテムに対して1回のみ適用 |

#### BUYGETプロモーション
```
1. buy_rulesで対象アイテムを特定
2. buy_rules_min_quantityを満たすか確認
3. target_rulesで割引対象を特定
4. apply_to_quantityの数量に割引適用
5. max_quantityで上限制御
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| プロモーション取得 | promotion | SELECT | 有効なプロモーション取得 |
| ルール取得 | promotion_rule, promotion_rule_value | SELECT | ルール条件取得 |
| 予算チェック | campaign_budget, campaign_budget_usage | SELECT | 予算・使用量確認 |

※計算処理自体はDBを更新しない。使用量の更新はregisterUsageで別途実行。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| INVALID_DATA | MedusaError | itemsが配列でない | itemsを配列で渡す |
| INVALID_DATA | MedusaError | USE_BY_ATTRIBUTEで属性値がない | コンテキストに属性値を含める |
| - | スキップ | 通貨コード不一致 | 適用をスキップ（エラーなし） |
| - | アクション | 予算超過 | CAMPAIGN_BUDGET_EXCEEDEDを返却 |
| - | アクション | 使用制限超過 | PROMOTION_LIMIT_EXCEEDEDを返却 |

### リトライ仕様

- 計算処理は冪等性があり、同じ入力に対して同じ出力を返す
- リトライ可能

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

- 計算処理自体はトランザクション不要（参照のみ）
- 使用量登録（registerUsage）は別トランザクション

## パフォーマンス要件

- 計算処理：200ms以内
- プロモーション数：最大100件
- 自動プロモーションのプリフィルタリングでDB負荷軽減

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

- プロモーションコードの漏洩に注意
- 計算結果の改ざん防止（サーバーサイドで再計算）

## 備考

- computeActionsは冪等であり、既存の調整を削除してから再計算する
- 自動プロモーションはルールプリフィルタリングで効率的に絞り込む
- BUYGETとSTANDARDは別のロジックで処理
- 予算超過・使用制限超過は特別なアクションとして返却（エラーではない）

---

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

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

### 推奨読解順序

#### Step 1: enum定義を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | index.ts | `packages/core/utils/src/promotion/index.ts` | プロモーション関連enum定義 |

**主要enum**:
- **1-4行目**: PromotionType: STANDARD, BUYGET
- **12-15行目**: ApplicationMethodType: FIXED, PERCENTAGE
- **17-21行目**: ApplicationMethodTargetType: ORDER, SHIPPING_METHODS, ITEMS
- **23-27行目**: ApplicationMethodAllocation: EACH, ACROSS, ONCE
- **46-53行目**: ComputedActions: ADD_ITEM_ADJUSTMENT, ADD_SHIPPING_METHOD_ADJUSTMENT, REMOVE_*, *_EXCEEDED

#### Step 2: ApplicationMethodモデルを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | application-method.ts | `packages/modules/promotion/src/models/application-method.ts` | 適用方法の定義 |

**読解のコツ**:
- **9行目**: idプレフィックスは`proappmet`
- **10-11行目**: value（割引値）とcurrency_code
- **12-14行目**: max_quantity、apply_to_quantity、buy_rules_min_quantity
- **15-24行目**: type、target_type、allocationのenum
- **28-35行目**: target_rulesとbuy_rulesとのmanyToMany関連

#### Step 3: computeActionsメソッドを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | promotion-module.ts | `packages/modules/promotion/src/services/promotion-module.ts` | computeActions |

**主要処理フロー**:
- **631-636行目**: computeActionsメソッド定義
- **637-640行目**: オプション取得（prevent_auto_promotions, skip_usage_limit_checks）
- **654-680行目**: 既存調整のマップ構築
- **682行目**: appliedCodes取得
- **692-726行目**: 自動プロモーションの条件構築
- **728-745行目**: listActivePromotions_でプロモーション取得
- **760-778行目**: REMOVE_*アクションの生成
- **783-789行目**: BUYGETタイプでソート
- **800-924行目**: プロモーションごとのループ処理
- **811-851行目**: USE_BY_ATTRIBUTE予算チェック
- **854-862行目**: 使用制限チェック
- **864-876行目**: 通貨コードとルール評価
- **878-923行目**: タイプ別計算処理
- **926行目**: transformPropertiesToBigNumber

#### Step 4: 計算ユーティリティを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | line-items.ts | `packages/modules/promotion/src/utils/compute-actions/line-items.ts` | アイテム割引計算 |
| 4-2 | buy-get.ts | `packages/modules/promotion/src/utils/compute-actions/buy-get.ts` | BUYGET計算 |
| 4-3 | shipping-methods.ts | `packages/modules/promotion/src/utils/compute-actions/shipping-methods.ts` | シッピング割引計算 |

**line-items.ts主要処理**:
- **33-47行目**: getComputedActionsForItems（エントリーポイント）
- **49-100行目以降**: applyPromotionToItems（実際の計算）
- **79-81行目**: ONCE時のソート（最安順）
- **90-100行目**: ACROSS時の合計計算

**buy-get.ts主要処理**:
- **27-54行目**: コンテキスト検証とmin_quantity確認
- **56-76行目**: 設定値の正規化
- **78-100行目以降**: 残数量計算

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

```
computeActions() (promotion-module.ts)
    │
    ├─ 既存調整マップ構築
    │
    ├─ REMOVE_*アクション生成
    │
    ├─ [自動プロモーション取得]
    │      │
    │      └─ buildPromotionRuleQueryFilterFromContext()
    │             │
    │             └─ promotionService_.list({is_automatic: true, ...})
    │
    ├─ listActivePromotions_()
    │
    └─ [プロモーションごとにループ]
           │
           ├─ [USE_BY_ATTRIBUTE予算チェック]
           │      │
           │      └─ campaignBudgetUsageService_.list()
           │             │
           │             └─ computeActionForBudgetExceeded()
           │
           ├─ [使用制限チェック]
           │      └─ PROMOTION_LIMIT_EXCEEDEDアクション
           │
           ├─ areRulesValidForContext()
           │
           └─ [タイプ別計算]
                  │
                  ├─ [BUYGET]
                  │      └─ getComputedActionsForBuyGet()
                  │
                  └─ [STANDARD]
                         ├─ [ORDER/ITEMS]
                         │      └─ getComputedActionsForItems()
                         │             └─ applyPromotionToItems()
                         │                    └─ calculateAdjustmentAmountFromPromotion()
                         │
                         └─ [SHIPPING_METHODS]
                                └─ getComputedActionsForShippingMethods()
```

### データフロー図

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

promotionCodes ──────────▶ computeActions()
applicationContext               │
(items, shipping_methods,        │
 currency_code, customer_id)     │
                                 ▼
                    ┌─────────────────────────────┐
                    │ 1. 既存調整収集             │
                    │    → REMOVE_*アクション     │
                    └──────────────┬──────────────┘
                                   │
                    ┌──────────────▼──────────────┐
                    │ 2. プロモーション取得        │
                    │    - コード指定              │
                    │    - 自動プロモーション      │
                    └──────────────┬──────────────┘
                                   │
                    ┌──────────────▼──────────────┐
                    │ 3. プロモーションループ      │
                    │    - 予算/制限チェック       │
                    │    - ルール評価              │
                    │    - 割引計算                │
                    └──────────────┬──────────────┘
                                   │
                                   ▼
                              ComputeActions[]
                              [
                                {action: "removeItemAdjustment", ...},
                                {action: "addItemAdjustment", amount, code, item_id},
                                {action: "promotionLimitExceeded", code},
                                ...
                              ]
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.ts | `packages/core/utils/src/promotion/index.ts` | ユーティリティ | enum定義 |
| application-method.ts | `packages/modules/promotion/src/models/application-method.ts` | モデル | 適用方法エンティティ |
| promotion-module.ts | `packages/modules/promotion/src/services/promotion-module.ts` | サービス | computeActionsメソッド |
| line-items.ts | `packages/modules/promotion/src/utils/compute-actions/line-items.ts` | ユーティリティ | アイテム割引計算 |
| buy-get.ts | `packages/modules/promotion/src/utils/compute-actions/buy-get.ts` | ユーティリティ | BUYGET計算 |
| shipping-methods.ts | `packages/modules/promotion/src/utils/compute-actions/shipping-methods.ts` | ユーティリティ | シッピング割引計算 |
| usage.ts | `packages/modules/promotion/src/utils/compute-actions/usage.ts` | ユーティリティ | 予算超過チェック |
| sort-by-price.ts | `packages/modules/promotion/src/utils/compute-actions/sort-by-price.ts` | ユーティリティ | 価格ソート |
| promotion-rule.ts | `packages/modules/promotion/src/utils/validations/promotion-rule.ts` | バリデーション | ルール評価 |
