# 機能設計書 10-取引インポート

## 概要

本ドキュメントは、Actual Budget における外部ファイル（CSV、QIF、OFX、QFX、XML/CAMT）からの取引インポート機能の設計を記述する。

### 本機能の処理概要

**業務上の目的・背景**：銀行やクレジットカード会社から提供される取引データを手動入力することなく、効率的にシステムに取り込むことが予算管理の効率化に不可欠である。本機能は、様々な形式の金融データファイルを解析し、Actual Budget の取引データとして統合することを可能にする。

**機能の利用シーン**：
- 銀行からダウンロードした OFX/QFX ファイルをインポートする
- 会計ソフトからエクスポートした CSV ファイルを取り込む
- QIF 形式の取引履歴をインポートする
- 欧州の CAMT 形式（XML）の取引データを取り込む
- YNAB4/YNAB5 予算データを移行する

**主要な処理内容**：
1. ファイル形式の判定（拡張子ベース）
2. ファイルのパース（CSV, QIF, OFX, XML）
3. 取引データのプレビュー表示
4. フィールドマッピング設定（CSV の場合）
5. 日付形式の指定
6. 既存取引との照合（重複検出）
7. 取引データのインポート実行

**関連システム・外部連携**：口座管理、取引管理、カテゴリ管理、支払先管理

**権限による制御**：特別な権限制御なし

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 12 | 口座一覧画面（デスクトップ） | 主画面 | インポートダイアログの起動元 |
| - | 取引インポートモーダル | 主画面 | ファイル選択、プレビュー、マッピング設定、インポート実行 |

## 機能種別

ファイルインポート / データ変換

## 入力仕様

### 入力パラメータ

#### ファイルパース

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| filepath | string | Yes | インポートファイルパス | 有効なファイルパス |
| options | ParseFileOptions | No | パースオプション | |

#### ParseFileOptions

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| hasHeaderRow | boolean | No | ヘッダー行の有無（CSV用） | デフォルト: true |
| delimiter | string | No | 区切り文字（CSV用） | , ; \| \t ~ |
| skipStartLines | number | No | 先頭スキップ行数 | 0以上の整数 |
| skipEndLines | number | No | 末尾スキップ行数 | 0以上の整数 |
| fallbackMissingPayeeToMemo | boolean | No | PayeeがなければMemoを使用（OFX用） | |
| importNotes | boolean | No | メモをインポートするか | |

#### 取引インポート

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| accountId | string | Yes | インポート先口座ID | 存在する口座ID |
| transactions | array | Yes | インポートする取引データ | |
| isPreview | boolean | Yes | プレビューモードか | |
| reconcile | boolean | No | 既存取引との照合を行うか | デフォルト: true |

### 入力データソース

- ファイルシステム：CSV、QIF、OFX、QFX、XML（CAMT）ファイル
- 画面入力：フィールドマッピング、日付形式、オプション設定

### 対応ファイル形式

| 形式 | 拡張子 | 説明 |
|-----|--------|------|
| CSV | .csv, .tsv | カンマ区切り / タブ区切りファイル |
| QIF | .qif | Quicken Interchange Format |
| OFX | .ofx | Open Financial Exchange |
| QFX | .qfx | Quicken Financial Exchange（OFX互換） |
| XML | .xml | CAMT (ISO 20022) 形式 |

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| errors | array | パースエラー一覧 |
| transactions | array | パースされた取引データ |
| added | array | 追加された取引ID |
| updated | array | 更新された取引ID |

### 出力先

- データベース：transactions テーブル
- 画面表示：インポートプレビュー、インポート結果通知

## 処理フロー

### 処理シーケンス

