# 機能設計書 22-自動取引生成

## 概要

本ドキュメントは、Actual Budget における自動取引生成機能の設計仕様を記述する。この機能は、定期取引スケジュールに基づいて取引を自動的に生成し、ユーザーの入力作業を削減することを目的とする。

### 本機能の処理概要

自動取引生成機能は、スケジュールに登録された定期取引の期日が到来した際に、自動的に取引レコードを生成する機能である。これにより、ユーザーは毎回手動で取引を入力する必要がなくなり、予算管理の効率が向上する。

**業務上の目的・背景**：定期的な支払い（家賃、光熱費、サブスクリプション等）は毎月同じ内容で発生するため、手動入力は非効率である。また、入力忘れによる予算の不整合を防止し、常に正確な残高を把握できるようにする必要がある。

**機能の利用シーン**：
- 毎月の固定費（家賃、保険料等）が自動的に取引として記録される場面
- 給与日に収入が自動的に計上される場面
- サブスクリプション料金が引き落とし日に自動記録される場面
- 複数の定期取引を一括で処理する場面

**主要な処理内容**：
1. スケジュールの期日チェック：同期完了時にスケジュールの期日を確認
2. 取引の自動生成：`posts_transaction`フラグが有効なスケジュールの取引を自動作成
3. 次回日付の更新：取引生成後、スケジュールの次回日付を更新
4. クライアント通知：取引生成後、クライアントに同期イベントを送信

**関連システム・外部連携**：
- 定期取引スケジュール機能（No.21）：スケジュール情報の参照元
- 同期機能（No.23）：同期完了をトリガーとして実行
- 取引管理機能（No.9）：生成された取引の登録先

**権限による制御**：特別な権限制御はなく、予算ファイルにアクセスできるユーザーの環境で自動実行される。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 16 | スケジュール一覧画面（デスクトップ） | 補助画面 | スケジュールに基づく取引自動生成設定 |
| 17 | スケジュール一覧画面（モバイル） | 補助画面 | モバイルでのスケジュール一覧表示 |
| 18 | スケジュール編集画面（モバイル） | 補助画面 | 自動取引生成設定の変更 |

## 機能種別

自動処理 / データ生成

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| syncSuccess | boolean | Yes | 同期が成功したかどうか | - |

### 入力データソース

- schedulesテーブル：スケジュール情報（posts_transaction, completed, next_date等）
- schedules_next_dateテーブル：次回実行日情報
- rulesテーブル：スケジュールに紐づくルール（支払先、口座、金額条件）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | 生成された取引ID |
| payee | string | 支払先ID |
| account | string | 口座ID |
| amount | number | 金額（整数、セント単位） |
| date | string | 取引日（YYYY-MM-DD） |
| schedule | string | 紐づくスケジュールID |
| cleared | boolean | 消込状態（falseで生成） |

### 出力先

- transactionsテーブル：生成された取引レコード
- クライアント画面：同期イベント経由で更新通知

## 処理フロー

### 処理シーケンス

```
1. 同期完了イベント受信
   └─ syncイベントのtypeがsuccess/error/unauthorizedか確認
   └─ prefsが読み込まれているか確認
   └─ databaseが利用可能か確認

2. 日次実行チェック
   └─ lastScheduleRunが今日でないことを確認
   └─ 既に実行済みの場合はスキップ

3. スケジュール一覧取得
   └─ completed=falseかつ_account.closed=falseのスケジュールを取得
   └─ 各スケジュールに対して取引存在チェック

4. ステータス判定
   └─ getStatus関数でdue/missed/paidを判定
   └─ paidの場合は次回日付を進行

5. 取引自動生成
   └─ due/missedかつposts_transaction=trueの場合
   └─ syncSuccessの場合のみ生成実行
   └─ postTransactionForSchedule関数で取引作成

6. クライアント通知
   └─ 取引生成があった場合、sync-eventを送信
   └─ 同期失敗の場合、schedules-offlineを送信
```

### フローチャート

