課金履歴機能 — 初期設計セッション

2026-06-07 / スキーマ追加・一覧/詳細/編集画面・計算ロジック・再計算の設計合意。

状態: developing 対象: apps/system-manager + prisma

実装状況(2026-06-07)

型チェック(tsc --noEmit)通過。新規ファイルに lint 警告なし。マイグレーションは未適用(後述)。

対象ファイル状態
Prisma スキーマprisma/schema/billing-record.prisma(+ organization / contract-plan にリレーション)
マイグレーション SQLprisma/migrations/20260607053512_billing_records/✅ 作成(未適用)
型エクスポートpackages/database/index.ts
計算共有関数lib/billing-record/calculate.ts(前月集計+プランスナップショット)
実効値ヘルパlib/billing-record/effective.ts / previous-month.ts
サーバーアクションactions/billing-record/{list,get,list-…-year-month} + 詳細/編集の recalculate / update
一覧画面app/billing-records/page.tsx + 複数選択検索フォーム
詳細画面[billingRecordId]/page.tsx + view + 再計算ボタン(確認ダイアログ)
編集画面[billingRecordId]/edit/ + 手動10フィールド
ナビapp/_components/header-navigation/index.tsx に「課金履歴」追加

マイグレーション未適用について

開発DBに既存のドリフト(ai_instructions / system_configs の unique index が DB 側で欠落)があり、 prisma migrate dev がリセットを要求するため自動適用していない。 billing_records の CREATE TABLE SQL は prisma migrate diff の出力と一致確認済み。 ドリフト解消後に適用すること。

決定事項

  1. 課金モデルは後払い。 (Y年 M月) レコード = M月分の基本月額 + M-1月の生成実績に対する超過課金。計算対象画像生成数は前月(M-1)の生成数。
  2. 日割り(初月按分)は手動上書きで吸収。 自動計算は満額。initialPlanRatio は今回も未使用(課題のまま据え置き)。
  3. レコード生成は手動生成(今回)+自動生成バッチは後回し。 一括生成は将来「自動リクエストの API Route」が担う(スコープ外)。本セッションは計算ロジックを共有関数化し、再計算から呼ぶ。
  4. 再計算は既存レコードのみ更新。 未生成レコードの新規作成は兼ねない。
  5. 区分1の集計は「区分2/3 以外すべて(renovation 等も含む / catch-all)」。 区分2=refinement、区分3=solidFloorPlan。(当初は「renovation 除外」で合意したが、動作確認後に「含む」方針へ変更。)
  6. 計算対象画像生成数にも手動入力用フィールドを追加。 全計算量が「自動値+手動上書き(NULL=自動)」で対称。編集対象は手動フィールド計10個。
  7. 一覧の「課金額」は課金合計額(基本+超過、実効値ベース)。
  8. 再計算の挙動:組織の現在の contractPlanId から自動値を再取得し、手動フィールドはすべて破棄(NULL リセット)。実行前に確認ダイアログを表示する方針。
  9. 命名:モデル BillingRecord、テーブル billing_records、ルート /billing-records、用語「課金履歴」。year:Int / month:Int(1-12)deletedAt ソフトデリート。DB のユニーク制約は設けない(論理削除済み行とのタプル衝突を避けるため。組織×年月の一意性はアプリ側で担保)。
  10. 課金合計額を永続化(amount)。 一覧表示はこの永続値を使用。生成・再計算・編集の各書き込み経路で実効値ベースに再計算して保つ(共有関数 calculateBillingAmount)。
  11. プラン名フィールドは contractPlanName
  12. 備考(note)を追加。 手動入力(編集画面)で更新する場合は必須。
  13. 一括生成 API Route を追加(スコープ内に変更)。 POST /api/billing-records/generate。対象月の未生成組織にレコードを一括作成。

更新(動作確認後)