```
1. ファイル選択
   └─ 対応形式: .csv, .tsv, .qif, .ofx, .qfx, .xml

2. ファイルパース（transactions-parse-file）
   └─ 拡張子に基づいて適切なパーサーを選択
   └─ parseCSV / parseQIF / parseOFX / parseCAMT

3. 取引データのプレビュー表示
   └─ 日付、支払先、金額、カテゴリ等を表示

4. オプション設定（ファイル形式により異なる）
   └─ CSV: フィールドマッピング、区切り文字、ヘッダー有無
   └─ QIF/CSV: 日付形式、金額反転
   └─ OFX: Payee→Memoフォールバック
   └─ 共通: 既存取引との照合

5. プレビュー更新（importPreviewTransactions）
   └─ 既存取引との照合結果を表示
   └─ 重複/更新対象をハイライト

6. インポート実行（importTransactions）
   └─ reconcile=true: bankSync.reconcileTransactions
   └─ reconcile=false: api/transactions-add

7. 結果通知
   └─ 追加/更新された取引数を表示
   └─ エラーがあれば通知
```

### フローチャート

```mermaid
flowchart TD
    A[ファイル選択] --> B{拡張子判定}
    B -->|.csv/.tsv| C[parseCSV]
    B -->|.qif| D[parseQIF]
    B -->|.ofx/.qfx| E[parseOFX]
    B -->|.xml| F[parseCAMT]
    C --> G[プレビュー表示]
    D --> G
    E --> G
    F --> G
    G --> H{オプション設定}
    H -->|CSV| I[フィールドマッピング]
    H -->|QIF/CSV| J[日付形式]
    H -->|OFX| K[Memoフォールバック]
    I --> L[プレビュー更新]
    J --> L
    K --> L
    L --> M{既存取引と照合?}
    M -->|Yes| N[reconcileTransactions]
    M -->|No| O[api/transactions-add]
    N --> P[インポート完了]
    O --> P
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | 拡張子判定 | ファイル拡張子でパーサーを自動選択 | 常時 |
| BR-002 | 重複検出 | imported_id で既存取引との重複を検出 | OFX/QFX |
| BR-003 | フィールド推測 | CSV のフィールド名から自動マッピング | CSV |
| BR-004 | 日付形式自動検出 | 取引データから日付形式を推測 | CSV/QIF |
| BR-005 | 金額変換 | OFX の金額は 1/10 に変換（YNAB互換） | OFX/YNAB |
| BR-006 | 設定保存 | 口座ごとにインポート設定を保存 | CSV |

### 計算ロジック

```
// OFX 金額パース
function parseOfxAmount(amount: string): number | null {
  // 括弧表記: "(30.00)" → -30.00
  // 通貨記号除去、複数小数点対応
}

// 金額変換（YNAB5）
amountFromYnab = amount / 10  // YNAB: x1000, Actual: x100

// フィールド自動マッピング（CSV）
- 'date' を含むフィールド → date
- 'amount' を含むフィールド → amount
- 'payee' を含むフィールド → payee
- 'category' を含むフィールド → category
- 'notes' を含むフィールド → notes
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| インポート | transactions | INSERT | 新規取引追加 |
| 照合更新 | transactions | UPDATE | 既存取引との照合更新 |
| 設定保存 | prefs | UPDATE | インポート設定保存 |

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

#### transactions

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | UUID | 主キー |
| INSERT | account | 口座ID | |
| INSERT | date | インポートされた日付 | パース後 |
| INSERT | amount | 金額 | 整数（セント単位） |
| INSERT | payee | 支払先ID | 自動作成または既存 |
| INSERT | imported_payee | 元の支払先名 | |
| INSERT | imported_id | インポートID | OFX の fitId |
| INSERT | category | カテゴリID | |
| INSERT | notes | メモ | |
| INSERT | cleared | 0/1 | clearOnImport設定 |

#### prefs（同期設定）

