アカウント・注文・クレジット
Sushi SaaS テンプレートにおける users・orders・credits 台帳の関係を解説。残高の算出式、有効期限の扱い、クレジットの付与・消費・照会に使える API / サービスをまとめます。
なぜ重要なのか
このテンプレートには認証、Stripe と連携できる注文管理、クレジット残高システムが含まれています。テーブル同士のつながりを理解しておけば、プランを追加したり支払いプロバイダを差し替えたりするのが簡単になります。
登場人物(主要テーブル)
users— プロファイルのソース・オブ・トゥルース。uuid(共通キー)、email、locale、role などを保持。Better Auth が作成します。accounts/sessions— Better Auth の内部テーブル。監査には有用ですが、残高計算には直接関与しません。orders— すべての決済の試行/結果。order_no、status、amount、サブスク用sub_*、credits、expired_at。credits— 追記専用の台帳。プラス=付与/加算、マイナス=消費。trans_no、user_uuid、trans_type、credits、order_no、expired_at。tasks— 利用ジョブ(例: テキスト→動画)。credits_usedとcredits_trans_noで消費と結びつけます。- 脇役:
apikeys、affiliatesもuser_uuidにぶら下がります。
関係の早見表(FK は張っていませんが、安定リンクです):
users.uuid→orders.user_uuid|credits.user_uuid|tasks.user_uuid|apikeys.user_uuid|affiliates.user_uuidorders.order_no→credits.order_nocredits.trans_no→tasks.credits_trans_no
すべてのテーブルがタイムスタンプを持ち、履歴再構成や分析に役立ちます。
お財布+レシートのたとえ
creditsは中身の見えるお財布。入金も出金も、必ずレシート(行)を 1 枚入れます。- 入金(プラス行)は残高を増やし、出金(マイナス行)は減らします。期限切れギフト券はレシートは残るけど支払いには使えません。
- 残高 = 期限内のプラス合計 − マイナス合計。
履歴を説明可能に保つため、古いレシートは書き換えず、新しいレシートを追加する「追記専用」を採用しています。
1 アクション→複数行(ウォークスルー)
決済が「支払い済み」になる(Stripe)
orders:order_noの行がpaidに更新され、課金メタデータが入ります。credits:order_payのプラス行{ credits: <order.credits>, order_no }を追加します。affiliates: 必要なら紹介報酬を更新します。
タスクで消費(テキスト→動画)
credits: マイナス行{ trans_type: "task_text_to_video", credits: -N }を挿入しtrans_noを得ます。tasks:{ credits_used: N, credits_trans_no: <その trans_no> }を追加し、誰がどこで使ったかを追跡します。
お試し ping(1 クレジット)
credits: マイナス行{ trans_type: "ping", credits: -1 }。
有効期限
expired_atが過去のプラス行は残高に数えませんが、監査用に履歴には残ります。
便利なサービスと API
src/services/credit.ts#getUserCreditSummary—balance,granted,consumed,expired,expiringSoon[],ledger[](要約)を返します。src/services/credit.ts#getUserCredits— 機能制御用の軽量スナップショット{ left_credits, is_pro, is_recharged }。src/services/credit.ts#decreaseCredits— マイナス行を追加。残高不足なら例外。src/services/credit.ts#increaseCredits— プラス行を追加。Stripe や付与で使用。src/services/stripe.ts#handleCheckoutSession— 支払い完了を注文更新+クレジット付与に変換。src/services/tasks.ts#createTextToVideoTask— クレジットを引き落とし、そのtrans_noを参照するtasks行を作成。
HTTP エンドポイント
POST /api/account/credits— サマリ取得({ includeLedger, ledgerLimit, includeExpiring })。POST /api/account/profile— プロフィール+サマリ({ includeCreditLedger: false }で軽量化)。POST /api/ping— 消費デモ(デフォルト 1)。
いずれも respJson 形式 { code, message, data } で返ります。
ローカルで試す
-
アプリ起動:
.env.localに Postgres 接続文字列を設定しpnpm dev。 -
ユーザー作成:
/ja/signupで登録・ログインします。 -
手動でクレジット付与 (開発時):
insert into credits (trans_no, user_uuid, trans_type, credits, created_at) values ('dev-grant-1', '<ユーザーUUID>', 'manual_adjustment', 100, now());pnpm drizzle-kit studioまたは SQL クライアントで実行できます。 -
残高取得:
curl -X POST http://localhost:3000/api/account/credits \ -H "Content-Type: application/json" \ -H "Cookie: <ブラウザからコピーした認証クッキー>" -
消費をシミュレート:
/api/pingにメッセージを送信するとCreditsAmount.PingCost(デフォルトで 1 クレジット) が差し引かれます。再度サマリを取得して残高変化を確認しましょう。curl -X POST http://localhost:3000/api/ping \ -H "Content-Type: application/json" \ -H "Cookie: <ブラウザからコピーした認証クッキー>" \ -d '{\"message\":\"こんにちは\"}'
カスタマイズのヒント
期限通知の調整
src/services/credit.ts の EXPIRING_WINDOW_DAYS を変更すると「まもなく失効」の判定期間を変えられます。期限切れを自動反映したい場合は、expired_at を過ぎた行に対応するマイナス調整を追加する cron を用意します。
ウェルカムボーナス
insertUser の直後に src/models/credit.ts の insertCredit を呼び出して、初回クレジットを付与します。trans_no は一意にしてください。
別の決済プロバイダと連携
insertOrderに外部プロバイダのデータを渡して注文を作成します。- 対応するクレジット行を追加し、
order_noを紐づければ照合が楽になります。 - 定期課金を扱う場合は UI やドキュメントで
sub_*カラムの情報を表示しましょう。
追加メタデータ
実験 ID などを保存したい場合は、orders / credits に JSON カラム (例: meta) を追加できます。Drizzle のマイグレーションがスナップショットを更新します。
落とし穴とヒント
- 残高は単純な合計で計算します。特定のプラス行を“減らす”ことはしません。マイナス行は
order_noなどで出所を示しますが、過去の行は不変のままです。 getUserValidCreditsはexpired_at順に並べ、消費時に近いソースへ「ひもづけ」しやすくしています。- 厳密な FIFO や付与単位の消費が必要ならモデル拡張を。監査のため台帳の追記専用性は維持しましょう。
users.uuidは各テーブルの「のり」です。安定させましょう。
次のステップ
getUserCreditSummaryの結果を使ってダッシュボードを作成する。/api/pingエンドポイントをベースに、他の機能課金ロジックを組み立てる。- クレジット計算の単体テストを作成し、残高・失効・警告のロジックを検証する。
- 他言語版も必要であれば同期します。