# 画面設計書 6-参加コード入力画面

## 概要

本ドキュメントは、Fizzyアプリケーションの参加コード入力画面（`app/views/join_codes/new.html.erb`）の設計仕様を記述したものです。

### 本画面の処理概要

参加コード入力画面は、招待リンク（参加コード）を使用して既存のアカウント（組織）に参加するための画面です。参加コードを含むURLにアクセスすると、メールアドレスの入力を求められ、入力後にアカウントへの参加処理が実行されます。

**業務上の目的・背景**：組織やチームへの新規メンバー招待を簡素化するための機能です。管理者が発行した参加コード（招待リンク）を共有することで、新規ユーザーは簡単にアカウントに参加できます。参加コードには使用回数制限が設けられており、不正利用を防止しています。

**画面へのアクセス方法**：参加コードを含むURL（`/join/:code`）に直接アクセスします。通常、管理者から共有された招待リンクをクリック、または招待QRコードをスキャンすることでアクセスします。

**主要な操作・処理内容**：
1. メールアドレスを入力フィールドに入力する
2. 「Continue」ボタンをクリックしてフォームを送信する
3. 入力されたメールアドレスでIdentityを検索または作成
4. 参加コードを使用してアカウントに参加
5. 既存ユーザーかつセットアップ済みの場合はランディングページへ
6. 既存ユーザーかつ未セットアップの場合は検証画面へ
7. 新規ユーザーの場合はマジックリンクを送信して認証後に検証画面へ

**画面遷移**：
- 遷移元：招待リンク（URL、QRコード）からの直接アクセス
- 遷移先：ランディングページ（既存・セットアップ済み）、ユーザー検証画面（セットアップ未完了）、マジックリンク送信完了画面（新規ユーザー）、参加コード無効画面（コードが無効な場合）

**権限による表示制御**：
- 認証・未認証どちらでもアクセス可能
- 参加コードが無効または使用回数超過の場合は無効画面を表示

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 4 | 参加コードによるアカウント参加 | 主機能 | 参加コードを使用したアカウント参加処理 |

## 画面種別

登録（メールアドレス入力・参加）

## URL/ルーティング

| 項目 | 値 |
|------|-----|
| URL | `/join/:code` |
| HTTPメソッド | GET（表示）、POST（参加処理） |
| コントローラー | `JoinCodesController` |
| アクション | `new`（表示）、`create`（参加処理） |
| レイアウト | `public` |

## 入出力項目

### 入力項目

| No | 項目名 | 物理名 | データ型 | 必須 | 最大長 | 入力形式 | バリデーション | 備考 |
|----|--------|--------|---------|------|--------|----------|---------------|------|
| 1 | 参加コード | :code | string | 必須 | - | URL path | 存在確認、アクティブ確認 | URL経由で取得 |
| 2 | メールアドレス | email_address | string | 必須 | - | email | RFC準拠のメール形式 | autocomplete="username" |

### 出力項目

なし（リダイレクト）

## 表示項目

| No | 項目名 | 表示条件 | データソース | 備考 |
|----|--------|----------|-------------|------|
| 1 | タイトル「Join [アカウント名] in Fizzy」 | 常時 | @join_code.account.name | 動的生成 |
| 2 | メールアドレス入力フィールド | 常時 | - | placeholder="Email address" |
| 3 | 「Continue」ボタン | 常時 | 固定文言 | 矢印アイコン付き |
| 4 | フッター | 常時 | _footer部分テンプレート | サポートメール連絡先 |

## イベント仕様

### 1-「Continue」ボタン押下

**トリガー**: フォーム送信（submit）

**処理フロー**:
1. `JoinCodesController#create`が呼び出される
2. レート制限チェック（3分間に10回まで）
3. 参加コードの有効性を確認（before_action）
4. メールアドレスでIdentityを検索または作成
5. `@join_code.redeem_if`でアカウント参加処理を実行
6. Userレコードを取得
7. 分岐処理：
   - **同一Identity かつ セットアップ済み**: ランディングページにリダイレクト
   - **同一Identity かつ 未セットアップ**: ユーザー検証画面にリダイレクト
   - **別のIdentity または 新規**: 現在のセッションを終了し、マジックリンクを送信