| キー | 説明 |
|-----|------|
| csv-delimiter-{accountId} | CSV区切り文字 |
| csv-has-header-{accountId} | ヘッダー有無 |
| csv-skip-start-lines-{accountId} | 先頭スキップ行数 |
| csv-skip-end-lines-{accountId} | 末尾スキップ行数 |
| csv-mappings-{accountId} | フィールドマッピング |
| csv-in-out-mode-{accountId} | 入出金分離モード |
| csv-out-value-{accountId} | 出金識別値 |
| flip-amount-{accountId}-{filetype} | 金額反転設定 |
| parse-date-{accountId}-{filetype} | 日付形式 |
| ofx-fallback-missing-payee-{accountId} | OFX Payeeフォールバック |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------
| INVALID_FILE_TYPE | バリデーション | 非対応の拡張子 | 対応形式を案内 |
| PARSE_ERROR | パースエラー | ファイル形式不正 | エラーメッセージ表示 |
| INVALID_DATE | バリデーション | 日付パース失敗 | 日付形式の選択を促す |
| INVALID_AMOUNT | バリデーション | 金額パース失敗 | エラー通知 |
| ACCOUNT_NOT_FOUND | データ不整合 | 存在しない口座ID | エラー通知 |
| SKIP_TOO_MANY_LINES | バリデーション | スキップ行数が総行数超過 | エラーメッセージ表示 |

### リトライ仕様

- パースエラー時はファイル再選択を促す
- インポートエラー時は設定変更後に再試行可能

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

- 複数取引の一括インポートはバッチ処理
- undo/redo 対応（undoable ラッパー）

## パフォーマンス要件

- ファイルパースは非同期処理
- プレビューは最初の数件を即座に表示
- 大量取引のインポートは進捗表示

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

- ファイルはローカルで処理（サーバーにアップロードしない）
- インポートデータはローカルデータベースに保存

## 備考

- YNAB4/YNAB5 からの予算インポートは別機能（handleBudgetImport）で提供
- 銀行連携（GoCardless、SimpleFin、PluggyAI）による自動インポートは別機能

---

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

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

### 推奨読解順序

#### Step 1: ファイルパーサーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | parse-file.ts | `packages/loot-core/src/server/transactions/import/parse-file.ts` | parseFile 関数と各形式のパーサー |

**主要処理フロー**:
- **76-106行目**: `parseFile` 関数（拡張子判定とパーサー選択）
- **86-98行目**: switch文による拡張子別パーサー呼び出し
- **108-155行目**: `parseCSV` 関数
- **157-187行目**: `parseQIF` 関数
- **189-232行目**: `parseOFX` 関数
- **234-260行目**: `parseCAMT` 関数

#### Step 2: インポートモーダルを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | ImportTransactionsModal.tsx | `packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.tsx` | UI とインポートロジック |

**主要処理フロー**:
- **162-166行目**: コンポーネント定義とprops
- **371-452行目**: `parse` 関数（ファイルパース呼び出し）
- **379-385行目**: `transactions-parse-file` メッセージ送信
- **583-719行目**: `onImport` 関数（インポート実行）
- **704-710行目**: `importTransactions` dispatch
- **228-369行目**: `getImportPreview` 関数（プレビュー生成）
- **84-147行目**: `getInitialMappings` 関数（フィールド自動マッピング）

#### Step 3: サーバー側インポート処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | app.ts (accounts) | `packages/loot-core/src/server/accounts/app.ts` | importTransactions 関数 |
| 3-2 | accountsSlice.ts | `packages/desktop-client/src/accounts/accountsSlice.ts` | Redux actions |

**主要処理フロー**:
- **1089-1133行目**: `importTransactions` 関数（サーバー側）
- **1107-1114行目**: `bankSync.reconcileTransactions` 呼び出し
- **1240行目**: `app.method('transactions-import', ...)` 登録
- accountsSlice.ts **528-553行目**: `importPreviewTransactions` thunk
- accountsSlice.ts **561-612行目**: `importTransactions` thunk

#### Step 4: 予算インポート（YNAB）を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | index.ts | `packages/loot-core/src/server/importers/index.ts` | handleBudgetImport 関数 |
| 4-2 | ynab4.ts | `packages/loot-core/src/server/importers/ynab4.ts` | YNAB4 インポート |
| 4-3 | ynab5.ts | `packages/loot-core/src/server/importers/ynab5.ts` | YNAB5 インポート |