```mermaid
flowchart TD
    A[同期完了イベント] --> B{今日実行済み?}
    B -->|Yes| C[終了]
    B -->|No| D[スケジュール一覧取得]
    D --> E{スケジュールあり?}
    E -->|No| C
    E -->|Yes| F[各スケジュールのステータス判定]
    F --> G{ステータス?}
    G -->|paid| H[次回日付を進行]
    G -->|due/missed| I{posts_transaction?}
    G -->|other| J[次のスケジュールへ]
    I -->|No| J
    I -->|Yes| K{syncSuccess?}
    K -->|No| L[failedToPostに追加]
    K -->|Yes| M[取引を自動生成]
    M --> N[didPost=true]
    H --> J
    L --> J
    N --> J
    J --> O{全スケジュール完了?}
    O -->|No| F
    O -->|Yes| P{failedToPost > 0?}
    P -->|Yes| Q[schedules-offline送信]
    P -->|No| R{didPost?}
    R -->|Yes| S[sync-event送信]
    R -->|No| C
    Q --> C
    S --> C
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-22-1 | 日次実行制限 | 自動取引生成は1日1回のみ実行 | lastScheduleRunで管理 |
| BR-22-2 | 同期成功時のみ生成 | 取引は同期成功時のみ生成される | syncSuccess=trueの場合 |
| BR-22-3 | オフライン時の保留 | 同期失敗時は取引生成を保留 | syncSuccess=falseの場合 |
| BR-22-4 | クローズ口座除外 | 口座がクローズされている場合は処理しない | _account.closed=falseのみ対象 |
| BR-22-5 | 未消込で生成 | 自動生成された取引はcleared=false | 常時 |

### 計算ロジック

**スケジュール金額の取得**:
```typescript
function getScheduledAmount(amount, inverse = false) {
  if (amount == null) return 0;
  if (typeof amount === 'number') {
    return inverse ? -amount : amount;
  }
  // 範囲指定の場合は平均値
  const avg = (amount.num1 + amount.num2) / 2;
  return inverse ? -Math.round(avg) : Math.round(avg);
}
```

**取引日の決定**:
- `today`パラメータがtrueの場合：現在日
- それ以外：スケジュールのnext_date

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| スケジュール取得 | schedules | SELECT | 対象スケジュールの一覧取得 |
| 取引存在チェック | transactions | SELECT | スケジュールに紐づく取引の存在確認 |
| 取引生成 | transactions | INSERT | 新規取引の作成 |
| 次回日付更新 | schedules_next_date | UPDATE | 次回日付の進行 |
| スケジュール完了 | schedules | UPDATE | 単発スケジュールの完了処理 |

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

#### transactions（INSERT）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | UUID自動生成 | - |
| INSERT | payee | schedule._payee | スケジュールから取得 |
| INSERT | account | schedule._account | スケジュールから取得 |
| INSERT | amount | getScheduledAmount(schedule._amount) | 金額計算 |
| INSERT | date | next_date または currentDay() | 取引日 |
| INSERT | schedule | schedule.id | スケジュールへのリンク |
| INSERT | cleared | false | 未消込状態で作成 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| SYNC_FAILED | 処理スキップ | 同期が失敗した | failedToPostに追加、後で再試行 |
| NO_ACCOUNT | スキップ | スケジュールに口座が未設定 | 該当スケジュールをスキップ |
| DB_NOT_AVAILABLE | スキップ | データベースが利用不可 | ログ出力してスキップ |

### リトライ仕様

- 同期失敗時は`schedules-offline`イベントを送信し、次回同期成功時に再試行
- 明示的なリトライ機能はなく、日次処理として毎日実行される

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

- 各取引の生成は`addTransactions`関数を通じて行われる
- 複数スケジュールの処理は順次実行（並列処理ではない）
- 1つのスケジュールの処理失敗が他に影響しないよう、個別に例外処理

## パフォーマンス要件

- 同期完了後、数秒以内に処理完了
- 大量のスケジュール（100件以上）がある場合でも30秒以内

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

- 自動生成された取引も暗号化対象（E2E暗号化有効時）
- サーバーとの通信は認証済みセッションで実行

## 備考

- `posts_transaction`フラグはスケジュール作成時にユーザーが設定
- 自動生成された取引は手動で編集・削除可能
- 重複防止のため、同一日付で同一スケジュールの取引が既に存在する場合は生成しない

---

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

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

### 推奨読解順序

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

自動取引生成に関連するスケジュールと取引のデータ構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | schedules.ts | `packages/loot-core/src/shared/schedules.ts` | getStatus関数、getScheduledAmount関数 |
| 1-2 | models (types) | `packages/loot-core/types/models/` | ScheduleEntityのposts_transactionフィールド |

**読解のコツ**:
- `getStatus`関数（18-42行目）でスケジュールの状態判定ロジックを確認
- `getScheduledAmount`関数（396-409行目）で金額計算ロジックを確認

#### Step 2: エントリーポイントを理解する

自動取引生成のトリガーとなる同期イベントハンドラを把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | app.ts | `packages/loot-core/src/server/schedules/app.ts` | app.events.on('sync')イベントハンドラ |

**主要処理フロー**:
1. **558-575行目**: 同期イベントハンドラ
   - `type === 'success' || type === 'error' || type === 'unauthorized'`で完了イベントを判定
   - `lastScheduleRun !== currentDay()`で日次実行を制御
   - `advanceSchedulesService(type === 'success')`を呼び出し

#### Step 3: コアロジックを理解する

自動取引生成の中核となる処理を読み解く。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | app.ts | `packages/loot-core/src/server/schedules/app.ts` | advanceSchedulesService, postTransactionForSchedule |

**主要処理フロー**:
- **445-525行目**: `advanceSchedulesService` - メイン処理
  - **447-451行目**: 対象スケジュールの取得（completed=false, _account.closed=false）
  - **453-458行目**: 取引存在チェッククエリの実行
  - **469-509行目**: 各スケジュールの処理ループ
    - ステータス判定
    - paid: 次回日付を進行
    - due/missed + posts_transaction: 取引を自動生成
  - **512-524行目**: クライアント通知

- **416-441行目**: `postTransactionForSchedule` - 取引生成
  - スケジュール情報の取得
  - 取引オブジェクトの構築
  - `addTransactions`で取引を登録

#### Step 4: 取引登録処理を理解する

生成された取引がどのように登録されるかを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | sync.ts | `packages/loot-core/src/server/accounts/sync.ts` | addTransactions関数 |

**読解のコツ**:
- `addTransactions`は取引の一括登録を担当
- スケジュールIDがセットされることで、スケジュールとの紐付けが行われる

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

```
Sync Event Handler
    │
    └─ app.events.on('sync')
           │
           ├─ prefs.getPrefs()
           │      └─ lastScheduleRun チェック
           │
           └─ advanceSchedulesService(syncSuccess)
                  │
                  ├─ aqlQuery() ─────────────────▶ スケジュール取得
                  │
                  ├─ getHasTransactionsQuery()
                  │      └─ 取引存在チェック
                  │
                  ├─ getStatus() ◀──────────────── 各スケジュール
                  │
                  ├─ setNextDate() ─────────────▶ paid時の次回日付更新
                  │
                  ├─ postTransactionForSchedule()
                  │      │
                  │      ├─ aqlQuery() ──────────▶ スケジュール詳細取得
                  │      │
                  │      └─ addTransactions() ───▶ 取引登録
                  │
                  └─ connection.send()
                         ├─ 'schedules-offline' ─▶ 失敗通知
                         └─ 'sync-event' ────────▶ 成功通知
