コンテンツにスキップ

通知・リマインド仕様 (FR-010)

項目内容
対象FRFR-010(通知・リマインド)
優先度
ステータス詳細化済み
フェーズPhase 3

概要

システムはワクチン接種時期・投薬スケジュール・通院予定について、ユーザーが設定したタイミングで通知を送信する。通知はプッシュ通知・アプリ内通知・メール通知の3チャネルで配信され、ユーザーはペット×通知タイプ単位で通知設定をカスタマイズできる。

前提条件(依存FR)

本仕様は以下のFRが提供するデータを通知の元情報として利用する。各FRの仕様策定時に、下記フィールドが含まれることを前提とする。

依存FR必要なデータ用途
FR-006(通院履歴)次回通院予定日通院リマインドのトリガー日
FR-007(ワクチン履歴)次回接種予定日ワクチンリマインドのトリガー日
FR-009(投薬管理)投薬スケジュール(開始日・終了日・頻度)投薬リマインドのトリガー

注記: FR-006/007/009 の仕様は詳細化済み。各仕様で定義されたフィールド(nextVisitDate, nextDueDate, startDate/endDate/timesPerDay)と整合済み。

通知タイプ

タイプトリガーデフォルトリマインド日数
vaccineワクチン次回接種予定日7日前、1日前
medication投薬スケジュール開始日当日(モードにより毎回)
visit次回通院予定日3日前、1日前

配信チャネル

チャネル説明必要な登録情報
プッシュ通知FCM経由でモバイル/PWAに送信デバイストークン
アプリ内通知通知一覧画面で表示(既読/未読管理)なし(認証のみ)
メール通知登録メールアドレスに送信なし(アカウントのメール使用)
  • ユーザーはチャネルごとに ON/OFF を設定できる
  • アプリ内通知は常に生成される(OFF にできない)。プッシュ・メールは任意

通知設定

設定の粒度

ペット×通知タイプの組み合わせごとに設定する。

例: 「そうにゃ」のワクチン通知 → 7日前・1日前にプッシュ+メール、「みけ」の投薬通知 → 毎回プッシュのみ

設定項目

項目説明
enabledbooleanこの通知タイプの有効/無効(default: true)
pushEnabledbooleanプッシュ通知の有効/無効(default: true)
emailEnabledbooleanメール通知の有効/無効(default: false)
remindDaysBeforeinteger[]リマインド日数の配列(例: [7, 1])。通知タイプごとにデフォルトあり
sendTimetime通知送信時刻(default: ‘09:00’)
timezonevarcharタイムゾーン(default: ‘Asia/Tokyo’)

投薬通知の追加設定

項目説明
medicationNotifyModevarcharevery_dose: スケジュール通りに毎回通知 / start_end_only: 投薬開始日・終了日のみ通知(default: ‘every_dose’)

デフォルト値の生成タイミング

  • ペット登録時に全通知タイプのデフォルト設定レコードを自動作成する
  • ユーザーが明示的に設定を変更するまではデフォルト値が適用される

通知スケジューリング

バッチ処理

  • 1日1回のバッチジョブで翌日分までの通知対象を抽出しキューに積む
  • バッチ実行時刻: 毎日 00:00 UTC(各ユーザーの send_time + timezone に従い配信時刻を決定)
  • 投薬の every_dose モードはスケジュール日に毎日生成する

通知生成フロー

1. バッチジョブ起動
2. 通知設定が有効なユーザー×ペット×タイプを走査
3. 各タイプの元データ(次回予定日/投薬スケジュール)を参照
4. remind_days_before に一致する日付の通知を生成
5. アプリ内通知レコードを作成(notifications テーブル)
6. 各チャネル(プッシュ/メール)の配信をキューに登録
7. send_time に従い配信を実行

重複防止

  • 同一ユーザー・同一ペット・同一タイプ・同一トリガー日・同一 remindDaysBefore の通知は1回のみ生成する
  • バッチ処理の冪等性を保証するため、生成済み通知を notifications テーブルで確認してからキューに積む

デバイストークン管理

  • クライアント(Flutter/PWA)がFCMトークンを取得した時点で /api/devices に登録する
  • トークンの更新時は既存トークンを差し替える(同一デバイスの重複登録を防ぐ)
  • ログアウト時に該当デバイスのトークンを削除する
  • プッシュ送信時にFCMから無効トークンのエラーが返った場合は該当レコードを削除する

APIエンドポイント

全エンドポイントで認証必須(__Host-session Cookie)。

デバイストークン

メソッドパス概要
POST/api/devicesデバイストークン登録
DELETE/api/devices/:deviceIdデバイストークン削除

通知設定

メソッドパス概要
GET/api/pets/:petId/notification-settingsペットの通知設定一覧取得
PATCH/api/pets/:petId/notification-settings/:type通知設定を更新

アプリ内通知

メソッドパス概要
GET/api/notifications通知一覧取得(ページネーション)
GET/api/notifications/unread-count未読件数取得
PATCH/api/notifications/:notificationId/read既読にする
POST/api/notifications/read-all全件既読にする

POST /api/devices — デバイストークン登録

// リクエスト
{
"token": "fcm-token-string",
"platform": "ios"
}
フィールド必須バリデーション
tokenvarchar空文字不可
platformvarcharios / android / web
  • 成功時: 201 Created
  • 同一トークンが既に存在する場合: 200 OK(更新扱い)
{
"data": {
"id": 1,
"platform": "ios",
"createdAt": "2026-02-18T10:00:00Z"
}
}

DELETE /api/devices/:deviceId — デバイストークン削除

  • 成功時: 204 No Content
  • 存在しない / 他ユーザーのデバイス: 404 Not Found

GET /api/pets/:petId/notification-settings — 通知設定一覧

ペットに紐づく全通知タイプの設定を返す。

  • 成功時: 200 OK
{
"data": [
{
"type": "vaccine",
"enabled": true,
"pushEnabled": true,
"emailEnabled": false,
"remindDaysBefore": [7, 1],
"sendTime": "09:00",
"timezone": "Asia/Tokyo"
},
{
"type": "medication",
"enabled": true,
"pushEnabled": true,
"emailEnabled": false,
"remindDaysBefore": [0],
"sendTime": "09:00",
"timezone": "Asia/Tokyo",
"medicationNotifyMode": "every_dose"
},
{
"type": "visit",
"enabled": true,
"pushEnabled": true,
"emailEnabled": false,
"remindDaysBefore": [3, 1],
"sendTime": "09:00",
"timezone": "Asia/Tokyo"
}
]
}

PATCH /api/pets/:petId/notification-settings/:type — 通知設定更新

指定したフィールドのみ部分更新する。:typevaccine / medication / visit

// リクエスト例
{
"pushEnabled": false,
"emailEnabled": true,
"remindDaysBefore": [14, 7, 1]
}
  • 成功時: 200 OK、更新後の設定を返す
  • 存在しないペット / 他ユーザーのペット: 404 Not Found
  • 不正な :type: 422 Unprocessable Entity
  • remindDaysBefore のバリデーション: 0以上の整数の配列、最大5要素

GET /api/notifications — 通知一覧

ログインユーザーの通知一覧をページネーションで返す。新しい順にソート。

パラメータ必須デフォルト説明
cursorinteger--前回レスポンスの最後の通知ID
limitinteger-20取得件数(最大50)
  • 成功時: 200 OK
{
"data": [
{
"id": 42,
"petId": 1,
"petName": "そうにゃ",
"type": "vaccine",
"title": "ワクチン接種のリマインド",
"body": "そうにゃの3種混合ワクチン接種予定日が7日後です",
"readAt": null,
"createdAt": "2026-03-01T00:00:00Z"
}
],
"nextCursor": 41
}

GET /api/notifications/unread-count — 未読件数

{
"data": {
"count": 3
}
}

PATCH /api/notifications/:notificationId/read — 既読にする

  • 成功時: 200 OK
  • 既に既読: 200 OK(冪等)
  • 存在しない / 他ユーザーの通知: 404 Not Found

POST /api/notifications/read-all — 全件既読

ログインユーザーの未読通知を全て既読にする。

  • 成功時: 200 OK
{
"data": {
"updatedCount": 5
}
}

認可ルール

  • 全エンドポイントで認証必須
  • デバイストークン: 自分のトークンのみ操作可能
  • 通知設定: 自分が登録したペットのみ操作可能(他ユーザーのペットIDは 404
  • 通知一覧: 自分宛の通知のみ表示(他ユーザーの通知IDを指定した場合は 404
  • Phase 3 では通知はペットオーナーのみに送信する(家族共有メンバーへの通知は FR-011 対応時に拡張)

エッジケース

  • ペット削除(論理削除)後 → 該当ペットの未送信通知はキャンセルされる。通知設定は保持するが新規通知は生成しない
  • デバイストークンが無効になった場合 → FCMエラー時にトークンを自動削除し、プッシュ送信をスキップ
  • 元データ(予定日)が変更された場合 → 次回バッチ処理で新しい日付に基づく通知を生成する。生成済みの未来日通知があれば削除して再生成
  • remindDaysBefore[0] を指定した場合 → 予定日当日に通知
  • 全チャネルが OFF の設定 → アプリ内通知は常に生成される(enabled: true の場合)
  • 通知設定の enabled: false → アプリ内通知を含む全通知を停止
  • ワクチン・通院の予定日が過去日に設定された場合 → リマインド通知は生成しない
  • 投薬の終了日が過去の場合 → 通知を生成しない
  • メール送信失敗時 → リトライせず、アプリ内通知とプッシュ通知には影響しない

通知テンプレート

タイプタイトル本文例
vaccineワクチン接種のリマインド{pet_name}の{vaccine_name}接種予定日が{days}日後です
medication投薬のリマインド{pet_name}の{medication_name}の投薬予定があります
visit通院のリマインド{pet_name}の通院予定日が{days}日後です
  • テンプレートのローカライズは Phase 3 では日本語のみ

拡張予定(現時点ではスコープ外)

  • 家族共有メンバーへの通知配信(FR-011 対応時)
  • 通知テンプレートのカスタマイズ
  • 多言語対応
  • 管理画面からのお知らせ配信(FR-012 対応時)
  • リアルタイム配信(WebSocket / SSE)
  • 通知のスヌーズ・再通知機能

検証方法

  • ワクチン予定日を設定 → remind_days_before に一致する日にアプリ内通知が生成されること
  • 投薬スケジュール設定 → every_dose モードで毎回通知、start_end_only で開始/終了日のみ通知されること
  • 通院予定日を設定 → リマインド通知が生成されること
  • 通知設定の enabled: false で通知が生成されないこと
  • プッシュ通知 OFF 時にプッシュが送信されず、アプリ内通知は生成されること
  • デバイストークン登録 → プッシュ通知が受信できること
  • デバイストークン削除後にプッシュ通知が送信されないこと
  • 通知一覧のページネーションが正しく動作すること
  • 既読/全件既読操作が正しく反映されること
  • 他ユーザーのペット・通知へのアクセスが 404 を返すこと
  • ペット論理削除後に新規通知が生成されないこと
  • 予定日変更時に通知が再生成されること
  • 同一条件の通知が重複生成されないこと
  • send_timetimezone に従った時刻に配信されること