**レスポンス**:
- 既存ユーザー（セットアップ済み）: ランディングページにリダイレクト
- 既存ユーザー（未セットアップ）: ユーザー検証画面にリダイレクト
- 新規ユーザー: マジックリンク送信完了画面にリダイレクト

## データベース更新仕様

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| 参加処理（新規Identity） | identities | INSERT | 新規Identityレコード作成 |
| 参加処理 | users | INSERT | 新規Userレコード作成（アカウント参加） |
| 参加処理 | account_join_codes | UPDATE | usage_countをインクリメント |
| 参加処理（新規ユーザー） | magic_links | INSERT | マジックリンクレコード作成 |

### テーブル別更新項目詳細

#### identities（新規の場合のみ）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | email_address | 入力されたメールアドレス（正規化済み） | find_or_initialize_by |
| INSERT | created_at | 現在日時 | 自動設定 |
| INSERT | updated_at | 現在日時 | 自動設定 |

#### users

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | account_id | 参加コードのアカウントID | 外部キー |
| INSERT | identity_id | 対象IdentityのID | 外部キー |
| INSERT | role | member | デフォルト値 |
| INSERT | created_at | 現在日時 | 自動設定 |
| INSERT | updated_at | 現在日時 | 自動設定 |

#### account_join_codes

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | usage_count | usage_count + 1 | increment! |

## メッセージ仕様

| No | メッセージ種別 | メッセージ内容 | 表示条件 |
|----|--------------|---------------|---------|
| なし | - | - | メッセージ表示なし（リダイレクトまたはHTTPステータス） |

## 例外処理

| 例外パターン | 対応内容 | 備考 |
|------------|---------|------|
| 参加コードが存在しない | HTTP 404 Not Found | head :not_found |
| 参加コードが無効（使用回数超過） | 参加コード無効画面を表示（410 Gone） | render :inactive |
| メールアドレス形式不正 | HTTP 422 Unprocessable Entity | Identity.invalid? |
| レート制限超過 | HTTP 429 Too Many Requests | head :too_many_requests |

## 備考

- 参加コードは12文字（4文字x3、ハイフン区切り）で、SecureRandom.base58で生成される
- 参加コードには使用回数制限（usage_limit）があり、上限に達すると無効になる
- `redeem_if`はアトミックな処理（with_lock）で使用回数をインクリメントする
- 同一Identityで既にアカウントに参加している場合、新たなUserは作成されない（`join`メソッドの実装による）
- 未認証ユーザーが参加した場合、マジックリンク認証後にユーザー検証画面に遷移する
- 認証済みユーザーが別のIdentityで参加した場合、現在のセッションは終了される
- フォームには`data: { controller: "form" }`によりStimulus.jsのformコントローラーが適用される

---

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

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

### 推奨読解順序

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

まず、参加コードに関わるデータモデルの構造を理解することが重要です。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | join_code.rb | `app/models/account/join_code.rb` | 参加コードの構造、active?、redeem_if |
| 1-2 | identity.rb | `app/models/identity.rb` | Identityモデル、joinable concern |
| 1-3 | user.rb | `app/models/user.rb` | Userモデル、setup? |

**読解のコツ**: `Account::JoinCode`モデルの`redeem_if`メソッドがトランザクション制御（with_lock）を行っています。`active?`で使用回数制限をチェックしています。

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

処理の起点となるコントローラーを特定します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | join_codes_controller.rb | `app/controllers/join_codes_controller.rb` | new/createアクション、before_action、分岐処理 |

