← 一覧に戻る

Phase 2 TDD実装プラン: 電話番号登録フロー + 無料利用制限

2026年3月14日 18:10 更新
MD から自動変換されたページです。内容について質問があれば右下の ? ボタンからどうぞ。

Context

LINE Call Reminder Bot は現在シングルユーザー設計 — env.MY_PHONE_NUMBER 固定で自分にしか電話できない。友達に試してもらうには、ユーザーごとの電話番号登録と通話コスト制御が必要。Phase 1 の TDD iteration 6 まで完了(全36テスト通過)の状態から継続する。

Codex レビュー反映事項

Codex (gpt-5.4) によるレビューで Critical 3件・High 3件の指摘を受け、以下を修正:

  1. INSERT OR REPLACEON CONFLICT DO UPDATE — REPLACE は行削除→再挿入なので call_count がリセットされる脆弱性
  2. β版アクセス制御の追加 — 任意の LINE ユーザーが任意の番号に発信できる迷惑発信装置リスク → ALLOWED_LINE_USER_IDS 環境変数で許可制に
  3. 無料枠チェックに pending 件数を含める — 登録時チェック vs 発信時カウントのタイミング差で上限を突破できる脆弱性
  4. 既存 reminders の移行計画 — Phase 1 の pending リマインダーが「ユーザー未登録」で全滅する問題
  5. 月判定の JST 明示化 — UTC で月判定すると JST 月初でずれる問題 → 境界テスト追加
  6. cron 二重実行リスク — 既知の問題だが Phase 2 のスコープ外。Phase 3 で status='processing' 導入予定として記録

スキーマ変更

-- schema.sql に追加
CREATE TABLE users (
  line_user_id TEXT PRIMARY KEY,
  phone_number TEXT NOT NULL,       -- E.164形式: +819012345678
  call_count INTEGER DEFAULT 0,     -- 今月の通話回数
  call_count_month TEXT,            -- リセット判定用: 'YYYY-MM'(JST基準)
  created_at INTEGER DEFAULT (unixepoch())
);

環境変数の追加

# wrangler.toml に追加
ALLOWED_LINE_USER_IDS = ""  # カンマ区切りの許可済みLINE User ID

# .dev.vars に追加
ALLOWED_LINE_USER_IDS=Uxxxx,Uyyyy

定数

const FREE_MONTHLY_LIMIT = 10;  // β版: 月10回まで無料

TDDイテレーション(RED → GREEN[Codex] → REFACTOR)

Iteration 7: 電話番号の判定と正規化(純関数)

ファイル: src/phone.ts(新規), src/phone.test.ts(新規)

テスト 入力 期待値
090 番号を検出 isPhoneNumber("09012345678") true
ハイフン付きを検出 isPhoneNumber("090-1234-5678") true
080/070 も検出 isPhoneNumber("08012345678") true
リマインダーは false isPhoneNumber("明日9時に歯医者") false
桁数不足は false isPhoneNumber("0901234567") false
E.164形式は false isPhoneNumber("+819012345678") false(ユーザー入力としては非想定)
前後空白は true isPhoneNumber(" 09012345678 ") true(trim して判定)
電話番号+テキストは false isPhoneNumber("09012345678です") false(完全一致のみ)
全角数字は false isPhoneNumber("09012345678") false
E.164 に正規化 normalizePhoneNumber("090-1234-5678") "+819012345678"
ハイフンなしも正規化 normalizePhoneNumber("09012345678") "+819012345678"

Iteration 8: 電話番号メッセージで users テーブルに登録 + アクセス制御

ファイル: src/index.ts, src/index.test.ts, schema.sql, worker-configuration.d.ts

テスト シナリオ 期待動作
許可ユーザー + 電話番号 → 登録 ALLOWED に含まれるユーザーが "09012345678" 送信 users に INSERT + 確認返信
ハイフン付き → 正規化保存 "090-1234-5678" 送信 +819012345678 で保存
確認返信の内容 「電話番号を登録したよ!」を含む
番号再登録 → 番号のみ更新 既登録ユーザーが別番号送信 phone_number のみ更新、call_count は維持
未許可ユーザー → 拒否 ALLOWED に含まれないユーザーからのメッセージ 「このBotはβ版で招待制だよ」返信

