# 通知設計書 7-Webプッシュ通知

## 概要

本ドキュメントは、Fizzyアプリケーションにおける「Webプッシュ通知」機能について、その設計仕様を記載するものである。

### 本通知の処理概要

アプリ内通知と連動して、ブラウザのプッシュ通知機能を通じてユーザーのデスクトップまたはモバイルデバイスに通知を送信する機能を提供する。

**業務上の目的・背景**：ユーザーがFizzyアプリケーションを開いていない状態でも、重要な通知をリアルタイムで受け取れるようにする必要がある。Webプッシュ通知により、カードの更新、コメント、メンションなどの重要なイベントを即座にユーザーに伝えることができ、レスポンスの迅速化とコラボレーションの活性化が期待できる。

**通知の送信タイミング**：アプリ内通知が作成または更新された際（source_idが変更された際）に、PushNotifiableコンサーンによりPushNotificationJobがキューに投入され、非同期でプッシュ通知が送信される。

**通知の受信者**：プッシュ通知の購読（Push::Subscription）を登録しているユーザーが受信者となる。ただし、通知作成者がシステムユーザーの場合、ユーザーが非アクティブの場合、アカウントが非アクティブの場合は送信されない。

**通知内容の概要**：通知には、タイトル（カードタイトルまたはメンション情報）、本文（イベント内容や抜粋）、カードへのリンクパス、未読バッジ数が含まれる。

**期待されるアクション**：受信者はプッシュ通知をクリックし、直接対象カードにアクセスして、必要なアクションを行うことが期待される。

## 通知種別

プッシュ通知（Web Push API）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（Active Job + Web Push Pool） |
| 優先度 | 高（urgency: "high"） |
| リトライ | Active Jobのデフォルト設定に準拠 |
| プロトコル | Web Push Protocol (RFC 8030) |

### 送信先決定ロジック

1. 通知対象ユーザーのpush_subscriptionsを取得
2. 送信可否判定（should_push?）:
   - ユーザーにpush_subscriptionsが存在すること
   - 通知作成者がシステムユーザーでないこと
   - ユーザーがアクティブであること
   - アカウントがアクティブであること

```ruby
def should_push?
  notification.user.push_subscriptions.any? &&
    !notification.creator.system? &&
    notification.user.active? &&
    notification.account.active?
end
```

## 通知テンプレート

### プッシュ通知ペイロード

| 項目 | 内容 |
|-----|------|
| title | 通知タイトル |
| body | 通知本文 |
| icon | `/apple-touch-icon.png` |
| path | カード詳細画面へのパス |
| badge | 未読通知数 |

### イベント種別ごとのペイロード

| イベント種別 | タイトル | 本文 |
|------------|---------|------|
| card_published | {カードタイトル/番号} | "Added by {作成者名}" |
| card_assigned | {カードタイトル/番号} | "Assigned to you by {作成者名}" |
| card_closed | {カードタイトル/番号} | "Moved to Done by {作成者名}" or "Closed by {作成者名}" |
| card_reopened | {カードタイトル/番号} | "Reopened by {作成者名}" |
| comment_created | "RE: {カードタイトル/番号}" | {コメント抜粋（200文字）} |
| mention | "{名前} mentioned you" | {本文抜粋（200文字）} |

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| - | - | - | 添付ファイルなし |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| title | 通知タイトル | イベント種別に依存 | Yes |
| body | 通知本文 | イベント種別に依存 | Yes |
| path | カードパス | cards.id + account.slug | Yes |
| badge | 未読通知数 | notifications.unread.count | Yes |
| icon | アイコンパス | 固定値 | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| コールバック | Notification after_save_commit | source_id_previously_changed? | 通知のソースが変更された際 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| push_subscriptions.empty? | プッシュ通知の購読がない場合 |
| creator.system? = true | システムユーザーによる通知の場合 |
| user.active? = false | ユーザーが非アクティブの場合 |
| account.active? = false | アカウントが非アクティブの場合 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[Notification保存] --> B{source_id変更?}
    B -->|Yes| C[PushNotificationJob キュー投入]
    B -->|No| D[処理終了]
    C --> E[NotificationPusher#push]
    E --> F{should_push?}
    F -->|No| D
    F -->|Yes| G[build_payload]
    G --> H{source_type?}
    H -->|Event| I[build_event_payload]
    H -->|Mention| J[build_mention_payload]
    H -->|Other| K[build_default_payload]
    I --> L[push_to_user]
    J --> L
    K --> L
    L --> M[user.push_subscriptions取得]
    M --> N[WebPush Pool キュー投入]
    N --> O[WebPush::Notification#deliver]
    O --> P[Web Push API送信]
    P --> Q[終了]
```

## データベース参照・更新仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| notifications | 通知情報取得 | 対象通知 |
| push_subscriptions | 購読情報取得 | エンドポイント、キー情報 |
| users | ユーザー情報取得 | アクティブ状態確認 |
| accounts | アカウント情報取得 | アクティブ状態確認 |
| cards | カード情報取得 | タイトル、パス生成 |
| events | イベント情報取得 | source_typeがEventの場合 |
| mentions | メンション情報取得 | source_typeがMentionの場合 |

### テーブル別参照項目詳細

#### push_subscriptions

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | サブスクリプションID | ユーザー単位で取得 |
| endpoint | プッシュサービスエンドポイント | 送信先URL |
| p256dh_key | 暗号化用公開鍵 | ペイロード暗号化 |
| auth_key | 認証キー | ペイロード暗号化 |
| user_id | ユーザーID | ユーザー関連付け |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| - | - | プッシュ通知送信では更新なし |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| ActiveJob::DeserializationError | 通知が削除済み | PushNotificationJobでdiscard |
| WebPush::ExpiredSubscription | 購読が期限切れ | 該当Subscriptionを削除 |
| WebPush::InvalidSubscription | 購読が無効 | 該当Subscriptionを削除 |
| 接続エラー | プッシュサービス接続失敗 | リトライ |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | Active Jobデフォルト（5回） |
| リトライ間隔 | 指数バックオフ |
| リトライ対象エラー | StandardError（DeserializationError除く） |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | WebPush Poolの設定に依存 |
| 1日あたり上限 | 制限なし |

### 配信時間帯

制限なし（24時間配信可能）

### 許可されたエンドポイントホスト

| ホスト | サービス |
|-------|---------|
| jmt17.google.com | Google (旧) |
| fcm.googleapis.com | Firebase Cloud Messaging |
| updates.push.services.mozilla.com | Mozilla |
| web.push.apple.com | Apple |
| notify.windows.com | Microsoft |

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

- プッシュ通知ペイロードはWeb Push暗号化仕様に従い暗号化される
- VAPID (Voluntary Application Server Identification) による認証を使用
- エンドポイントURLは許可されたホストのみ（SSRF対策）
- エンドポイントIPはパブリックIPのみ許可（SsrfProtection）
- HTTPSのみ許可

## 備考

- Push::SubscriptionはPERMITTED_ENDPOINT_HOSTSでホワイトリスト制御
- WebPush Poolは設定ファイル（Rails.configuration.x.web_push_pool）で管理
- VAPID設定はRails.configuration.x.vapidで管理
- 未読バッジ数はユーザーの未読通知数から動的に取得

---

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

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

### 推奨読解順序

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

まず、プッシュ通知で使用されるデータモデルの構造を理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | push/subscription.rb | `app/models/push/subscription.rb` | 購読モデル、エンドポイント検証（1-55行目） |
| 1-2 | schema.rb | `db/schema.rb` | push_subscriptionsテーブル定義（418-429行目） |

**読解のコツ**: PERMITTED_ENDPOINT_HOSTSで許可されたプッシュサービスのみ使用可能。resolved_endpoint_ipでSSRF対策も実施。

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

処理の起点となるファイル・関数を特定する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | push_notifiable.rb | `app/models/concerns/push_notifiable.rb` | after_save_commitでpush_notification_laterが呼ばれる（1-12行目） |
| 2-2 | push_notification_job.rb | `app/jobs/push_notification_job.rb` | NotificationPusherの呼び出し（1-7行目） |

**主要処理フロー**:
1. **5行目（push_notifiable.rb）**: `after_save_commit :push_notification_later, if: :source_id_previously_changed?`
2. **9-11行目**: `PushNotificationJob.perform_later(self)`

#### Step 3: 通知送信ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | notification_pusher.rb | `app/models/notification_pusher.rb` | プッシュ通知送信の中核ロジック（1-124行目） |

**主要処理フロー**:
- **11-17行目**: push メソッド
  ```ruby
  def push
    return unless should_push?
    build_payload.tap do |payload|
      push_to_user(payload)
    end
  end
  ```
- **20-25行目**: should_push? の判定条件
- **27-36行目**: build_payload でsource_typeに応じたペイロード生成

#### Step 4: ペイロード構築を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | notification_pusher.rb | `app/models/notification_pusher.rb` | イベント種別ごとのペイロード構築（38-94行目） |

**主要処理フロー**:
- **38-75行目**: build_event_payload（イベント種別ごとの分岐）
- **77-86行目**: build_mention_payload
- **88-94行目**: build_default_payload

#### Step 5: Web Push送信を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | web_push/notification.rb | `lib/web_push/notification.rb` | Web Push API送信処理（1-29行目） |

**主要処理フロー**:
- **7-14行目**: deliver メソッド
  ```ruby
  def deliver(connection: nil)
    WebPush.payload_send \
      message: encoded_message,
      endpoint: @endpoint, endpoint_ip: @endpoint_ip,
      p256dh: @p256dh_key, auth: @auth_key,
      vapid: vapid_identification,
      connection: connection,
      urgency: "high"
  end
  ```
- **22-24行目**: encoded_message（JSON形式でtitle, options含む）

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

```
Notification#save (source_id変更時)
    │
    ├─ PushNotifiable#push_notification_later (after_save_commit)
    │      └─ PushNotificationJob.perform_later(self)
    │             └─ NotificationPusher.new(notification).push
    │                    │
    │                    ├─ #should_push?
    │                    │     ├─ push_subscriptions.any?
    │                    │     ├─ !creator.system?
    │                    │     ├─ user.active?
    │                    │     └─ account.active?
    │                    │
    │                    ├─ #build_payload
    │                    │     ├─ Event → build_event_payload
    │                    │     ├─ Mention → build_mention_payload
    │                    │     └─ Other → build_default_payload
    │                    │
    │                    └─ #push_to_user
    │                           └─ #enqueue_payload_for_delivery
    │                                  └─ Rails.configuration.x.web_push_pool.queue
    │
    └─ Push::Subscription#notification
           └─ WebPush::Notification.new(...)
                  └─ #deliver
                         └─ WebPush.payload_send(...)
```

### データフロー図

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

Notification         ───▶ NotificationPusher            ───▶ プッシュ通知ペイロード
  │                                │                              │
  ├─ source_type                   ├─ should_push?判定            ├─ title
  ├─ source_id                     ├─ build_payload               ├─ body
  ├─ user_id                       │                              ├─ icon
  ├─ creator_id                    │                              ├─ path
  └─ card_id                       │                              └─ badge
                                   │
Push::Subscription  ───────────────┼────────────────────▶ WebPush::Notification
  │                                │                              │
  ├─ endpoint                      │                              └─ WebPush.payload_send
  ├─ p256dh_key                    │                                     │
  └─ auth_key                      │                                     ▼
                                   │                              プッシュサービス
                                   │                                (FCM/Mozilla/Apple等)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| notification_pusher.rb | `app/models/notification_pusher.rb` | モデル | プッシュ通知送信の中核 |
| push/subscription.rb | `app/models/push/subscription.rb` | モデル | 購読モデル定義 |
| push_notifiable.rb | `app/models/concerns/push_notifiable.rb` | Concern | プッシュ通知トリガー |
| push_notification_job.rb | `app/jobs/push_notification_job.rb` | ジョブ | 非同期ジョブ |
| web_push/notification.rb | `lib/web_push/notification.rb` | ライブラリ | Web Push API送信 |
| notification.rb | `app/models/notification.rb` | モデル | 通知モデル（PushNotifiableをinclude） |
| schema.rb | `db/schema.rb` | スキーマ | テーブル定義（push_subscriptions: 418-429行目） |
