ブラウザウズプレミアム (Web サブスク)
ウズプレミアム (IAP product uzu_premium、月額 980 円) を web (Stripe) から加入できるようにするための技術ドキュメント。ブラウザコイン購入 (Windows) の後続フェーズ (Phase 5) にあたる。
前提
還元方式 (P-1: web 経由の月額を下げる / P-2: 付与コインを増やす) は、コイン価格差の軸 1 / 軸 2 とセットで事業側が未確定。本ドキュメントは還元方式がどちらに転んでも成立する技術設計を先に固めておくもの。
現状の実装 (IAP 直結)
サブスク実装にはプロバイダ抽象化が存在せず、すべて IAP (App Store / Play Store) 直結。ただし「加入中か」の判定だけはプロバイダ非依存になっており、これが web 対応の足場になる。
スキーマ
| テーブル | IAP 依存箇所 |
|---|---|
user_subscriptions | platform が "IOS" / "ANDROID" 決め打ち。契約キーは iap_transaction_id 1 カラム (iOS = originalTransactionId / Android = purchaseToken を詰め込み)。有効期限は持たず expiration_confirmed_at (NULL = 有効) のみ。iap_transaction_id に WHERE expiration_confirmed_at IS NULL の部分ユニークインデックス |
subscription_plans | ストアとの紐付けが iap_product_id (Unique) 一本。Stripe Price ID を持つ場所がない |
iap_subscription_transaction_infos | 課金期ごとの付与記録 (冪等性担保)。名前は iap 固有だが構造は汎用 ((user_subscription_id, transaction_id) Unique) |
プロバイダ非依存な部分 (無改修で動く)
以下はすべて usersubscription.UserID(userID) AND ExpirationConfirmedAtIsNil() の Exist() だけで判定しており、user_subscriptions に有効な行さえ作れば Stripe 契約でもそのまま動く:
User.hasActiveSubscription(GraphQL、user.resolvers.go)- コイン購入時のプレミアムボーナス (
wallet.goのiapDepositingWithBonus) - ログインボーナス (
subscription_login_bonus.go)
月次コイン付与 GrantSubscriptionCoins (usecase/subscription_coins.go) も、冪等キー transactionID が単なる string (課金期単位: iOS = transactionId / Android = latestSuccessfulOrderId) なので、Stripe の invoice ID を渡せばロジックごと流用できる。
IAP 直結な部分 (改修が必要)
- 加入 mutation
startIAPSubscription(start_subscription.gql/ resolver): 入力がiapTransactionId/purchaseToken固定。ストアを正として契約キーを解決する (UZU-5636 対策) - 契約状態の表示
Wallet.GetCurrentSubscription/inferPurchasePlatform(wallet.go): platform で iOS / Android のストア API に分岐。端末 platform ≠ 購入 platform ならIsDifferentPlatformだけ返す - 更新・解約・失効:
appstore_webhook.go/playstore_webhook.go+ 毎時バッチbatch_check_subscription_status(ストア API で状態照会) - 通知正規化層
iap_notification.go: 両ストアの通知をUnifiedNotificationType(InitialBuy / Renewed / Expired / Revoked …) に正規化し、失効確定markSubscriptionExpiredを共有。ここが唯一の再利用しやすい抽象点
設計方針
P-1: Stripe Billing (Checkout mode=subscription) を使う
- store アプリ (ブラウザコイン購入 の
apps/store) に/premiumページを追加し、Checkout Session (mode=subscription) で加入する。決済 UI・3DS・カード更新は Stripe に任せる - 解約・支払い方法変更は Stripe Customer Portal に委ねる (自前の解約 UI を作らない)。store に「契約を管理」リンクを置くだけ
- 月額・還元方式 (P-1 / P-2) は Stripe Price と
subscription_plans.monthly_coin_amountの設定値であり、コード変更なしで動かせる形にする
P-2: user_subscriptions をプロバイダ汎用化する (additive)
ALTER TABLE user_subscriptions
ADD COLUMN provider text; -- 'STRIPE'。NULL = 既存 IAP (platform が IOS/ANDROID)- 契約キーは
iap_transaction_idに Stripe Subscription ID (sub_...) を入れて流用する (Apple / Google の形式と衝突しない。コインのiap_purchase_idに PaymentIntent ID を入れるのと同じ判断) - 部分ユニークインデックスは既存のままで成立する (契約キー空間が重ならないため)。カラム名の rename (
contract_key等) は価値に対して影響範囲が大きいのでやらない subscription_plansは Stripe Price のlookup_key = iap_product_id(=uzu_premium) で対応づけ、新カラムを足さない。web 専用プラン (P-2 で付与コインを変える場合) が必要になったら、そのときuzu_premium_webの別行を追加する
P-3: Stripe webhook を UnifiedNotificationType にマッピングする
コインの /webhook/stripe (Go backend) にサブスク系イベントのハンドラを追加し、既存の正規化層に流し込む:
| Stripe イベント | UnifiedNotificationType 相当 | 処理 |
|---|---|---|
checkout.session.completed (mode=subscription) | InitialBuy | user_subscriptions に行を作成 (provider = 'STRIPE')、初回付与は invoice.paid に委ねる |
invoice.paid | InitialBuy / Renewed | GrantSubscriptionCoins(userSubscription, plan, invoiceID)。冪等キー = invoice ID |
customer.subscription.deleted | Expired | markSubscriptionExpired (既存共有ヘルパー) |
charge.refunded (サブスク分) | Revoked | 失効 + 必要なら付与済みコインの回収 (運用判断) |
- 支払い失敗のリトライ (dunning) は Stripe の Smart Retries に任せ、backend は
invoice.paid/subscription.deletedの結果だけを見る - 毎時バッチ
batch_check_subscription_statusにはprovider = 'STRIPE'の分岐を追加し、Stripe API で Subscription status を照会する取りこぼし救済を IAP と同等に持つ
P-4: 契約表示とガード
GetCurrentSubscriptionに provider 分岐を追加。Stripe 契約は Stripe API からcurrent_period_end等を取得してSubscriptionInfoに詰める- 二重加入ガード: 加入フロー冒頭で
hasActiveSubscriptionを確認し、IAP 契約が有効なユーザーには web 加入をブロックして「アプリ側の契約を解約してから」と案内する。逆方向 (Stripe 契約中に IAP 加入) はアプリ側startIAPSubscriptionで同様にガードする - 端末をまたぐ表示は既存の
IsDifferentPlatformパターンを踏襲 (Stripe 契約はどのプラットフォームから見ても「web で契約中 → 管理は web で」と表示)
P-5: アプリ側 (Windows)
isOSUzuPremiumUnavailable(purchase.dart) で Windows のプレミアム UI が全て隠れている。これを解除し、加入導線は ブラウザコイン購入 D-1 と同じ transfer フローで store の/premiumを開く- サブスク管理 (
_openManageSubscriptions) の Windows 分岐は Customer Portal への導線にする - Remote Config フラグ (
web_premium_purchase等) で段階公開
改修範囲一覧
| 箇所 | 変更 |
|---|---|
schema/user_subscription.go | provider カラム追加 (additive) |
schema/subscription_plan.go | 変更なし (Stripe Price の lookup_key で対応づけ) |
usecase/subscription_coins.go | 変更なし (invoice ID を渡すだけ) |
usecase/wallet.go (GetCurrentSubscription / inferPurchasePlatform) | provider 分岐追加。最も IAP 直結な箇所 |
adapters/rest/iap_notification.go | Stripe イベントのマッピング追加、markSubscriptionExpired 共有 |
/webhook/stripe (コイン対応で新設) | サブスク系イベントのハンドラ追加 |
jobs/batch_check_subscription_status | provider 分岐追加 |
| GraphQL | startIAPSubscription は触らない。web 加入は store → Stripe → webhook 経路のため新 mutation 不要。契約表示系 (GetCurrentSubscription 経由) のみ影響 |
apps/store | /premium ページ + Checkout (mode=subscription) + Customer Portal リンク |
dart/apps/mobile | isOSUzuPremiumUnavailable の解除 (Windows)、管理導線、Remote Config フラグ |
要確認事項
- [ ] 還元方式 (P-1 / P-2): 事業側の決定待ち。P-2 (付与コイン増) の場合は web 専用の
subscription_plans行 (uzu_premium_web) を追加する - [ ] 二重加入の運用: IAP → web への乗り換え導線 (解約タイミング、日割りなし、期限切れまで待つ案内) の UX をどうするか
- [ ] 既存 IAP 契約者の扱い: 移行を促すか、自然流入に任せるか (Apple の 7 日間ルールがあるためアプリ内での告知は Windows のみ)
- [ ] 返金時のコイン回収: サブスク返金時に当月付与済みの期間限定コインを回収するか (IAP 側の現行運用に合わせる)
- [ ] 多通貨・内税: コイン側 (ブラウザコイン購入 D-4) に合わせ JPY / KRW / TWD / HKD を内税 (
tax_behavior: 'inclusive') で提供する。各通貨の月額は当社が定めた相当額を設定。過去にストア間の為替レート差で価格がズレた事故 (2026-03) があるため、web 価格と IAP 価格の並記管理は料金表側で行う - [ ] 資金決済法 / コンプラ: 月次付与の期間限定コインが「対価を得て発行された前払式支払手段」と整理される場合の未使用残高・供託への影響 (コイン側と共通の論点)
実装フェーズ
コイン側 (ブラウザコイン購入) の Phase 1〜4 が完了し、/webhook/stripe と store アプリが存在することが前提。
| フェーズ | 内容 |
|---|---|
| Premium Phase 1: スキーマ汎用化 | user_subscriptions.provider 追加、GetCurrentSubscription の provider 分岐。IAP の挙動を変えないことをテストで担保 |
| Premium Phase 2: Stripe サブスク基盤 | webhook ハンドラ (InitialBuy / Renewed / Expired マッピング)、batch_check_subscription_status 分岐、二重加入ガード |
Premium Phase 3: store /premium | 加入ページ + Checkout (mode=subscription) + Customer Portal 導線。ブラウザ単体で加入 → 月次付与 → 解約の E2E |
| Premium Phase 4: アプリ導線 | Windows の プレミアム UI 解除 + transfer 導線 + Remote Config フラグ。還元方式 (P-1 / P-2) 決定後にリリース |