実装メモ:


Iteration 9: 未登録/登録済みユーザーの分岐

ファイル: src/index.ts, src/index.test.ts

テスト シナリオ 期待動作
未登録 → 登録促す users にいないユーザーが「明日9時に歯医者」 「電話番号を教えてね」返信、D1保存なし
登録済み → 従来通り users にいるユーザーが「明日9時に歯医者」 リマインダー保存 + 確認返信

実装メモ:

既存テストへの影響(詳細):


Iteration 10: cron で users.phone_number を使って発信

ファイル: src/index.ts, src/cron.test.ts

テスト シナリオ 期待動作
users から番号取得 pending リマインダーの user が登録済み users.phone_number で makeCall
ユーザー未登録 line_user_id に対応する users がない status='failed', reason='ユーザー未登録'
MY_PHONE_NUMBER 不要 env に MY_PHONE_NUMBER なし 正常に動作

実装メモ:

移行計画:


Iteration 11: 通話成功時に call_count をインクリメント

ファイル: src/index.ts, src/cron.test.ts

テスト シナリオ 期待動作
成功 → count +1 通話成功後 UPDATE users SET call_count = call_count + 1, call_count_month = ?
月初リセット call_count_month が先月 call_count = 1, call_count_month = 今月 に更新
同月 → 加算 call_count_month が今月 count に +1 のみ
call_count_month が null 初回発信 call_count = 1, call_count_month = 今月 に設定
JST 月境界 2026-03-31 23:59 JST(UTC 14:59) 3月としてカウント
JST 月境界 2026-04-01 00:01 JST(UTC 15:01前日) 4月としてカウント

実装メモ:


Iteration 12: リマインダー登録時に月間上限チェック

ファイル: src/index.ts, src/index.test.ts

テスト シナリオ 期待動作
上限到達 → 拒否 call_count=8 + pending 2件 = 10 「今月の無料枠を使い切ったよ」返信、D1保存なし
上限未到達 → 許可 call_count=5 + pending 2件 = 7 通常通りリマインダー保存
先月の count → リセット扱い call_count=10 だが先月 completed count を 0 扱いで許可
pending のみで上限到達 call_count=0 + pending 10件 拒否

実装メモ:


影響を受ける既存ファイル

ファイル 変更内容
schema.sql users テーブル追加
src/index.ts アクセス制御 + phone 分岐 + users 確認 + 上限チェック、cron に users 参照 + call_count 更新
src/index.test.ts mockDB を SQL パターンマッチ方式に刷新、既存テスト 6 件以上の mock 更新
src/cron.test.ts mockDB 拡張(users SELECT/UPDATE)、MY_PHONE_NUMBER 依存除去
wrangler.toml ALLOWED_LINE_USER_IDS 環境変数追加
worker-configuration.d.ts ALLOWED_LINE_USER_IDS: string 追加、MY_PHONE_NUMBER をオプショナルに(最後に wrangler types で再生成)

新規ファイル

ファイル 内容
src/phone.ts isPhoneNumber(), normalizePhoneNumber()
src/phone.test.ts 上記のテスト

スコープ外(Phase 3 以降)

GREEN フェーズの方針

各イテレーションの RED(テスト作成)は私が担当。GREEN(実装)は以下で Codex に委譲:

~/bin/codex-full exec "テストを通して。変更は最小限に。"

REFACTOR は実装後に私がレビューして判断。

検証手順

各イテレーション完了時:

  1. pnpm exec tsc --noEmit — 型チェック
  2. pnpm test — 全テスト通過
  3. コミット & プッシュ(CI自動デプロイ)

全イテレーション完了後:

  1. LINE Bot に未許可ユーザーでメッセージ → 「β版で招待制」返信
  2. 許可ユーザーで 09012345678 を送信 → 登録確認返信
  3. 明日9時に歯医者 を送信 → リマインダー登録
  4. cron 発火 → 登録した番号に電話が来る
  5. 月10回発信後 → 「無料枠を使い切った」メッセージ確認
📝 質問モード — テキストを選択してね
✓ 質問を送信しました