# 機能設計書 4-参加コードによるアカウント参加

## 概要

本ドキュメントは、Fizzyアプリケーションにおける参加コード（招待リンク）を使用して既存アカウントに参加する機能の設計書である。管理者が発行した参加コードを使用することで、新規ユーザーや他アカウントのユーザーがチームに参加できる。

### 本機能の処理概要

**業務上の目的・背景**：チームやプロジェクトへの新規メンバー追加を簡便に行うため、招待リンク方式を採用している。管理者がメールアドレスを事前に登録する必要がなく、参加コードを共有するだけでメンバーを招待できる。これにより、オンボーディングの手間を大幅に削減し、チーム拡大をスムーズに行える。

**機能の利用シーン**：新規メンバーが招待リンク（/join/{code}形式のURL）をクリックし、メールアドレスを入力してアカウントに参加する。既存ユーザーが別のアカウントに参加する場合にも使用される。

**主要な処理内容**：
1. 参加コード付きURLへのアクセス（/join/{code}）
2. 参加コードの有効性確認（存在確認、利用制限確認）
3. メールアドレス入力と送信
4. Identityの検索または新規作成
5. アカウントへのユーザー参加（User作成）
6. 参加コードの使用回数カウントアップ
7. 既存セッションの場合は直接ランディング、新規の場合はマジックリンク認証

**関連システム・外部連携**：メール送信（マジックリンク認証用）にRailsのAction Mailerを使用。

**権限による制御**：参加コードは認証不要でアクセス可能。参加コードの管理（編集・リセット）はadmin権限が必要。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 6 | 参加コード入力画面 | 主画面 | 参加コードを使用したアカウント参加処理 |
| 7 | 参加コード無効画面 | 結果表示画面 | 無効な参加コード使用時のエラー表示 |
| 45 | ユーザー参加画面 | 参照画面 | 新規ユーザーが参加する処理 |

## 機能種別

CRUD操作 / データ作成 / 認証処理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| code | String | Yes | 参加コード（URLパス） | 12文字、4文字ごとにハイフン区切り |
| email_address | String | Yes | 参加するユーザーのメールアドレス | メールアドレス形式 |

### 入力データソース

- URLパス（参加コード）
- 画面入力（メールアドレス）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| identity | Identity | 作成または取得されたIdentityレコード |
| user | User | 作成されたユーザーレコード |
| magic_link | MagicLink | 新規セッション時に生成されるマジックリンク |

### 出力先

- データベース（identities, users, account_join_codesテーブル）
- メール送信（新規認証時のマジックリンク）
- Cookie（セッショントークン）

## 処理フロー

### 処理シーケンス

```
1. 参加コード画面表示（JoinCodesController#new）
   └─ set_join_codeで参加コード取得
   └─ ensure_join_code_is_validで有効性確認
2. 参加処理実行（JoinCodesController#create）
   └─ set_identityでIdentity取得または作成
   └─ @join_code.redeem_ifでアカウント参加
   └─ @identity.join(account)でUser作成
   └─ 使用回数インクリメント
3. リダイレクト処理
   └─ 既存セッション＋セットアップ済み: ランディング画面
   └─ 既存セッション＋未セットアップ: ユーザー検証画面
   └─ 新規セッション: マジックリンク認証→ユーザー検証画面
```

### フローチャート

