メインコンテンツへスキップ
友田 陽大

金融リテラシー教育のサブスク学習プラットフォーム(マルチチャネル課金・冪等な決済・代理店コミッションをNext.js 16モノレポで構築)

pnpm + Turborepo モノレポ(受講者アプリ/運営管理/共有14パッケージ)|6系統のID・NFT特典から料金を決定的に解決し、Stripe Webhookの冪等性・順序保証・PII墨消し、銀行振込サブスクの状態機械、代理店コミッションの追記専用台帳、型安全規律(as/any/enum禁止+NeverError)まで、中核フルスタックエンジニアとしてチーム開発を牽引

クライアント

某金融リテラシー教育のサブスクリプション学習プラットフォーム(投資・売買を勧誘する場ではなく、『お金の学び』を体系的に提供)|提供形態: 受講者向け Web アプリ + 運営者向け管理ダッシュボード(モノレポ内の2アプリ+14共有パッケージ)|販路: 直販・代理店(アフィリエイト)・NFTホルダー向け優待・外部プラットフォームからの移行コホートが交差するマルチチャネル課金|開発体制: 複数名によるチーム開発(GitHub)。私はリポジトリ全体で個人別最多規模のコミットを担う中核フルスタックエンジニアの一人として、受講者フロント・運営管理画面・サブスク/決済・代理店コミッション・DBスキーマ・認証・共有UIまで主要領域を横断して設計・実装。

私の役割

チーム開発(複数名)の中核フルスタックエンジニアの一人。受講者向けフロントエンド(Next.js 16 / React 19 / RSC)、運営管理ダッシュボード、サブスクリプション/決済ドメイン(共有パッケージ `@workspace/subscription`)、代理店コミッション(`@workspace/commission`)、DBスキーマ(Prisma)、認証(`@workspace/auth`)、共有UIまで主要領域を横断して設計・実装し、対応するユニットテストも担当。とりわけ料金体系の解決・冪等な決済・型安全規律の整備に深く関与した。

課題(Situation & Task)

『学習プラットフォーム』という見た目の裏側で、本質的な難所は課金ドメインの複雑さでした。直販・代理店・NFTホルダー優待・外部プラットフォームからの移行という複数チャネルの利用者が、それぞれ異なる料金・特典・支払い手段(Stripe/銀行振込)で混在します。これを破綻なく1つの基盤で扱い、二重課金・順序逆転・PII漏れ・コミッションの二重計上を構造的に防ぎ、かつ複数人のチームが高速に変更し続けても回帰しない型安全規律を敷くことが要件でした。

サブスク学習プラットフォームに凝縮されていた難所は、以下の5点でした。

  1. マルチチャネル × 多次元の料金体系: 同じプロダクトを、直販・代理店・学生・Alphaコホート・NFTホルダー(旧/新の12ティア)・外部からの移行ユーザーが、異なる料金と無料特典期間で利用します。料金分岐をUIやAPIに散らすと、取りこぼしと不整合が必ず発生します。

  2. 冪等で順序に強い決済: Stripe Webhook は『少なくとも1回』『順不同』で届きます。再送による二重処理、古いイベントの後着によるサブスク状態の巻き戻り、生ペイロードに含まれるPIIの保持——この3つを同時に防ぐ必要がありました。

  3. Stripeに乗らない決済経路: 銀行振込の継続課金は Stripe の外で、入金期限・猶予期間・督促・自動失効を自前で正しく状態遷移させる必要がありました。

  4. 代理店コミッションの正確性: 売上に対する報酬を、月次バッチで at-least-once(再実行が安全)に、丸め誤差なく、最低支払い額未満は翌月へ繰り越して計算する台帳が必要でした。

  5. チームで速く・安全に変える: 複数人が並行開発するため、複雑なドメイン(料金・特典・状態遷移)を型とテストで固め、any や 取りこぼした switch 分岐が本番に出ない規律が不可欠でした。