一括生成 API・再計算とも catch-all 集計・Asia/Tokyo 境界・DateTime へ統一(経路間の差異は解消、calculate.ts は削除)。 区分1 は「区分2/3 以外すべて(renovation 含む)」を正と確定。 基本月額は当月(M)分・超過分は前月(M-1)分の表示に確定。 日時は LocalDateTime を非推奨化し DateTime へ統一規約: 日時・タイムゾーン)。 残りは集計ロジックの重複解消のみ → 課題

詳細画面:課金内訳イメージ(後払いモデル)

例:3月のレコード。基本月額は3月分、超過分は2月の生成実績に対して課金される。

ABC不動産 / 2026年 3月 課金合計 ¥58,000
基本月額(3月分 実効(月額) ¥50,000
超過 区分1(2月分
超過 20 × ¥200
max(0, 120 − 100) × 200 ¥4,000
超過 区分2 画像キレイ(2月分
超過 8 × ¥500
max(0, 58 − 50) × 500 ¥4,000
超過 区分3 3D間取り(2月分
超過 0(上限内)
max(0, 12 − 20) × 800 ¥0

※ 再計算は手動入力をすべて破棄し、現在のプランと実測値で再計算します。

スキーマ追加(予定)

/// 課金履歴
///
/// @namespace 課金
model BillingRecord {
  id String @id @default(cuid(2)) @db.VarChar(30)

  organizationId String  @map("organization_id") @db.VarChar(30)
  contractPlanId String? @map("contract_plan_id") @db.VarChar(30)
  planName       String? @map("plan_name")

  year  Int @map("year")
  month Int @map("month")

  // --- 自動計算値(プラン由来のスナップショット) ---
  monthlyCharge           Int @map("monthly_charge")
  monthlyGenerationQuota1  Int @map("monthly_generation_quota_1")
  monthlyGenerationQuota2  Int @map("monthly_generation_quota_2")
  monthlyGenerationQuota3  Int @map("monthly_generation_quota_3")
  extraGenerationCharge1   Int @map("extra_generation_charge_1")
  extraGenerationCharge2   Int @map("extra_generation_charge_2")
  extraGenerationCharge3   Int @map("extra_generation_charge_3")

  // --- 自動計算値(前月実測の計算対象生成数) ---
  targetGenerationCount1 Int @map("target_generation_count_1")
  targetGenerationCount2 Int @map("target_generation_count_2")
  targetGenerationCount3 Int @map("target_generation_count_3")

  // --- 手動上書き(NULL のとき上の自動値を使用) ---
  manualMonthlyCharge           Int? @map("manual_monthly_charge")
  manualMonthlyGenerationQuota1 Int? @map("manual_monthly_generation_quota_1")
  manualMonthlyGenerationQuota2 Int? @map("manual_monthly_generation_quota_2")
  manualMonthlyGenerationQuota3 Int? @map("manual_monthly_generation_quota_3")
  manualExtraGenerationCharge1  Int? @map("manual_extra_generation_charge_1")
  manualExtraGenerationCharge2  Int? @map("manual_extra_generation_charge_2")
  manualExtraGenerationCharge3  Int? @map("manual_extra_generation_charge_3")
  manualTargetGenerationCount1  Int? @map("manual_target_generation_count_1")
  manualTargetGenerationCount2  Int? @map("manual_target_generation_count_2")
  manualTargetGenerationCount3  Int? @map("manual_target_generation_count_3")

  createdAt DateTime  @default(now()) @map("created_at")
  updatedAt DateTime  @default(now()) @updatedAt @map("updated_at")
  deletedAt DateTime? @map("deleted_at")

  organization Organization  @relation(fields: [organizationId], references: [id])
  contractPlan ContractPlan? @relation(fields: [contractPlanId], references: [id])

  // 論理削除運用のため @@unique は設けない(削除済み行とのタプル衝突を避ける)。
  @@map("billing_records")
}

Organization / ContractPlan 側にもリレーション(billingRecords BillingRecord[])を追加。プラン名・型・命名は実装時に最終確認。

実装メモ(次の作業)