# 機能設計書 12-分割取引（Split Transactions）

## 概要

本ドキュメントは、Actualアプリケーションにおける分割取引（Split Transactions）機能について、その処理内容、入出力仕様、および実装詳細を記述した機能設計書である。

### 本機能の処理概要

分割取引機能は、1つの取引を複数のカテゴリに分割して記録する機能である。例えば、スーパーマーケットでの買い物で食料品と日用品を同時に購入した場合、それぞれの金額を異なるカテゴリに割り当てることができる。

**業務上の目的・背景**：実際の支出は単一のカテゴリに収まらないことが多い。1回の支払いに複数の性質の異なる支出が含まれる場合、正確な予算管理のためにそれぞれを適切なカテゴリに割り当てる必要がある。本機能により、ユーザーはより精緻な支出管理と予算追跡が可能になる。

**機能の利用シーン**：
- スーパーマーケットでの食料品と日用品の同時購入
- 出張費の交通費・宿泊費・食費への分割
- オンラインショッピングでの複数カテゴリ商品の一括注文
- 家族間での費用按分が必要な場合

**主要な処理内容**：
1. 親取引（is_parent=true）を作成し、元の取引情報（日付、口座、支払先、総額）を保持
2. 子取引（is_child=true）を作成し、各分割分の金額とカテゴリを設定
3. 子取引のparent_idを親取引のIDに設定して関連付け
4. 子取引の合計金額が親取引の金額と一致することを検証
5. AQLクエリで`splits: 'grouped'`オプションを指定すると子取引をグループ化して取得

**関連システム・外部連携**：本機能は内部完結型であり、外部システムとの直接連携はない。ただし、インポート機能（OFX、QIF等）から分割取引をインポートする場合がある。

**権限による制御**：特別な権限制御はなく、予算ファイルを開いているユーザーであれば利用可能。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 12 | 口座一覧画面（デスクトップ） | 補助機能 | 取引の分割処理 |
| 14 | 口座詳細画面（モバイル） | 主機能 | モバイルでの取引表示・操作 |
| 15 | 取引編集画面（モバイル） | 補助機能 | モバイルでの分割取引編集 |

## 機能種別

CRUD操作 / データ変換

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| parent_transaction | TransactionEntity | Yes | 親取引情報 | is_parent=true必須 |
| subtransactions | TransactionEntity[] | Yes | 子取引のリスト | 各要素にis_child=true、parent_id必須 |

### 入力データソース

- 画面入力（取引編集フォーム）
- インポートファイル（OFX、QIF形式で分割取引がサポートされている場合）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | 取引ID（UUID） |
| is_parent | boolean | 親取引フラグ（true） |
| is_child | boolean | 子取引フラグ（true） |
| parent_id | string | 親取引ID（子取引のみ） |
| account | string | 口座ID |
| payee | string | 支払先ID |
| amount | IntegerAmount | 金額（整数形式、セント単位） |
| category | string | カテゴリID（子取引のみ、親取引はnull） |
| date | string | 取引日（YYYY-MM-DD形式） |
| notes | string | メモ |
| sort_order | number | 表示順序 |

### 出力先

- データベース（transactionsテーブル）

## 処理フロー

### 処理シーケンス

```
1. 分割取引作成開始
   └─ ユーザーが取引編集画面で「分割」を選択

2. 親取引の準備
   └─ is_parent=true、category=nullを設定
   └─ 元の取引情報（日付、口座、支払先、総額）を保持

3. 子取引の作成
   └─ 各分割分について子取引を作成
   └─ is_child=true、parent_id=親取引IDを設定
   └─ 各子取引にカテゴリと金額を割り当て

4. 金額検証
   └─ 子取引の合計金額が親取引の金額と一致するか確認

5. データベース保存
   └─ batchUpdateTransactionsを使用して一括保存

6. UI更新
   └─ 分割取引の表示を更新
```

### フローチャート

```mermaid
flowchart TD
    A[開始] --> B[取引編集画面で分割選択]
    B --> C[親取引作成]
    C --> D[is_parent=true設定]
    D --> E[category=null設定]
    E --> F{子取引追加?}
    F -->|Yes| G[子取引作成]
    G --> H[is_child=true設定]
    H --> I[parent_id設定]
    I --> J[カテゴリ・金額設定]
    J --> F
    F -->|No| K{金額合計チェック}
    K -->|一致| L[データベース保存]
    K -->|不一致| M[エラー表示]
    L --> N[UI更新]
    M --> F
    N --> O[終了]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-01 | 親取引カテゴリ無効 | 親取引（is_parent=true）にはカテゴリを設定しない | 親取引作成時 |
| BR-02 | 子取引必須フィールド | 子取引にはis_child=true、parent_idが必須 | 子取引作成時 |
| BR-03 | 金額整合性 | 子取引の金額合計は親取引の金額と一致する必要がある | 保存時 |
| BR-04 | 親削除時子削除 | 親取引を削除すると関連する子取引も自動削除 | 削除時 |
| BR-05 | オフバジェット無効 | オフバジェット口座の取引は分割してもカテゴリ無効 | オフバジェット口座 |

### 計算ロジック

**金額整合性チェック**:
```typescript
// 子取引の金額合計と親取引の金額を比較
const childrenSum = subtransactions.reduce((sum, t) => sum + t.amount, 0);
const isValid = childrenSum === parentTransaction.amount;
```

**金額差異の計算**（エラー表示用）:
```typescript
error = {
  type: 'SplitTransactionError',
  version: 1,
  difference: parentTransaction.amount - childrenSum
};
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 親取引作成 | transactions | INSERT | is_parent=true、category=nullの取引を追加 |
| 子取引作成 | transactions | INSERT | is_child=true、parent_id設定の取引を追加 |
| 子取引取得 | v_transactions_internal | SELECT | parent_idで関連子取引を取得 |
| 親取引削除 | transactions | UPDATE(tombstone) | 親取引と関連子取引を削除 |

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

#### transactions

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT (親) | is_parent | 1 (true) | 親取引フラグ |
| INSERT (親) | category | null | 親はカテゴリなし |
| INSERT (子) | is_child | 1 (true) | 子取引フラグ |
| INSERT (子) | parent_id | 親取引のID | 親子関連付け |
| SELECT | parent_id | 指定された親ID | 子取引取得時 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| SplitTransactionError | 金額不整合 | 子取引合計と親取引金額が不一致 | 差額をUIに表示し修正を促す |

### リトライ仕様

金額不整合エラーの場合、ユーザーが子取引の金額を修正して再保存する。

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

- batchMessages関数でデータベース操作をバッチ化
- 親取引と子取引の作成は同一トランザクション内で実行
- 親取引削除時、子取引の削除も同一トランザクションで実行

## パフォーマンス要件

- 1取引の分割数は通常10件以下を想定
- AQLクエリで`splits: 'grouped'`を使用することで効率的な取得が可能

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

- 特別なセキュリティ要件なし
- 通常の取引管理と同様の権限制御

## 備考

- AQLでの分割取引取得オプション：
  - `splits: 'none'` - 親取引のみ取得（子取引を除外）
  - `splits: 'grouped'` - 親取引に子取引をグループ化して取得
  - `splits: 'all'` - すべての取引をフラットに取得

---

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

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

### 推奨読解順序

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

まず、分割取引に関連するフィールドの構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | transaction.ts | `packages/loot-core/src/types/models/transaction.ts` | is_parent、is_child、parent_id、subtransactionsフィールドを確認 |

**読解のコツ**:
- **10行目**: `is_parent?: boolean` - 親取引フラグ
- **11行目**: `is_child?: boolean` - 子取引フラグ
- **12行目**: `parent_id?: TransactionEntity['id']` - 親取引への参照
- **29行目**: `subtransactions?: TransactionEntity[]` - 子取引配列

#### Step 2: 取引バッチ更新処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | index.ts | `packages/loot-core/src/server/transactions/index.ts` | batchUpdateTransactions関数の実装 |

**主要処理フロー**:
1. **13-23行目**: idsWithChildren関数 - 親取引IDから子取引IDも取得
2. **40-196行目**: batchUpdateTransactions関数 - 取引の一括更新
3. **84-88行目**: 親取引またはオフバジェット口座の場合、categoryをnullに設定
4. **94-103行目**: 削除時に子取引も含めて削除

#### Step 3: AQLクエリでの分割取引取得を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | aql.ts | `packages/loot-core/src/server/aql/` | splits オプションの処理 |

**主要処理フロー**:
- `splits: 'none'` - WHERE句に`is_child = 0`を追加
- `splits: 'grouped'` - 親取引に子取引をネストして返却
- `splits: 'all'` - フィルタリングなしで全取引を返却

#### Step 4: エクスポートでの分割取引処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | export-to-csv.ts | `packages/loot-core/src/server/transactions/export/export-to-csv.ts` | 分割取引のCSV出力形式 |

**主要処理フロー**:
- **80-92行目**: 親取引の子数とソート順序を計算
- **100-112行目**: NotesフィールドにSPLIT表記を追加
- **114-119行目**: 親取引のAmountは0、Split_Amountに元金額を設定

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

```
UI (取引編集画面)
    │
    ├─ send('transactions-batch-update')
    │      │
    │      └─ index.ts: batchUpdateTransactions
    │             │
    │             ├─ idsWithChildren (子取引ID取得)
    │             │
    │             ├─ batchMessages (トランザクション処理)
    │             │      │
    │             │      ├─ db.insertTransaction (追加)
    │             │      ├─ db.updateTransaction (更新)
    │             │      └─ db.deleteTransaction (削除)
    │             │
    │             └─ transfer.onInsert/onUpdate/onDelete (振替処理)
    │
    └─ UIの再レンダリング
```

### データフロー図

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

親取引データ ─────────┐
                      │
                      ├──▶ batchUpdateTransactions ──▶ transactions テーブル
                      │           │
子取引データ[] ───────┘           │
                                  ├─▶ 親取引 (is_parent=1, category=null)
                                  └─▶ 子取引[] (is_child=1, parent_id=親ID)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| transaction.ts | `packages/loot-core/src/types/models/transaction.ts` | 型定義 | 取引データ型（分割関連フィールド含む） |
| index.ts | `packages/loot-core/src/server/transactions/index.ts` | ソース | 取引バッチ更新ロジック |
| app.ts | `packages/loot-core/src/server/transactions/app.ts` | ソース | 取引APIハンドラ |
| export-to-csv.ts | `packages/loot-core/src/server/transactions/export/export-to-csv.ts` | ソース | CSV出力での分割取引処理 |
| db/index.ts | `packages/loot-core/src/server/db/index.ts` | ソース | データベース操作 |