```

### データフロー図

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

同期完了イベント     ┌──────────────────────────┐
  └─ success/    ──▶│                          │
     error/         │  advanceSchedulesService  │
     unauthorized   │                          │
                    └──────────────────────────┘
                              │
                              ▼
                    ┌──────────────────────────┐
schedulesテーブル  ──▶│                          │
  ├─ next_date      │  ステータス判定・処理分岐   │
  ├─ posts_trans    │                          │
  └─ _account       └──────────────────────────┘
                              │
                    ┌─────────┴─────────┐
                    ▼                   ▼
           ┌───────────────┐   ┌───────────────┐
           │ setNextDate   │   │ postTransaction│──▶ transactionsテーブル
           │ (paid時)      │   │ (due/missed時) │
           └───────────────┘   └───────────────┘
                    │                   │
                    └─────────┬─────────┘
                              ▼
                    ┌──────────────────────────┐
                    │    connection.send()     │──▶ クライアント通知
                    └──────────────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| app.ts | `packages/loot-core/src/server/schedules/app.ts` | ソース | 自動取引生成のコアロジック |
| schedules.ts | `packages/loot-core/src/shared/schedules.ts` | ソース | ステータス判定、金額計算ユーティリティ |
| sync.ts | `packages/loot-core/src/server/accounts/sync.ts` | ソース | 取引登録処理 |
| prefs.ts | `packages/loot-core/src/server/prefs.ts` | ソース | lastScheduleRun設定の管理 |
| connection.ts | `packages/loot-core/src/platform/server/connection.ts` | ソース | クライアント通知 |
| query.ts | `packages/loot-core/src/shared/query.ts` | ソース | AQLクエリビルダー |