技術選定の理由(Rationale)

  • Next.js 16 / React 19(RSC)+ pnpm + Turborepo モノレポ: 受講者アプリ・運営管理・14の共有パッケージが同じドメイン語彙を共有するため、ドメインロジックを @workspace/* に集約して単一真実源化。turbo のタスクグラフ(build^buildtypecheck に依存)でビルド順序と差分実行を担保

  • Prisma 7 + PostgreSQL(Supabase/RLSは不採用): 72モデル・2,130行のスキーマを Prisma Migrate で93世代にわたり版管理。認可はRLSではなくアプリ層(サーバーアクション境界の requireAdmin() + ユースケース)に寄せ、監査ログFKは onDelete: Restrict でDB側からも改ざんを防止

  • ドメインを純粋関数に隔離: 料金解決・NFT特典選定・銀行振込の状態遷移・コミッション計算を、副作用のない純粋関数として実装。DBなしでゴールデンに単体テストでき、複雑な分岐を型で網羅検査できるようにした

  • 型安全規律(as/any/enum/non-null を全面禁止 + NeverError: TypeScript の enum すら避けて Union 型+satisfies で表現し、switch の取りこぼしは NeverError(value: never) でコンパイルエラー化。境界は Zod で検証

  • Stripe + 自前の銀行振込パイプライン: カード決済は Stripe に寄せつつ、Stripe に乗らない銀行振込は猶予期間(7日)・失効(30日)・5段階督促を持つ純粋な状態機械で自前運用。冪等性は『送信済みリマインダ集合』で担保

  • 認証は jose(JWT)+ bcrypt + OTP の二段: セッションは __Host- 接頭辞付きCookieのHS256 JWTで、tokenVersion をクレームに載せパスワード変更時に全トークンを一括失効。OTPは HMAC-SHA256・5分TTL・5回ロックアウト。アカウント列挙はダミーハッシュの定数時間比較+応答時間フロアで抑止

実施したこと(Action)

  • 【マルチチャネル料金の決定的な解決】 銀行振込ID・月額/年額支払いコホート・学生リスト・Alpha・旧Pro late・NFT特典(12ティア)という6系統の入力を、優先順位を固定した1つの純粋関数 resolvePricingClassification に畳み込み、決済フロー(Stripe Checkout / 外部移行 / 銀行振込スキップ)と料金バケット(K01/K02/K13)と購入可能プランへ決定的にマッピング。料金ロジックの分散と分岐の取りこぼしを構造的に排除

  • 【NFT特典の安定した解決】 複数保有時は『無料期間が最長の特典』を選ぶ selectPrimaryNft を実装し、無料期間→購入日→ティアの順で決定的にタイブレーク。旧Proは購入日の境界で EARLY/LATE に refine し、外部NFTの保有判定が purchasedAt を返すまでの橋渡しは明示的な暫定オーバーライドテーブルで隔離

  • 【冪等で順序に強いStripe Webhook】 イベントIDの一意制約で再送を排除し、event.createdlastProcessedEventCreatedAt と比較して古いイベントの後着をスキップ(サブスク状態の巻き戻りを防止)。生ペイロードはPIIキー(card/email/ip 等)を再帰的に <redacted> へ墨消ししてから保存し、PIIを保持しない

  • 【銀行振込サブスクの状態機械】 入金期限からの経過日数を、ACTIVE → GRACE_PERIOD(7日)→ SUSPENDED(30日)→ CANCELED の純粋関数 computeNextStatus に落とし込み、別の純粋関数で T-30/14/7/0 の5段階督促を選定。督促は『送信済み集合』への包含チェックで冪等化し、日次Cronで安全に再実行可能に

  • 【代理店コミッションの追記専用台帳】 報酬を ${scope}:${期間}:${通貨}:r${revision} の冪等キーで一意化した追記専用台帳に記録し、月次再計算は revision を増やして安全に再実行。金額は浮動小数を排し BigInt のベーシスポイント演算(10000=100%)で丸め誤差を排除、最低支払い額未満は翌月へ繰り越し

  • 【アトミックな監査ログ】 管理操作のユースケースは Prisma.TransactionClient を第一引数で受け取り、業務書き込みと AdminAuditLogs への記録を同一トランザクションでアトミックに実行。トランザクション境界と revalidatePathrequireAdmin() による認可はサーバーアクション側が所有し、関心を分離

  • 【堅牢な認証の細部】 サインインは未知ユーザーでもダミーハッシュと bcrypt 比較を必ず実行して定数時間化し、パスワード再設定リンク要求は応答時間フロアでアカウント列挙を抑止。tokenVersion をJWTに載せ、パスワード変更で全セッションを一括失効。OTPは HMAC-SHA256 で保存し5回でロックアウト

  • 【型安全規律とテスト】 as/any/enum/non-null を禁止し、switch の網羅性は NeverError でコンパイル時に強制。料金解決・状態機械・コミッション計算・CSV整合などの純粋ロジックを中心に433件のVitestテストをCIで実行(PR毎にPostgresを起動して typecheck → build → test → lint → format を通過させるゲート)

本プロダクトを貫く設計思想は、課金の『正しさ』を運用の注意深さではなく、純粋関数と型、そしてDBの制約で構造的に保証することでした。

複雑さを純粋関数に隔離する: マルチチャネルの料金体系は、UIやAPIハンドラに if を散らすと必ず破綻します。そこで6系統の判定入力(銀行振込・年額/月額コホート・学生・Alpha・旧Pro late・NFT特典)を、優先順位を固定した1つの純粋関数に集約し、決済フローと料金バケットと購入可能プランへ決定的に解決しました。同じ思想で、NFT特典の選定・銀行振込の状態遷移・コミッション計算もすべて副作用のない純粋関数にし、DBなしで網羅的に単体テストできるようにしています。

冪等性を“仕組み”で担保する: Stripe Webhook はイベントIDの一意制約で再送を吸収し、event.created の比較で順序逆転を弾き、PIIは保存前に再帰的に墨消しします。銀行振込の督促は『送信済み集合』への包含チェックで、コミッションは冪等キー付きの追記専用台帳で、それぞれ再実行しても二重処理が起きない構造にしました。決済金額や報酬は BigInt/整数のマイナー単位で扱い、丸め誤差の累積を排除しています。

チームで速く・安全に変える: 複数人が並行開発するため、as/any/enum/non-null を禁止し、Union 型+satisfiesNeverError で「分岐の取りこぼしはコンパイルが落ちる」状態を作りました。境界は Zod で検証し、管理操作は監査ログを業務書き込みと同一トランザクションで残します。PR毎にPostgresを立てて型・ビルド・テスト・Lint・フォーマットを通すCIゲートと、毎日の暗号化バックアップ(pg_dump → age暗号化 → Cloudflare R2)+復旧訓練ワークフローで、本番運用とDRまで作り込んでいます。

技術選定の理由

  • 純粋関数 resolvePricingClassification:6系統の入力から料金を決定的に解決(分岐の取りこぼしを排除)

  • Stripe Webhook:イベントID一意制約+event.created順序保証+PII墨消しで冪等化

  • 銀行振込サブスク:猶予/失効/5段階督促を持つ純粋な状態機械(送信済み集合で冪等)

  • コミッション:冪等キー付き追記専用台帳+BigIntベーシスポイント演算+繰り越し

  • 型安全規律:as/any/enum/non-null禁止+NeverErrorの網羅検査+Zod境界検証

担当領域

  • 受講者向けフロントエンド(Next.js 16 / React 19 / RSC / Mantine)
  • 運営管理ダッシュボード(サーバーアクション+アトミック監査ログ)
  • サブスク/決済ドメイン(`@workspace/subscription`・料金解決・状態機械)
  • 代理店コミッション(`@workspace/commission`・追記専用台帳)
  • DBスキーマ設計(Prisma / PostgreSQL)と認証(`@workspace/auth`)
  • 型安全規律の整備とユニットテスト(Vitest)

使用技術

TypeScript 5
Next.js 16
React 19
React Server Components
Prisma 7
PostgreSQL
Mantine 8
Zod
React Hook Form
Stripe
jose (JWT)
bcryptjs
api.video
Vercel Blob
LINE Messaging API
pnpm
Turborepo
Vitest
Testing Library
Vercel
Vercel Cron
GitHub Actions
age (暗号化)
Cloudflare R2

数字で見る成果

自動テスト
433件Vitest(受講者アプリ247・管理81・共有パッケージ105)。状態機械・料金解決・コミッション計算・CSV整合などの純粋ロジックを網羅
DBマイグレーション
93世代Prisma Migrateで版管理。72モデル・43 enum・2,130行のスキーマを進化
共有パッケージ
14個受講者/管理/scaffoldの3アプリをpnpm+Turborepoのモノレポで構成し、ドメイン語彙を単一真実源に
料金判定の入力系統
6系統銀行振込・年額/月額コホート・学生・Alpha・旧Pro late・NFT特典(12ティア)を、決定的に料金バケットへ解決
定期バッチ(Cron)
9本MP失効・督促・解約復帰・年次プロモ・コミッション月次締めなどをVercel Cronで自動化
Prismaモデル
72モデル学習(コース/ステージ/ステップ)・課金・代理店・NFT特典・監査などのドメインをリレーショナルに表現

成果

  • マルチチャネル課金(直販・代理店・NFT優待・外部移行)を、6系統のID/特典判定から決定的に料金バケットへ解決する純粋関数に集約し、料金ロジックの分散と分岐の取りこぼしを排除
  • Stripe Webhookを「イベントID一意制約+`event.created`の順序保証+PII墨消し」で冪等化し、再送・順序逆転・PII保持の3つを構造的に排除
  • 銀行振込サブスク(非Stripe)をACTIVE→GRACE(7日)→SUSPENDED(30日)→CANCELEDの純粋な状態機械+5段階督促ラダーで自動運用(送信済み集合で冪等化し日次Cronで安全に再実行)
  • 代理店コミッションを冪等キー付きの追記専用台帳+`BigInt`ベーシスポイント演算(10000=100%)+最低支払い繰り越しで、月次バッチを*at-least-once*安全・丸め誤差なく計算
  • 認証はbcryptダミーハッシュの定数時間比較+応答時間フロアでアカウント列挙を抑止し、`tokenVersion`でパスワード変更時に全JWTを一括失効、OTPはHMAC-SHA256・5分TTL・5回ロックアウト
  • `as`/`any`/`enum`/non-nullを全面禁止し、`NeverError`の網羅検査+Zod境界検証で、複雑な課金ドメインを型で固める規律をチーム開発に徹底
  • 管理操作のユースケースは`Prisma.TransactionClient`を第一引数で受け、業務書き込みと監査ログ(`AdminAuditLogs`/FKは`onDelete: Restrict`)を同一トランザクションでアトミックに記録
  • 9本のVercel Cron(MP失効・督促・解約復帰・年次プロモ・コミッション締め等)と、毎日のage暗号化バックアップ(pg_dump→Cloudflare R2)+復旧訓練ワークフローで本番運用・DRを担保
  • 433件のVitestテストで状態機械・料金解決・コミッション計算・CSV整合などの純粋ロジックを網羅し、PR毎にPostgresを起動する品質ゲート(typecheck→build→test→lint→format)で回帰を防止

同様の課題、抱えていませんか?

あなたのビジネス課題も、最新の技術で解決できます。 まずは30分の無料技術相談から、状況をお聞かせください。

自社の課題もSaaS化できるか相談する

プロジェクト単位(請負)・技術顧問、どちらにも対応可能です

全ケーススタディを見る