```mermaid
flowchart TD
    A[参加コードURL] --> B{参加コード存在?}
    B -->|No| C[404 Not Found]
    B -->|Yes| D{参加コード有効?}
    D -->|No| E[無効画面表示]
    D -->|Yes| F[参加画面表示]
    F --> G[メールアドレス入力]
    G --> H{Identity存在?}
    H -->|Yes| I[Identity取得]
    H -->|No| J{バリデーションOK?}
    J -->|No| K[422エラー]
    J -->|Yes| L[Identity作成]
    I --> M[アカウント参加処理]
    L --> M
    M --> N[使用回数インクリメント]
    N --> O{現在のセッション?}
    O -->|同一Identity| P{セットアップ済み?}
    P -->|Yes| Q[ランディング画面]
    P -->|No| R[ユーザー検証画面]
    O -->|異なるIdentity| S[セッション終了]
    S --> T[マジックリンク送信]
    T --> U[認証→ユーザー検証画面]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | 参加コード形式 | 12文字のBase58、4文字ごとにハイフン区切り | コード生成時 |
| BR-002 | 使用回数制限 | usage_count < usage_limitで有効 | 参加実行時 |
| BR-003 | 最大使用回数 | 100億回まで設定可能 | 管理画面設定時 |
| BR-004 | レート制限 | 3分間に10回まで | 参加実行時 |
| BR-005 | 重複参加防止 | 既に参加済みの場合はUser作成をスキップ | 参加処理時 |
| BR-006 | コードリセット | reset時に新規コード生成と使用回数0リセット | 管理操作時 |

### 計算ロジック

- 参加コード生成: SecureRandom.base58(12)を4文字ごとにハイフンで区切り（例: XXXX-XXXX-XXXX）
- 有効判定: usage_count < usage_limit

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 参加コード取得 | account_join_codes | SELECT | codeとaccount_idで検索 |
| Identity取得/作成 | identities | SELECT/INSERT | メールアドレスで検索または作成 |
| ユーザー作成 | users | INSERT | アカウントとIdentityに紐づくユーザー作成 |
| 使用回数更新 | account_join_codes | UPDATE | usage_countをインクリメント |

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

#### account_join_codes

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | code, account_id | URLパスのcodeとCurrent.account | |
| UPDATE | usage_count | 現在値 + 1 | with_lockで排他制御 |

#### identities

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT/INSERT | email_address | 入力されたメールアドレス | find_or_initialize_by |

#### users

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | account_id | 参加先アカウントのID | |
| INSERT | identity_id | 関連するIdentityのID | |
| INSERT | name | メールアドレス（デフォルト） | 後でユーザー検証画面で変更 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 404 | Not Found | 参加コードが存在しない | 404画面表示 |
| 410 | Gone | 参加コードが無効（使用制限超過） | 無効画面（inactive）表示 |
| 422 | バリデーションエラー | メールアドレス形式不正 | 422レスポンス |
| 429 | レート制限超過 | 3分間に10回超過 | 429レスポンス |

### リトライ仕様

- メールアドレス入力: バリデーションエラー時は再入力可能
- レート制限: 3分後に再試行可能

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

- redeem_if処理: with_lockで排他制御し、使用回数チェックとインクリメントを安全に実行
- Identity.join: トランザクション内でUser作成

## パフォーマンス要件

- 排他制御: with_lockで参加コードの競合を防止
- マジックリンク送信: 非同期処理（deliver_later）

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

- 参加コード: 12文字のBase58で予測困難
- 使用回数制限: 無制限の参加を防止
- レート制限: ブルートフォース攻撃防止
- 排他制御: 競合状態での二重カウント防止

## 備考

- 参加コードは各アカウントに1つずつ自動生成される（after_create :create_join_code）
- 既に同一アカウントに参加済みのユーザーは、User作成をスキップ（find_or_create_by）
- 参加後のユーザー名はメールアドレスがデフォルトで、ユーザー検証画面で変更可能

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | join_code.rb | `app/models/account/join_code.rb` | 参加コードモデル、generate_code、redeem_if、active?メソッド |
| 1-2 | joinable.rb | `app/models/identity/joinable.rb` | Identity#joinメソッド、User作成ロジック |

**読解のコツ**: Account::JoinCodeのredeem_ifメソッドはブロックを受け取り、with_lockで排他制御しながらユーザー参加とカウントアップを行う。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | join_codes_controller.rb | `app/controllers/join_codes_controller.rb` | 参加処理のエントリーポイント |

**主要処理フロー**:
1. **3行目**: レート制限（3分間10回）
2. **5-7行目**: before_actionで参加コード取得と有効性確認
3. **14-28行目**: createアクションで参加処理とリダイレクト分岐
4. **32-42行目**: set_identityでIdentity取得/作成

#### Step 3: 管理機能を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | join_codes_controller.rb | `app/controllers/account/join_codes_controller.rb` | 参加コード管理（表示・編集・リセット） |

**主要処理フロー**:
- **11-16行目**: updateで使用制限更新
- **19-22行目**: destroyでコードリセット

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

```
JoinCodesController#create
    │
    ├─ set_identity
    │      └─ Identity.find_or_initialize_by(email_address:)
    │             ├─ [新規] バリデーション → save!
    │             └─ [既存] そのまま使用
    │
    └─ @join_code.redeem_if { |account| @identity.join(account) }
           │
           ├─ with_lock (排他制御)
           │
           ├─ active? チェック
           │
           ├─ @identity.join(account)
           │      └─ account.users.find_or_create_by!(identity: self)
           │
           └─ increment!(:usage_count)
```

### データフロー図

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

code (URLパス) ─────▶ JoinCodesController ────────────▶ 参加コード検証
                           │
email_address ─────────────▶ set_identity ───────────────▶ Identity（DB）
                           │
                           └──▶ redeem_if ───────────────▶ User（DB）
                                   │                          │
                                   └──▶ increment! ──────────▶ JoinCode更新（DB）
                                                               │
                                   [新規セッション]              └──▶ Cookie/Email
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| join_codes_controller.rb | `app/controllers/join_codes_controller.rb` | コントローラー | 参加処理 |
| join_codes_controller.rb | `app/controllers/account/join_codes_controller.rb` | コントローラー | 参加コード管理 |
| join_code.rb | `app/models/account/join_code.rb` | モデル | 参加コード管理 |
| joinable.rb | `app/models/identity/joinable.rb` | コンサーン | Identity#joinメソッド |
| identity.rb | `app/models/identity.rb` | モデル | ユーザー認証基盤 |
| user.rb | `app/models/user.rb` | モデル | ユーザー管理 |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義（/join/:code） |
