Skip to content

[Release] Pro / 広告リリース(2026-05-31) - #216

Draft
ippei-shimizu wants to merge 216 commits into
stgfrom
release/pro-202605
Draft

[Release] Pro / 広告リリース(2026-05-31)#216
ippei-shimizu wants to merge 216 commits into
stgfrom
release/pro-202605

Conversation

@ippei-shimizu

Copy link
Copy Markdown
Owner

概要

BUZZ BASE Pro リリース(2026年5月31日予定)の統合リリースブランチ。
各機能の作業ブランチはこの release/pro-202605 から切り、このブランチへマージしていく。
本番反映時に stg へマージする。

含まれる issue(ルートリポジトリ: ippei-shimizu/buzzbase)

開発フロー

作業ブランチ(feature/{issue番号}-{機能名})→ release/pro-202605 → stg

マージ予定

2026-05-30 頃 → stg へマージ → 本番反映。
それまでは Draft 状態を維持。

関連ドキュメント(buzzbase ルートリポジトリ)

  • 戦略: docs/strategy/product/pro-plan-202605.md
  • PRD 親: docs/strategy/product/pro-plan-prd-202605.md
  • PRD 各機能: docs/strategy/product/pro-plan-prd/01-11

@ippei-shimizu ippei-shimizu self-assigned this May 16, 2026
ippei-shimizu and others added 28 commits May 17, 2026 15:38
…ーブルを追加 (#317)

業界標準の Entitlement Pattern に沿って Pro 加入状態管理用のテーブルを追加。
既存ユーザーには 'free' subscription を SQL バックフィルで投入する。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Subscription: status / plan_type / platform enum と pro_active? / in_trial? / in_grace_period? / days_remaining / can_use_trial? を実装
- Entitlement concern: FREE_FEATURES / PRO_FEATURES と has_entitlement? を提供
- PlanLimits concern: 練習メニュー・メディア・スケジュール・月次目標の Business Rules
- User: 上記 concern を include し、after_create で 'free' subscription を生成
- UserSubscriptionEvent / WebhookEvent: 監査ログ・冪等性キャッシュ用モデル骨格 (書き込みは #318)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ルーティング:
- GET  /api/v1/pro/status
- POST /api/v1/pro/sync
- GET  /api/v1/pro/entitlements
- POST /api/v1/webhooks/revenuecat

実装方針:
- 各エンドポイントを CRUD 単位のコントローラに分割 (Pro::StatusController, Pro::SyncController, Pro::EntitlementsController)
- Webhook は受信ルートのみ提供。署名検証と状態遷移は #318 で実装
- /pro/status のレスポンスは Design Doc-01 §10 と同形式 ({ subscription: {...}, entitlements: [...] })

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Subscription model: status enum / pro_active? / in_trial? / in_grace_period? / days_remaining / can_use_trial? の境界含む 40 例
- Entitlement concern: FREE_FEATURES / PRO_FEATURES / has_entitlement? のパス網羅
- /api/v1/pro/{status,sync,entitlements}: 認証あり/なしの2分岐
- /api/v1/webhooks/revenuecat: スタブが 200 を返すことの保証

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FREE_FEATURES / PRO_FEATURES の各キーの意味と無料・Pro 境界(数量制限)を
キー横のインラインコメントで明示し、各 Pro 機能 issue 実装時の参照負荷を下げる。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…問題を修正 (#317)

%w[] 内では # 以降もホワイトスペース区切りの文字列として配列に含まれてしまうため、
ALL_FEATURES.size が 22 ではなく 38 になり、ゴミ文字列('#' や日本語説明)が
feature key として誤って認識される問題があった。

配列リテラル ['...'] に書き換え、各要素の後ろに通常の Ruby コメントを置く形に変更。
回帰防止のため、ALL_FEATURES のサイズと key 形式を検証する spec を追加。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
レビュー指摘への対応:
- in_grace_period? が status のみで判定していたため、cancelled/billing_issue で
  expires_at < Time.current のユーザーで true が返ってしまっていた。
  クライアントがこのフラグを Pro アクセス判定に使うため、pro_active? と同じ
  期限チェックを入れる。
- status の enum 定義を index_with(&:itself) のメタプログラミングから、
  plan_type / platform と同じ明示的なハッシュ形式に揃える。
- pending ステータスの意味と PRO_ACTIVE_STATUSES に含めない理由を
  enum 定義部にコメントとして明記。
- spec に pending と in_grace_period? の期限境界ケースを追加。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
レビュー指摘への対応:
- ActiveModelSerializers::SerializableResource.new の冗長な書き方を
  V1::SubscriptionSerializer.new(subscription).as_json に置き換え、
  controller-design.md の規約に沿った形にする
- SubscriptionSerializer に pro_active 属性を追加。クライアントが
  status + expires_at から Pro 利用可否を再計算しなくて済むようにし、
  in_grace_period の期限切れケースとも整合させる
- /pro/status の spec で pro_active フィールドの期待値を検証

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…逆化 (#317)

- 100001: revenuecat_entitlement_id の default 'pro' を撤廃。無料ユーザーは
  NULL のまま残し、Pro 加入時のみ値が入る形に統一する。'pro' をデフォルトに
  置くと、RevenueCat 連携 (#318) で「pro entitlement 保有」と誤判定される恐れがある。
- 100004: backfill の INSERT SQL から revenuecat_entitlement_id 列を除外。
  既存無料ユーザーは NULL で埋める。
- 100004: down は単純な「status = free を全削除」では Pro 解約 → free に
  戻った正規データも巻き込むため、ActiveRecord::IrreversibleMigration に変更。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- spec/factories/subscriptions.rb:
  - User の after_create で free subscription が既に作られているため、
    create(:subscription, :active) でユニーク制約違反が起きていた。
    to_create で「既存 subscription を attributes 上書き」する戦略に変更。
  - default の revenuecat_entitlement_id 'pro' を撤去し、Pro 加入相当の
    trait (trial / active / cancelled / billing_issue / expired) でのみ 'pro' を
    セットする形に整理。マイグレでデフォルトを撤廃したのと整合させる。
  - :pending trait を追加。
- spec/requests/api/v1/webhooks/revenuecat_spec.rb: 空ボディ / Content-Type なし /
  text/plain など RevenueCat の異常系リクエストでも 200 を返すことを保証する
  ケースを追加。#318 で署名検証実装時の指針コメントも記載。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…on-foundation

Add: Pro サブスクリプション骨格 (#317)
リリース: 2026/05/19 stg → main
…ture/344-merge-stg-into-release-pro-202605

# Conflicts:
#	app/models/user.rb
#	db/schema.rb
…-release-pro-202605

Chore: stg を release/pro-202605 に取り込み (#344)
#318 課金フロー実装の前提となる Webhook 受信基盤を整備する。

- gem 追加: solid_queue / stripe / flipper / flipper-active_record
- マイグレーション: solid_queue (11 tables) / flipper (2 tables) / webhook_events.error_message
- 同 DB 運用のため Solid Queue 用 connects_to は設定しない
- application.rb で全環境 queue_adapter を :solid_queue に固定
- Procfile に worker: bin/jobs を追加(Heroku worker dyno 起動)
- initializer 追加: stripe (test/prod 自動切替) / flipper (pro_features, cancellation_survey 予約)

ロジック層 (TDD):
- WebhookEvent#find_or_create_pending! / #mark_processed! / #mark_failed!
- RevenueCatWebhookProcessor: 冪等判定 + 例外時 Sentry 通知 + 未知イベントの warning 記録
- RevenueCatWebhookJob: 失敗時の指数バックオフ最大 5 回リトライ
- RevenuecatController: Authorization Bearer 署名検証 + 新規作成時のみ Job 化

各イベント (INITIAL_PURCHASE / RENEWAL 等) の本処理は #346 で実装する。
`back/CLAUDE.md` のコメント規約に従い、`#346` などの issue 番号参照を
コメントから取り除き、冗長な多段コメントを WHY 中心に圧縮する。
各 event_type ごとに必要な subscription 更新内容を TODO として明記する。
- verify_signature!: 長さの違いをタイミング情報として漏らさないよう SHA256 ハッシュ化後に fixed_length_secure_compare で比較
- event_id 抽出を params.dig(:event, :id) に簡素化
- queue.yml: production の worker polling_interval を 0.1 から 1.0 に緩和(突発トラフィック想定で UX 影響なし、DB 負荷を抑える)
- stripe initializer: テストモード時に本番キーへフォールバックしないよう STRIPE_SECRET_KEY_TEST のみ参照
- Processor#process に failed 再 enqueue 再処理の意図コメントを追加
release/pro-202605 等のリリースブランチ上での push / PR でも CI を走らせる。
…ch-in-ci

Chore: CI workflow の対象ブランチに release/* を追加
…tency-foundation

Add: Webhook 冪等処理基盤 + Solid Queue + Flipper 導入 (#345)
…張 (#346)

- spec/fixtures/revenuecat/*.json を全 10 イベント分整備
- spec/support/revenuecat_fixture_loader.rb で load_revenuecat_fixture(name) を提供
- UserSubscriptionEvent::EVENT_TYPES に initial_purchase / uncancelled / product_changed を追加
- spec/models/user_subscription_event_spec.rb を新規追加(バリデーション・関連・uniqueness 8 example)
- 早期特典窓内 30 日 / 窓外 7 日 / 再加入 0 日の判定を一元化
- ENV (EARLY_SUBSCRIBER_WINDOW_START / END) で窓を override 可能にし、緊急対応を容易にする
- spec 7 example で正常系・境界・ENV override を検証
- INITIAL_PURCHASE / TRIAL_STARTED 兼用。period_type で trial / active を出し分け
- TrialDaysCalculator.in_early_window? で is_early_subscriber を判定
- find_user は Subscription#revenuecat_user_id 優先 → User.id でフォールバック
- 未知 app_user_id は Sentry warning + 処理スキップ(webhook 自体は processed)
- record_subscription_event で RecordNotUnique を握り潰し冪等性を担保
- User#has_many :user_subscription_events を追加(監査ログ参照用)
- spec 10 example を追加(正常系・has_used_trial 出し分け・早期窓・冪等性・未知 user)
- 既存 expires_at が新しい expires_at 以上なら無視(順序逆転対策)
- billing_issue → active 復帰時に recovered イベントを追加記録
- record_subscription_event を event_id 引数化し、派生イベントの uniqueness 衝突を回避
- spec 5 example(通常 / 古い event / billing_issue 復帰)
ippei-shimizu and others added 29 commits July 28, 2026 13:39
強制Proモード実装で追加したusers.is_adminが、v1のUsersController#show_user_id_data
(他ユーザーのプロフィール閲覧)と#search(ユーザー検索)でas_json経由でそのまま
レスポンスに含まれてしまっていた。内部向けの開発用フラグを他ユーザーからも
参照できてしまう状態だったため、except: :is_adminで除外する。
#show(自分自身の情報取得)も同様に除外し、旧クライアント向けv1 API形状の
golden snapshotを変更しない。
F-16(バックグラウンド継続実行)がPro差別化要素としてPRDに明記されているが
対応するentitlementキー自体が存在しなかった。shadow_swing_backgroundを追加する。
実装はmobile側(counter.tsx)。
/api/v2/plans/calendar が entitlement を一切見ておらず、無料ユーザーも
全期間を閲覧できてしまっていた。schedule_calendar_full_history entitlementを
追加し、無料は当日の前後15日(直近月中心)にfrom/toをクランプする。
PRD-06で投手側も打者側と同様のシーズン跨ぎ比較・自己ベスト強調が求められていたが
月別集計のみでgranularity自体が存在しなかった。EraTrendServiceにseason粒度を追加し
season_transition_graph entitlementで打者側と同じくPro限定にする。season粒度選択時の
season_idクランプもbatting_trendと共通化(season_aware_params)した。
レスポンス形状を{trend: [{month, era}]}から{granularity, points: [{key, label, era}]}へ
統一し、batting_trendと同じ形にした。
FinalizeGoalsJobがgoal_badgesを作成する一方、閲覧できるAPI・UIが存在せず
DoDの「バッジ獲得が動作する」が実質未達だった。GET /api/v2/goal_badgesで
自分のバッジを新しい順に返す(閲覧のみ、付与は引き続きFinalizeGoalsJobが担当)。
status判定はgrace_period_expires_dateを考慮したeffective_expires_atを使う一方、
expires_atカラムには生のentitlement['expires_date']を保存していたため、
グレース期間中に同期するとstatusは'active'/'billing_issue'のままexpires_atだけ
過去日付になり、Subscription#pro_active?がfalseを返してPro機能が失われていた。
Webhook Handler群がグレース中にexpires_atを変更しない設計と揃え、
effective_expires_atを保存するようにする。
Net::HTTP.startのタイムアウト・接続断・TLSエラーがRequestFailedErrorへ
変換されておらず、SyncControllerのrescueをすり抜けて素の500になっていた。
想定される主要な失敗モードをRequestFailedErrorへ変換し、意図通りbad_gatewayに
倒れるようにする。open_timeoutも明示的に設定した。
前後15日は狭すぎるとの判断で、今日を中心に前後3ヶ月(月単位)へ広げる。
月末月初での日数ズレを避けるためActiveSupportの3.monthsで計算する。
is_adminカラムのマイグレーションrevert時にdb:schema:loadを実行し、
開発環境の全データ(ユーザー・Flipperフラグ設定等)を消してしまった実例を踏まえ、
特定マイグレーションのみを戻すdb:rollback/db:migrate:downを使うこと、
データ削除を伴う操作は事前に確認することを明記する。
試合記録のみで練習を一度もしていないユーザーで、以下2つの独立した問題により
実際には存在しない「傾向」が断定カードとして表示されていた。

1. 素振り本数のような入力値が全週0(変動が無い)場合でも上位群/下位群に
   分割してしまい、意味の無い差分をあたかも傾向であるかのように表示していた。
   入力値に変動が無いペアは非断定カードにする。
2. 「練習した日数」の判定にintensity_level(草・Streak向けの定義で試合のみの日も
   L4として含む)を使っていたため、試合しか記録していない日も「練習した日」として
   カウントされていた。practice_menu_countまたはtotal_swing_countが実際に
   正の値を持つ日だけを練習日として数えるようにする。
…合を修正

Webhook側のBaseHandler#unknown_product?は未登録のproduct_id/storeが来ると
更新自体をスキップしSentryへ警告するが、SubscriberSyncには同等のガードが無く、
plan_type/platformにnilを静かに保存していた。RevenueCat側に新SKUが追加されて
PlanCatalogの追従が漏れた場合、Webhook経由では検知できてもsync経由では
気付けない非対称が生まれていたため、同じガードを追加する。
現状write_timeoutオプションを渡していないため実害は無いが、将来
write_timeoutを設定した際にこのリストへの追加が漏れやすいため先に含めておく。
Webhook側のENV.fetch('REVENUECAT_WEBHOOK_SECRET', nil)と違いデフォルト値が
無く未設定時はKeyErrorになる点についてレビューで確認を求められたため、
意図的な設計判断であることをコメントで残す。
…eview

Fix: Pro機能Epicリリース前レビュー指摘の解消(Critical+Major)
TrialDaysCalculator.for は早期加入者期間に関わらず常に7日を返すよう
に変更する。is_early_subscriber の判定(in_early_window?)は別用途
のため維持する。
in_early_window? はトライアル日数とは無関係な is_early_subscriber
判定専用の期間であることが伝わるよう、コメント・spec内の表現を統一する。
…-window

Remove: 早期特典期間の30日間トライアル特例ロジックを削除
back/mobileの16件の考慮漏れ・バグ・デグレについて、ユーザーと1件ずつ
合意した対応方針をまとめる。実装はこのプランに従って別セッションで行う。
新しいRENEWALでexpires_atが延長された後に古いEXPIRATIONが遅延して
届いた場合、有効なsubscriptionをexpiredに落とさないようにする
(RenewalHandlerと同じ順序逆転耐性パターン)。
二重配信・同一イベント同時到達時のlost update(read-modify-writeの
交錯)を防ぐため、永続化済みsubscriptionへの処理を行ロックで包む。
SubscriptionEventRecorderのINSERTはsavepointで分離し、監査ログの
uniqueness衝突がロック中のトランザクション全体をabortさせないようにする。
after_commitで同期実行されるDailyActivityRecalculatorの例外を
rescueしてSentry記録に留め、試合結果保存のレスポンス自体は
成功させる。
find_or_create_by!のSELECT→INSERT間の競合でRecordNotUniqueに
なった場合、先勝ちした既存レコードを返して処理を継続する。
Stripe/RevenueCat両webhookコントローラ共通の入口のため1箇所で効く。
with_lock 導入により、メール送信(deliver_now)とExpo Push APIへの
HTTPリクエストが行ロック+トランザクション内で実行される状態だった。
外部サービスの遅延がロック保持時間に直結し、同一subscriptionへの
後続webhookのブロックとDBコネクション占有を招くため、BaseHandlerに
ロック解放後に実行する仕組み(after_unlock)を追加して通知を移した。
通知が同期実行される挙動自体は変えていない。
Fix: issue#450 Pro機能レビュー指摘対応(back)
release/pro-202605にstgを取り込む際、spec/models/user_spec.rbで
Pro Epic側が追加したdescribeブロックとstg側が追加したdescribeブロックが
同じ末尾行に挿入されコンフリクトしていた。両方のテストケースを維持する形で解消する。

他の衝突ファイル(app/models/user.rb, app/mailers/email_authentication_mailer.rb,
email_authentication_mailerのビュー, spec/golden_v1/users_show.json)はGitが
自動マージ済みで、両ブランチの変更が両方とも反映されている。

bundle exec rspec (1638 examples, 0 failures) と rubocop (596 files, no offenses)
で検証済み。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants