# 画面設計書 17-請求管理

## 概要

本ドキュメントは、OpenCodeコンソールの請求管理画面の設計書である。ワークスペースの残高管理、自動リロード設定、月間利用上限、支払い履歴を管理する画面である。管理者のみがアクセス可能。

### 本画面の処理概要

請求管理画面は、Zenサービスの課金・支払いを包括的に管理するための画面である。残高の追加、自動リロードの設定、月間利用上限の設定、支払い方法の管理、過去の支払い履歴の確認が可能。Stripe連携により安全な決済処理を提供する。

**業務上の目的・背景**：Zenは従量課金制サービスであり、ユーザーは事前にクレジットをチャージして利用する。本画面は管理者がワークスペースの課金状態を管理し、残高不足による利用停止を防ぐための自動リロード機能、コスト管理のための月間上限設定を提供する。

**画面へのアクセス方法**：ワークスペース詳細レイアウトのナビゲーションから「Billing」を選択（管理者のみ表示）、または`/workspace/:id/billing`に直接アクセス。

**主要な操作・処理内容**：
1. 現在の残高確認と手動チャージ
2. 自動リロードの有効化/無効化と金額設定
3. 月間利用上限の設定
4. 支払い方法の確認・変更（Stripe管理画面へ遷移）
5. 支払い履歴の確認とレシートダウンロード
6. サブスクリプション契約（Black）の確認

**画面遷移**：Stripe Checkout、Stripe Customer Portalへのリダイレクトあり。

**権限による表示制御**：管理者のみアクセス可能。sessionInfo?.isAdmin判定で全セクションの表示を制御。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 26 | 使用量追跡 | 主機能 | 使用量に基づく請求情報の表示 |

## 画面種別

詳細 / 設定 / 一覧

## URL/ルーティング

| 環境 | URL |
|------|-----|
| 本番 | `https://opencode.ai/workspace/:id/billing` |
| 開発 | `http://localhost:3000/workspace/:id/billing` |

ルーティング定義: `packages/console/app/src/routes/workspace/[id]/billing/index.tsx`

## 入出力項目

### 入力項目（残高追加）

| 項目名 | 項目ID | データ型 | 必須 | 入力形式 | バリデーション | 備考 |
|--------|--------|----------|------|----------|----------------|------|
| 追加金額 | addBalanceAmount | number | Yes | 数値入力 | reloadAmountMin以上 | デフォルトはreloadAmount |

### 入力項目（自動リロード設定）

| 項目名 | 項目ID | データ型 | 必須 | 入力形式 | バリデーション | 備考 |
|--------|--------|----------|------|----------|----------------|------|
| 自動リロード有効 | reload | boolean | Yes | チェックボックス | - | true/false |
| リロード金額 | reloadAmount | number | 有効時Yes | 数値入力 | RELOAD_AMOUNT_MIN以上 | 自動追加する金額 |
| トリガー残高 | reloadTrigger | number | 有効時Yes | 数値入力 | RELOAD_TRIGGER_MIN以上 | この金額以下で発動 |

### 入力項目（月間上限設定）

| 項目名 | 項目ID | データ型 | 必須 | 入力形式 | バリデーション | 備考 |
|--------|--------|----------|------|----------|----------------|------|
| 月間利用上限 | monthlyLimit | number | No | 数値入力 | 0以上、null許可 | 空欄は無制限 |

### 出力項目（請求情報）

| 項目名 | データ型 | 取得元 | 備考 |
|--------|----------|--------|------|
| 残高 | number | billingInfo.balance | マイクロ通貨単位 |
| 顧客ID | string | billingInfo.customerID | Stripe顧客ID |
| 支払い方法タイプ | string | billingInfo.paymentMethodType | "card" / "link" |
| 支払い方法下4桁 | string | billingInfo.paymentMethodLast4 | カード番号下4桁 |
| 自動リロード状態 | boolean | billingInfo.reload | true/false |
| リロード金額 | number | billingInfo.reloadAmount | 自動リロード時の金額 |
| トリガー残高 | number | billingInfo.reloadTrigger | リロード発動残高 |
| 月間上限 | number | billingInfo.monthlyLimit | null=無制限 |
| リロードエラー | string | billingInfo.reloadError | 失敗時のエラーメッセージ |

## 表示項目

### セクション構成（条件付き表示）

| セクション | コンポーネント | 表示条件 | 説明 |
|-----------|---------------|----------|------|
| BlackSection | BlackSection | subscriptionID または timeSubscriptionBooked | サブスクリプション契約表示 |
| BillingSection | BillingSection | isAdmin=true | 残高・支払い方法管理 |
| ReloadSection | ReloadSection | customerID存在 | 自動リロード設定 |
| MonthlyLimitSection | MonthlyLimitSection | customerID存在 | 月間上限設定 |
| PaymentSection | PaymentSection | customerID存在 | 支払い履歴 |

### BillingSection詳細

| 要素 | 表示条件 | 説明 |
|------|----------|------|
| 残高表示 | 常時 | $X.XX形式で表示 |
| 残高追加フォーム | customerID存在 | 金額入力→Stripe Checkout |
| 課金有効化ボタン | customerID未設定 | Stripe Checkoutへ遷移 |
| クレジットカード表示 | customerID存在 | カード種別・下4桁 |
| Stripeリンク表示 | paymentMethodType="link" | Stripeアカウント連携 |
| 管理ボタン | customerID存在 | Stripe Customer Portal |

### ReloadSection詳細

| 要素 | 表示条件 | 説明 |
|------|----------|------|
| 状態表示 | 常時 | "Enabled" / "Disabled" |
| 設定内容表示 | reload=true | 金額、トリガー残高、手数料 |
| 設定フォーム | フォーム表示時 | 有効化トグル、金額、トリガー |
| リロードエラー | reloadError存在 | 失敗日時、理由、リトライボタン |

### PaymentSection詳細

| カラム | 説明 |
|--------|------|
| Date | 支払い日時 |
| Payment ID | Stripe支払いID |
| Amount | 金額（$X.XX形式）+ タイプ（credit/subscription） |
| Receipt | レシートダウンロードボタン |

## イベント仕様

### 1-残高追加（手動チャージ）

| 項目 | 内容 |
|------|------|
| トリガー | 「Add」ボタンクリック |
| 処理 | onClickCheckout関数が実行される |
| 詳細フロー | 1. createCheckoutUrl actionを実行<br>2. 成功時: Stripe Checkout URLへリダイレクト<br>3. 支払い完了後: 残高に反映 |

### 2-課金有効化

| 項目 | 内容 |
|------|------|
| トリガー | 「Enable Billing」ボタンクリック（customerID未設定時） |
| 処理 | onClickCheckout関数（デフォルト金額） |
| 結果 | Stripe Checkoutで初回支払い→customerID設定 |

### 3-支払い方法管理

| 項目 | 内容 |
|------|------|
| トリガー | 「Manage」ボタンクリック |
| 処理 | onClickSession関数が実行される |
| 詳細フロー | 1. createSessionUrl actionを実行<br>2. Stripe Customer Portal URLへリダイレクト |

### 4-自動リロード設定

| 項目 | 内容 |
|------|------|
| トリガー | 「Save」ボタンクリック |
| 処理 | setReload actionが実行される |
| 詳細フロー | 1. [SERVER] reload, reloadAmount, reloadTrigger取得<br>2. バリデーション（有効時は金額・トリガー必須）<br>3. BillingTableを更新<br>4. 一覧を再取得 |

### 5-リロードリトライ

| 項目 | 内容 |
|------|------|
| トリガー | 「Retry」ボタンクリック（リロードエラー時） |
| 処理 | reload actionが実行される |
| 詳細フロー | 1. [SERVER] Billing.reload()を実行<br>2. 成功時: リロードエラーをクリア<br>3. 失敗時: エラーを更新 |

### 6-レシートダウンロード

| 項目 | 内容 |
|------|------|
| トリガー | 「View」ボタンクリック |
| 処理 | downloadReceipt actionが実行される |
| 詳細フロー | 1. [SERVER] Billing.generateReceiptUrl()を実行<br>2. 新しいタブでレシートURLを開く |

## データベース更新仕様