**主要処理フロー**:
1. **5-7行目**: before_action - set_join_code、ensure_join_code_is_valid、set_identity
2. **11-12行目**: `new`アクション - 画面表示のみ
3. **14-29行目**: `create`アクション - 参加処理と分岐リダイレクト
4. **32-42行目**: `set_identity` - Identity検索または作成
5. **44-54行目**: `set_join_code`、`ensure_join_code_is_valid` - 参加コード検証

#### Step 3: ビューとテンプレートを理解する

画面表示のテンプレートを確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | new.html.erb | `app/views/join_codes/new.html.erb` | フォーム構造、動的タイトル |

**主要処理フロー**:
- **1行目**: 動的タイトル生成（アカウント名を含む）
- **6-17行目**: メールアドレス入力フォーム

#### Step 4: 参加処理を理解する

アカウント参加の詳細処理を確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | joinable.rb | `app/models/identity/joinable.rb` | Identity#joinメソッド |
| 4-2 | join_code.rb | `app/models/account/join_code.rb` | redeem_ifメソッド |

**主要処理フロー**:
- **13-17行目（join_code.rb）**: `redeem_if` - アトミックな使用回数インクリメント

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

```
JoinCodesController#create
    │
    ├─ [before_action] set_join_code
    │      └─ Account::JoinCode.find_by(code:, account:)
    │
    ├─ [before_action] ensure_join_code_is_valid
    │      ├─ [nil] head :not_found
    │      └─ [!active?] render :inactive
    │
    ├─ [before_action] set_identity
    │      ├─ Identity.find_or_initialize_by(email_address:)
    │      └─ [new_record?] save! or head :unprocessable_entity
    │
    └─ @join_code.redeem_if { |account| @identity.join(account) }
           │
           ├─ with_lock
           │      └─ increment!(:usage_count)
           │
           └─ User.active.find_by!(account:, identity:)
                  │
                  ├─ [Current.identity && setup?]
                  │      └─ redirect_to landing_url
                  │
                  ├─ [Current.identity && !setup?]
                  │      └─ redirect_to new_users_verification_url
                  │
                  └─ [別のIdentity or 新規]
                         ├─ terminate_session
                         └─ redirect_to_session_magic_link
```

### データフロー図

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

:code (URL) ─────────▶ JoinCodesController
email_address ───────▶        │
                              │
                              ├──▶ set_join_code
                              │        │
                              │        └─[nil]─▶ 404 Not Found
                              │
                              ├──▶ ensure_join_code_is_valid
                              │        │
                              │        └─[!active?]─▶ inactive画面 (410)
                              │
                              ├──▶ set_identity
                              │        │
                              │        └─▶ Identity検索/作成
                              │
                              └──▶ redeem_if
                                       │
                                       ├─▶ User作成（参加）
                                       │
                                       └─▶ usage_countインクリメント
                                                │
                                                ├─[既存・セットアップ済]─▶ landing
                                                │
                                                ├─[既存・未セットアップ]─▶ verification
                                                │
                                                └─[新規]─▶ magic_link
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| join_codes_controller.rb | `app/controllers/join_codes_controller.rb` | コントローラー | 参加コード処理の制御 |
| new.html.erb | `app/views/join_codes/new.html.erb` | ビュー | 参加コード入力画面テンプレート |
| inactive.html.erb | `app/views/join_codes/inactive.html.erb` | ビュー | 参加コード無効画面テンプレート |
| _footer.html.erb | `app/views/sessions/_footer.html.erb` | パーシャル | フッター表示 |
| join_code.rb | `app/models/account/join_code.rb` | モデル | 参加コードの管理 |
| identity.rb | `app/models/identity.rb` | モデル | ユーザー識別情報の管理 |
| joinable.rb | `app/models/identity/joinable.rb` | Concern | アカウント参加処理 |
| user.rb | `app/models/user.rb` | モデル | ユーザー管理 |
| authentication.rb | `app/controllers/concerns/authentication.rb` | Concern | 認証共通処理 |
| routes.rb | `config/routes.rb` | 設定 | ルーティング定義 |