**主要処理フロー**:
- index.ts **22-58行目**: `handleBudgetImport` 関数
- ynab4.ts **408-441行目**: `parseFile` 関数（ZIP解凍）
- ynab4.ts **124-242行目**: `importTransactions` 関数
- ynab5.ts **183-451行目**: `importTransactions` 関数

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

```
ImportTransactionsModal.tsx
    │
    ├─ parse()
    │      │
    │      └─ send('transactions-parse-file')
    │             │
    │             └─ parseFile() [parse-file.ts]
    │                    │
    │                    ├─ parseCSV()
    │                    ├─ parseQIF()
    │                    ├─ parseOFX()
    │                    └─ parseCAMT()
    │
    ├─ getImportPreview()
    │      │
    │      └─ dispatch(importPreviewTransactions)
    │             │
    │             └─ send('transactions-import', { isPreview: true })
    │                    │
    │                    └─ importTransactions() [accounts/app.ts]
    │                           │
    │                           └─ bankSync.reconcileTransactions()
    │
    └─ onImport()
           │
           └─ dispatch(importTransactions)
                  │
                  ├─ reconcile=false
                  │      └─ send('api/transactions-add')
                  │
                  └─ reconcile=true
                         └─ send('transactions-import', { isPreview: false })
                                │
                                └─ importTransactions() [accounts/app.ts]
                                       │
                                       └─ bankSync.reconcileTransactions()
```

### データフロー図

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

ファイル選択       ───▶  parseFile()
(.csv/.qif/.ofx)              │
                              ▼
                         パース結果                ───▶  プレビュー表示
                         (transactions)                  (TransactionList)
                              │
                              ▼
フィールドマッピング  ───▶  applyFieldMappings()
(CSV の場合)                  │
                              ▼
                         importPreviewTransactions  ───▶  照合結果表示
                         (重複検出)                      (existing/ignored)
                              │
                              ▼
インポート実行       ───▶  importTransactions()
                              │
                              ├─ reconcile=true
                              │     └─ reconcileTransactions ───▶ transactions INSERT/UPDATE
                              │
                              └─ reconcile=false
                                    └─ addTransactions     ───▶ transactions INSERT
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| parse-file.ts | `packages/loot-core/src/server/transactions/import/parse-file.ts` | ソース | ファイルパーサー |
| ofx2json.ts | `packages/loot-core/src/server/transactions/import/ofx2json.ts` | ソース | OFX パーサー |
| qif2json.ts | `packages/loot-core/src/server/transactions/import/qif2json.ts` | ソース | QIF パーサー |
| xmlcamt2json.ts | `packages/loot-core/src/server/transactions/import/xmlcamt2json.ts` | ソース | CAMT パーサー |
| ImportTransactionsModal.tsx | `packages/desktop-client/src/components/modals/ImportTransactionsModal/ImportTransactionsModal.tsx` | ソース | インポートモーダル |
| FieldMappings.tsx | `packages/desktop-client/src/components/modals/ImportTransactionsModal/FieldMappings.tsx` | ソース | フィールドマッピングUI |
| utils.ts | `packages/desktop-client/src/components/modals/ImportTransactionsModal/utils.ts` | ソース | インポートユーティリティ |
| app.ts | `packages/loot-core/src/server/accounts/app.ts` | ソース | importTransactions 関数 |
| accountsSlice.ts | `packages/desktop-client/src/accounts/accountsSlice.ts` | ソース | Redux actions |
| sync.ts | `packages/loot-core/src/server/accounts/sync.ts` | ソース | reconcileTransactions |
| index.ts | `packages/loot-core/src/server/importers/index.ts` | ソース | 予算インポートエントリ |
| ynab4.ts | `packages/loot-core/src/server/importers/ynab4.ts` | ソース | YNAB4 インポーター |
| ynab5.ts | `packages/loot-core/src/server/importers/ynab5.ts` | ソース | YNAB5 インポーター |