### 操作別データベース影響一覧

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| ページロード | BillingTable | SELECT | 請求情報の取得 |
| 自動リロード設定 | BillingTable | UPDATE | reload, reloadAmount, reloadTrigger更新 |
| リロード実行 | BillingTable | UPDATE | balance更新（Stripe経由） |
| 支払い履歴取得 | PaymentTable | SELECT | 支払い一覧の取得 |

### テーブル別更新項目詳細

#### setReload アクション

```typescript
const setReload = action(async (form: FormData) => {
  const reloadValue = form.get("reload")?.toString() === "true"
  const reloadAmount = parseInt(form.get("reloadAmount"))
  const reloadTrigger = parseInt(form.get("reloadTrigger"))

  return Database.use((tx) =>
    tx.update(BillingTable).set({
      reload: reloadValue,
      reloadAmount,
      reloadTrigger,
      ...(reloadValue ? { reloadError: null, timeReloadError: null } : {}),
    })
  )
})
```

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|--------|------|
| UPDATE | reload | true/false | 自動リロード有効/無効 |
| UPDATE | reloadAmount | 数値 | 自動リロード金額 |
| UPDATE | reloadTrigger | 数値 | トリガー残高 |
| UPDATE | reloadError | null | 有効化時にエラーをクリア |
| UPDATE | timeReloadError | null | 有効化時にエラー日時をクリア |

## メッセージ仕様

| メッセージID | メッセージ種別 | 表示条件 | メッセージ内容（キー） |
|-------------|---------------|----------|----------------------|
| title | タイトル | 常時 | workspace.billing.title |
| subtitle | サブタイトル | 常時 | workspace.billing.subtitle |
| currentBalance | ラベル | 常時 | workspace.billing.currentBalance |
| addBalance | ボタン | customerID存在 | workspace.billing.addBalance |
| enable | ボタン | customerID未設定 | workspace.billing.enable |
| manage | ボタン | customerID存在 | workspace.billing.manage |
| loading | ボタン | 処理中 | workspace.billing.loading |
| reload.title | タイトル | 常時 | workspace.reload.title |
| reload.enabled.state | 状態 | reload=true | workspace.reload.enabled.state |
| reload.disabled.state | 状態 | reload=false | workspace.reload.disabled.state |
| payments.title | タイトル | 支払い履歴あり | workspace.payments.title |

## 例外処理

| 例外パターン | 検出方法 | 対応処理 | ユーザーへの通知 |
|------------|----------|---------|----------------|
| Checkout URL取得失敗 | actionのエラーレスポンス | エラー内容を返却 | フォーム内エラー表示 |
| Session URL取得失敗 | actionのエラーレスポンス | エラー内容を返却 | （通常発生しない） |
| リロード金額不足 | バリデーション | { error: formErrorReloadAmountMin } | フォーム内エラー表示 |
| トリガー金額不足 | バリデーション | { error: formErrorReloadTriggerMin } | フォーム内エラー表示 |
| 自動リロード失敗 | reloadError存在 | エラー表示+リトライボタン | エラーセクション表示 |

## 備考

- 管理者のみアクセス可能（sessionInfo?.isAdmin判定）
- 残高はマイクロ通貨単位（100,000,000 = $1.00）
- Stripe手数料は (金額 + $0.30) / 0.956 * 0.044 + $0.30 で計算
- BlackSection: エンタープライズサブスクリプション契約画面
- 各セクションはCSSモジュールでスタイリング

---

## コードリーディングガイド

本画面を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

本コンポーネントでは複数のサーバーサイドクエリとアクションを使用する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | common.tsx | `packages/console/app/src/routes/workspace/common.tsx` | queryBillingInfo（93-120行目） |
| 1-2 | billing-section.tsx | `packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx` | createSessionUrl（12-27行目） |
| 1-3 | reload-section.tsx | `packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx` | setReload（22-59行目）、reload（13-20行目） |
| 1-4 | payment-section.tsx | `packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx` | getPaymentsInfo（9-14行目）、downloadReceipt（16-19行目） |

**読解のコツ**: Billingモジュールの定数（RELOAD_AMOUNT_MIN、RELOAD_TRIGGER_MIN）がバリデーションに使用される。

#### Step 2: エントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | index.tsx | `packages/console/app/src/routes/workspace/[id]/billing/index.tsx` | セクション構成と表示条件 |

**主要処理フロー**:
1. **11-12行目**: createAsync()でsessionInfo、billingInfo取得
2. **15-31行目**: 各セクションの条件付きレンダリング

#### Step 3: 各セクションを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | billing-section.tsx | `packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx` | BillingSection（29-251行目） |
| 3-2 | reload-section.tsx | `packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx` | ReloadSection（61-219行目） |
| 3-3 | payment-section.tsx | `packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx` | PaymentSection（21-129行目） |
| 3-4 | monthly-limit-section.tsx | `packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.tsx` | MonthlyLimitSection |
| 3-5 | black-section.tsx | `packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx` | BlackSection |

### プログラム呼び出し階層図

```
BillingPage (index.tsx)
    |
    +-- createAsync(querySessionInfo) -> isAdmin判定
    |
    +-- createAsync(queryBillingInfo) -> 請求情報
    |
    +-- <Show when={isAdmin}>
            |
            +-- <BlackSection /> (条件付き)
            |
            +-- <BillingSection />
            |       |
            |       +-- createAsync(queryBillingInfo)
            |       +-- useAction(createCheckoutUrl)
            |       +-- useAction(createSessionUrl)
            |       +-- onClickCheckout() -> Stripe Checkout
            |       +-- onClickSession() -> Stripe Customer Portal
            |
            +-- <ReloadSection /> (customerID存在時)
            |       |
            |       +-- createAsync(queryBillingInfo)
            |       +-- useSubmission(setReload)
            |       +-- useSubmission(reload)
            |
            +-- <MonthlyLimitSection /> (customerID存在時)
            |
            +-- <PaymentSection /> (customerID存在時)
                    |
                    +-- createAsync(getPaymentsInfo)
                    +-- useAction(downloadReceipt)
```

### データフロー図

```
[入力]                    [処理]                         [出力]

ページロード ─────────> queryBillingInfo()
                              |
                              v
                        請求情報取得 ──────────────────> 各セクション表示

残高追加 ────────────> createCheckoutUrl()
                              |
                              v
                        Stripe Checkout URL ──────────> Stripeへリダイレクト
                              |
                              v
                        支払い完了 ───────────────────> 残高更新

自動リロード設定 ────> setReload action
                              |
                              v
                        [SERVER] BillingTable更新
                              |
                              v
                        revalidate queryBillingInfo ──> UI更新

支払い履歴表示 ──────> getPaymentsInfo()
                              |
                              v
                        [SERVER] Billing.payments()
                              |
                              v
                        支払い一覧 ───────────────────> テーブル表示

レシートダウンロード ─> downloadReceipt()
                              |
                              v
                        [SERVER] Billing.generateReceiptUrl()
                              |
                              v
                        レシートURL ──────────────────> 新タブで開く
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.tsx | `packages/console/app/src/routes/workspace/[id]/billing/index.tsx` | ソース | ページエントリーポイント |
| billing-section.tsx | `packages/console/app/src/routes/workspace/[id]/billing/billing-section.tsx` | ソース | 残高・支払い方法セクション |
| reload-section.tsx | `packages/console/app/src/routes/workspace/[id]/billing/reload-section.tsx` | ソース | 自動リロードセクション |
| monthly-limit-section.tsx | `packages/console/app/src/routes/workspace/[id]/billing/monthly-limit-section.tsx` | ソース | 月間上限セクション |
| payment-section.tsx | `packages/console/app/src/routes/workspace/[id]/billing/payment-section.tsx` | ソース | 支払い履歴セクション |
| black-section.tsx | `packages/console/app/src/routes/workspace/[id]/billing/black-section.tsx` | ソース | サブスクリプションセクション |
| common.tsx | `packages/console/app/src/routes/workspace/common.tsx` | ソース | 共通クエリ・アクション |
| billing.js | `packages/console-core/src/billing.js` | ソース | Billingドメインロジック |
| billing.sql.js | `packages/console-core/src/schema/billing.sql.js` | ソース | BillingTableスキーマ |
