概要

項目内容
案件IDMAS-029
カテゴリFP&A / KPIレポーティング(BizDev)
PhaseP2
優先度★★
所要時間3〜4時間
実装ステータス📝 仕様書段階・実装未着手 (2026-04-28 監査時点)
対象ファイル600_report/610_datamart_uniteconomics.js(新規作成)
000_infra/002_constants.jsCAC_ACCOUNTS 追記・MENU_DEFINITION 追記)
600_report/602_datamart_main.jsbuildBudgetTrendDataMart() 末尾に呼び出し追記)
CLAUDE.mdDDL管理外タブリストに 94_metrics_uniteconomics を追記)
前提案件なし(InvoiceRepository / AccountRepository / Utils.normalizePartnerName 等の共通基盤を利用)

目的

月次でユニットエコノミクス(LTV・CAC・LTV/CAC 比率・Payback Period)を自動集計し、94_metrics_uniteconomics シートに出力する。顧客単位の収益性把握と投資回収管理を目的とし、プロダクト事業の拡大投資判断のファクトベースとする。

現在のコード

当該分析シート・ビルダー関数ともに未存在。新規作成案件。

既存の 94_metrics_* タブは存在しない。類似の動的生成シートとして 93_kpi_dashboard(MAS-003、buildKpiDashboard() により 609 で描画)があり、同じ運用パターン(DDL管理外・クリア→再構築)を踏襲する。

修正方針

アーキテクチャ設計

  • 新規ファイル: 600_report/610_datamart_uniteconomics.js
    • 600_report/ 既存ファイルは 601_datamart_ingest.js609_datamart_kpi.js まで存在。次番号 610 で採番する。
  • 新規シート: 94_metrics_uniteconomics
    • 93_kpi_dashboard と同様の DDL管理外・動的生成シートsetupAllSchemas の対象外。
    • CLAUDE.md の「DDL で管理されないタブ」リスト(L191-192)に追記する。
    • レイアウト:
      • 行 = 指標ラベル(下記 10 指標)
      • 列 = 年月(YYYY-MM)
      • 月列は C〜N 列(12列固定)getLastColumn() による動的取得は禁止(YoY差異列等の書式列を含む可能性。失敗パターン #21)
  • エントリポイント:
    • buildUnitEconomicsSheet() を新規関数として 610_datamart_uniteconomics.js に定義。
    • 600_report/602_datamart_main.jsbuildBudgetTrendDataMart() の L391 付近(buildKpiDashboard() 呼び出しの直後)に try { buildUnitEconomicsSheet(); } catch (ueErr) { Utils.logError('buildBudgetTrendDataMart.uniteconomics', ueErr); } を追記する。失敗してもマート更新全体は成功扱いとする(MAS-003 と同等の Resilient 実装)。
    • メニューから単独実行可能にするため、000_infra/002_constants.jsMENU_DEFINITION の「📋 サイドバー: 📊 マート更新」カテゴリ(L230-239)内、📊 KPIダッシュボード再描画 の直後に { label: '📊 ユニットエコノミクス再描画', funcName: 'buildUnitEconomicsSheet', description: 'LTV/CAC/Payback Period (94 タブ) を再計算' } を追記する。

データソース・集計ロジック

  • データ取得: InvoiceRepository.findAll(){ headers, dtos } を取得し、dtos のみ利用する。
  • フィルタ条件: 有効フラグ === true かつ 請求ステータス === '承認済' のレコードのみ対象とする。
  • 月次集計キー: 発生日(P/L計上日)Utils.parseDateToYm()"YYYY-MM" 文字列に正規化してキーとする(Date 型・文字列型の混在に対応済み)。
  • 顧客名正規化: Utils.normalizePartnerName()(MST_PART 照合付き)を第一選択とし、内部でフォールバックとして Utils.generateLogicalAbbr()(法人格除去のみ)が呼ばれるため、呼び出し側では第一選択のみ利用すればよい。
  • 科目マスタ参照: AccountRepository.findAsMap(){ [科目名]: { stmt, cat } } を取得し、cat(大分類)で売上系・原価系を判別する。マスタ未登録の科目名は集計対象からスキップする(例外を throw しない)。

新規顧客管理

  • 全期間の対象 InvoiceDTO を 発生日(P/L計上日) の年月昇順でソートし、Map<正規化顧客名, 初回売上年月> をインメモリ構築する。
  • Date オブジェクトのソートは Utils.parseDateToYm() で正規化した "YYYY-MM" 文字列に対して辞書式比較で行う(失敗パターン #17: Date型と文字列型の混在をそのままソートすると順序が壊れる)。
  • 収支区分 === '収入' のレコードのみ新規顧客判定に使用する(支出側 INV の取引先は顧客ではない)。

粗利計算

  • 月次粗利 = 収入側の売上系大分類の税抜金額_計画合計 − 支出側の原価系大分類の税抜金額_計画合計
  • 大分類 の実在値は 11_mst_account のプルダウン値を参照(実装時に MCP で確認)。暫定値として以下を想定:
    • 売上系大分類: '売上高'
    • 原価系大分類: '売上原価'
  • 実装時に実データの大分類値を確認し、マッチング文字列を確定すること。

CAC 対象費用

  • 000_infra/002_constants.jsConstants オブジェクト末尾、CC_MERCHANT_MAP(L182-195)の直後に新規プロパティ CAC_ACCOUNTS を追加する:
    CAC_ACCOUNTS: ['広告宣伝費', '販売促進費', '採用エージェント費'],  // TODO: ビジネスサイドレビュー必須
    
  • 月次 CAC 対象費用合計 = 対象 InvoiceDTO のうち 科目名 ∈ Constants.CAC_ACCOUNTS かつ 収支区分 === '支出'税抜金額_計画 合計。

指標の計算式(全指標を明示的に定義)

指標計算式単位
CAC(月)Σ CAC対象費用の税抜金額_計画 / 当月新規顧客数円/社
ARPU_粗利(月)月次粗利 / 当月アクティブ顧客数円/社・月
チャーンレート(月)当月チャーン顧客数 / 前月末アクティブ顧客数%
LTV(月)ARPU_粗利 / チャーンレート円/社
Payback Period(月)CAC / ARPU_粗利
LTV/CAC 比率LTV / CAC

顧客定義

区分定義
アクティブ顧客(当月)当月に 収支区分='収入' の InvoiceDTO が存在するユニーク取引先(正規化名)の Set
新規顧客(当月)Map<正規化顧客名, 初回売上年月> で初回売上年月が当月と一致する顧客
チャーン顧客(当月)前月アクティブ Set に含まれ、かつ当月アクティブ Set に含まれない取引先
前月末アクティブ顧客数前月のアクティブ顧客 Set のサイズ(チャーンレートの分母)

出力指標行(10 行)

ラベル備考
1LTV円/社
2CAC円/社
3LTV/CAC 比率
4Payback Period
5新規顧客数
6アクティブ顧客数
7チャーン顧客数
8チャーンレート%
9月次粗利
10CAC対象費用合計

対象月の決定

  • 既定では直近 12ヶ月(当月含む)を出力対象とする。C列=最古月、N列=最新月。
  • 年月ヘッダーは 2 行目。1 行目はタイトル 📊 ユニットエコノミクス(LTV/CAC)。指標ラベルは A 列、単位は B 列、月次値は C〜N 列。
  • 年月セルの書き込みは setNumberFormat('@') + setValue("'" + ym) の apostrophe 前置方式で行う(Sheets 自動数値解釈で 2026-032023 変換される失敗パターン #23 を防止)。

影響範囲

  • 新規ファイル追加: 600_report/610_datamart_uniteconomics.js(約 250〜300 行想定)
  • 既存ファイル末尾追記のみ:
    • 000_infra/002_constants.jsConstants.CAC_ACCOUNTS プロパティ追加(L195 直後に 1 行)、MENU_DEFINITION の「📊 マート更新」カテゴリに 1 項目追加
    • 600_report/602_datamart_main.jsbuildBudgetTrendDataMart() の L391 直後に try { buildUnitEconomicsSheet(); } catch (...) { ... } を 1 行追加
  • ドキュメント更新:
    • CLAUDE.md L191-192 の「DDL で管理されないタブ」リストに 94_metrics_uniteconomics を追記(編集前に git pull origin main 必須)
  • 既存動作への影響: なし(末尾追記のみ・既存ロジック変更なし)

注意事項

  1. Constants.CAC_ACCOUNTS の科目リストはビジネスサイドのレビューを必須とする。暫定リストで動作確認後、正式確定すること。
  2. 顧客名の正規化精度は 12_mst_partner の登録状況に依存する。MST_PART 未登録の取引先は generateLogicalAbbr() の法人格除去のみ適用される(過剰な名寄せは行わない)。
  3. 月列は C〜N 列(12列)固定getLastColumn() による動的取得は禁止(YoY差異列等の書式列を含む可能性。失敗パターン #21)。
  4. 年月のセル書き込みは setNumberFormat('@') + setValue("'YYYY-MM") の apostrophe 前置方式を使用し、Sheets の自動数値解釈(2026-032023 に変換される問題)を防ぐこと(失敗パターン #23)。
  5. InvoiceDTO['発生日(P/L計上日)']Date 型と文字列型の混在があり得る。必ず Utils.parseDateToYm() 経由で正規化すること。
  6. 科目マスタ (11_mst_account) に未登録の科目名は集計対象からスキップする(エラーにしない・ログ出力のみ)。
  7. 94_metrics_uniteconomics は DDL 管理外setupAllSchemas で初期化されないため、buildUnitEconomicsSheet() 内で ss.getSheetByName(...) || ss.insertSheet(...) パターンでシートを確保し、sheet.clear() でクリアしてから再構築する(93_kpi_dashboard と同じパターン)。
  8. オーケストレーション呼び出し箇所は MAS-003 と同様に try { ... } catch { Utils.logError } で囲み、失敗時もマート更新全体は成功扱いとする(Resilient 実装)。

エッジケース

以下の全パターンで例外を throw せず、表示値(文字列 or 数値)をシートに書き込むこと。派生指標は伝播ルールに従う。

条件対象指標表示値理由
当月新規顧客数 = 0CAC'N/A'ゼロ除算
CAC対象科目の当月費用 = 0(新規顧客あり)CAC0費用ゼロとして正常計算
当月アクティブ顧客数 = 0ARPU_粗利'N/A'ゼロ除算
前月末アクティブ顧客数 = 0(全期間最初の月を含む)チャーンレート'N/A'ゼロ除算 / 前月データなし
月次チャーンレート = 0(全顧客継続)LTV'>60ヶ月'無限大に対する上限表示(実務上の上限を 60ヶ月と設定。超過月数は SaaS 指標では実用価値が低いため)
ARPU_粗利 = 0 または 'N/A'Payback Period'N/A'ゼロ除算または前段計算不能
前段指標のいずれかが 'N/A'LTV, Payback Period, LTV/CAC 比率'N/A'伝播ルール(数値演算不能)
CAC = 0 または 'N/A'LTV/CAC 比率'N/A'(CAC=0 時)CAC=0 は「費用ゼロで獲得」を意味し、比率としては意味を持たない
月次粗利 < 0(逆ざや月)月次粗利・ARPU_粗利・LTV計算値をそのまま表示(負値)異常値だが人間の判断に委ねる。フィルタリングしない
科目マスタ未登録の科目名月次粗利・CAC対象費用集計対象から除外(スキップ)例外を throw しない。Utils.logInfo でスキップ件数を記録
MST_PART 未登録の取引先名顧客正規化generateLogicalAbbr() の法人格除去のみnormalizePartnerName() 内でフォールバック済み
発生日(P/L計上日) が空欄または parse 不能全集計対象から除外(スキップ)parseDateToYm() が空文字を返す場合はスキップ
収支区分'収入' '支出' 以外全集計対象から除外(スキップ)仕訳振替等の想定外区分を除外

実データ検証

実装前に MCP 等で以下を確認し、仕様書の暫定値を確定させること。

  1. 11_mst_account大分類 列の実際の値: 売上系・原価系の正確な表記(例: '売上高' '売上原価' '販管費' '営業外収益' 等)を確認。DDLコード値 vs 実データの乖離チェック。
  2. 12_mst_partner の主要取引先登録状況: プロダクト事業の主要顧客が登録されているか確認(normalizePartnerName() の照合精度に直結)。未登録の場合は暫定的に generateLogicalAbbr() ベースで集計し、ビジネスサイドに登録依頼を出す。
  3. CAC_ACCOUNTS 暫定リストの実在確認: '広告宣伝費' '販売促進費' '採用エージェント費'11_mst_account に実在するか確認。未登録の科目はリストから除外し、存在する類似科目を追加提案する。
  4. テスト対象月のデータ有無: 直近 12ヶ月の各月において、収支区分='収入' かつ 請求ステータス='承認済' の InvoiceDTO が最低 1 件存在する月を特定する。全月ゼロだと LTV/CAC の計算結果検証ができないため。
  5. 収支区分 のプルダウン値: '収入' '支出' の 2 値以外が格納されていないか確認(例外的な値が混在すると集計漏れになる)。

関連ドキュメント

仕様書 / 参照先関連箇所
CLAUDE.md「DDL で管理されないタブ」リスト(要追記)/「変更時の動作確認テスト」600_report/6*_datamart_*.js → マート更新テスト
TODO_future.mdMAS-029 案件定義行/関連案件 MAS-003(KPIダッシュボード)・MAS-028(ARR/MRR トラッキング)
dev_mas-024_bep_analysis.md類似の月次指標算出パターン(分母ゼロ処理・isActualOnly 非適用シートの例)
dev_mas-003_kpi_dashboard.mdDDL管理外シート(93_kpi_dashboard)の動的生成パターン
dev_mas-028_arr_mrr_tracking.mdプロダクト事業の顧客単位指標という近接ドメイン
600_report/609_datamart_kpi.js動的シート生成・12ヶ月列固定・年月 apostrophe 書き込みの実装リファレンス
000_infra/003_contracts.jsInvoiceDTO 型定義
200_data/202_repository.jsInvoiceRepository.findAll() / AccountRepository.findAsMap()
000_infra/004_utils.jsparseDateToYm / normalizePartnerName / generateLogicalAbbr

人間が検討すべき事項

TODO_future.md 記載事項(転記):

  • LTVの計算方法: ARPU ÷ チャーンレート方式 or コホートベース方式のどちらを採用するか
  • CACに含める費用の範囲: 広告宣伝費のみか、採用コスト・営業人件費配賦を含めるか

調査で判明した追加事項:

  • 本仕様書は簡易な「ARPU_粗利 / チャーンレート 方式」を採用する。コホート分析ベースの正確な LTV(将来キャッシュフロー割引現在価値)は計算コストが高いため、将来拡張として TODO_future.md に追記する(MAS-031 Compound Growth シミュレーションと合流して検討)。
  • Constants.CAC_ACCOUNTS(科目リスト)はビジネスサイドのレビューが必須。暫定リスト ['広告宣伝費', '販売促進費', '採用エージェント費'] での運用開始後、四半期ごとに見直しを行う(例: 顧客獲得目的のイベント費用・CRM/MAツール費用・パートナー紹介手数料を含めるか)。
  • 予算ベース vs 実績ベースの選択: 現実装は InvoiceDTO['税抜金額_計画'] を使用しているが、実績確定後は 42_trn_journal税抜金額_実績 ベースへの切り替えを検討する。月次締め前後でどちらを使うか運用ルールを確定すること。
  • 請求ステータスの取扱い: 本仕様では '承認済' のみを対象としている。'未処理'(草稿中)を含めるかをビジネスサイドで確認する。月末締め前のタイミングで未処理INVが多い場合、指標が実態より低く見える可能性がある。
  • 「顧客」の定義: 本仕様では 収支区分='収入' の INV の取引先 = 顧客としている。ただし、受託案件の一時顧客とプロダクト継続顧客を混在させると LTV が歪む可能性がある。将来的には PJ マスタで顧客種別(プロダクト / 受託 / その他)を分類して LTV を層別表示することを検討する。
  • LTV 上限値 '>60ヶ月': チャーンレート=0 時の上限表示を 60ヶ月としているが、SaaS 業界ベンチマーク(目安 36〜48ヶ月)と比較して妥当性をレビューする。
  • Payback Period の目安値: 一般的に Payback Period < 12ヶ月 が健全とされる。シート上で閾値超過時に条件付き書式(赤色ハイライト)を将来追加するかを検討する。

実装プロンプト(Claude Code 用)

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-029「ユニットエコノミクス(LTV/CAC)」を実装してください。

## 実行前タスク(Read で実在確認してから実装すること)

Grep は「どこにあるか」の発見まで。「どう書くか」の判断は必ず Read で行うこと。
名前や記憶から推測した瞬間に手を止めて Read する(失敗パターン #18-#20)。

1. `000_infra/003_contracts.js` — `InvoiceDTO` の全フィールド名(特に `有効フラグ`・`発生日(P/L計上日)`・`請求ステータス`・`収支区分`・`取引先名`・`諸表区分`・`大分類`・`科目名`・`税抜金額_計画`)を確認する
2. `000_infra/002_constants.js` — `Constants` オブジェクト末尾の構造を確認し、`CAC_ACCOUNTS` の追記位置(`CC_MERCHANT_MAP` の直後、`MENU_DEFINITION` の前)を特定する。また `MENU_DEFINITION` の「📋 サイドバー: 📊 マート更新」カテゴリ内 `📊 KPIダッシュボード再描画` の位置を確認する
3. `000_infra/004_utils.js` — `Utils.parseDateToYm` / `Utils.normalizePartnerName` / `Utils.generateLogicalAbbr` の引数・戻り値を確認する
4. `200_data/202_repository.js` — `InvoiceRepository.findAll()` と `AccountRepository.findAsMap()` の戻り値型を確認する(`findAll()` は `{ headers, dtos }`、`findAsMap()` は `{ [科目名]: { stmt, cat } }` のキャッシュ付きマップ)
5. `600_report/609_datamart_kpi.js` — シート構築パターン(クリア方法・値のセット方式・apostrophe 前置による年月書き込み・12列固定)を把握する
6. `600_report/602_datamart_main.js` L391 付近 — `buildBudgetTrendDataMart()` 末尾の `buildKpiDashboard()` try/catch パターンを確認し、`buildUnitEconomicsSheet()` の追記位置を特定する

## 修正対象ファイル

- `000_infra/002_constants.js` — `Constants.CAC_ACCOUNTS` 配列を `CC_MERCHANT_MAP` 直後に追記。`MENU_DEFINITION` の「📊 マート更新」カテゴリに項目追加
- `600_report/610_datamart_uniteconomics.js` — 新規作成(約 250〜300 行想定)
- `600_report/602_datamart_main.js` — `buildBudgetTrendDataMart()` L391 直後に `try { buildUnitEconomicsSheet(); } catch (...) { ... }` の 1 行追記
- `CLAUDE.md` L191-192 — 「DDL で管理されないタブ」リストに `94_metrics_uniteconomics` を追記(編集前に `git pull origin main` 必須)

## 実装内容

### 1. `Constants.CAC_ACCOUNTS` と メニュー項目の追加

`000_infra/002_constants.js` の `Constants` オブジェクト、`CC_MERCHANT_MAP` 配列の直後に以下を追加:

    CAC_ACCOUNTS: ['広告宣伝費', '販売促進費', '採用エージェント費'],  // MAS-029: TODO ビジネスサイドレビュー必須

`MENU_DEFINITION` の「📋 サイドバー: 📊 マート更新」カテゴリ内、`📊 KPIダッシュボード再描画` の直後に以下を追加:

    { label: '📊 ユニットエコノミクス再描画', funcName: 'buildUnitEconomicsSheet', description: 'LTV/CAC/Payback Period (94 タブ) を再計算' },

### 2. `buildUnitEconomicsSheet()` の実装

`600_report/610_datamart_uniteconomics.js` を新規作成する。処理フロー(インメモリで Map/Set を活用し、シート参照を最小化すること):

a. `InvoiceRepository.findAll()` で `{ headers, dtos }` を取得し `dtos` のみ使用
b. `有効フラグ === true` かつ `請求ステータス === '承認済'` でフィルタ
c. `発生日(P/L計上日)` を `Utils.parseDateToYm()` で `"YYYY-MM"` 文字列に正規化し、月次グループ(`Map<YYYY-MM, InvoiceDTO[]>`)に分類
d. 対象月の決定: 直近 12ヶ月(当月から過去方向・当月含む)の配列を生成し、C列=最古月、N列=最新月として列対応表を作る
e. 全対象 InvoiceDTO を年月昇順でソートし、`収支区分='収入'` レコードのみで `Map<正規化顧客名, 初回売上年月>` を構築(Date ソートは parseDateToYm 正規化文字列の辞書式比較で行う。失敗パターン #17)
f. `AccountRepository.findAsMap()` で `{ [科目名]: { stmt, cat } }` を取得
g. 月ごとに以下を集計:
   - アクティブ顧客 Set(当月 `収支区分='収入'` のユニーク正規化取引先名)
   - 新規顧客数(`Map` で初回年月が当月と一致する顧客数)
   - チャーン顧客数(前月アクティブ Set と当月アクティブ Set の差分)
   - 月次粗利(売上系大分類の収入合計 − 原価系大分類の支出合計。`acctMap[科目名].cat` で判別。マスタ未登録科目はスキップ)
   - CAC対象費用合計(`Constants.CAC_ACCOUNTS` に含まれる科目名かつ `収支区分='支出'` の `税抜金額_計画` 合計)
h. 各指標を計算(ゼロ除算は仕様書「エッジケース」テーブルに従う):
   - CAC = CAC対象費用合計 / 新規顧客数(新規=0 → `'N/A'`)
   - ARPU_粗利 = 月次粗利 / アクティブ顧客数(アクティブ=0 → `'N/A'`)
   - チャーンレート = チャーン / 前月末アクティブ(前月データなしまたは前月末=0 → `'N/A'`)
   - LTV = ARPU_粗利 / チャーンレート(チャーン=0 → `'>60ヶ月'`、前段 N/A → `'N/A'`)
   - Payback Period = CAC / ARPU_粗利(前段 N/A → `'N/A'`)
   - LTV/CAC 比率 = LTV / CAC(前段 N/A → `'N/A'`)
i. `94_metrics_uniteconomics` シートを `ss.getSheetByName(name) || ss.insertSheet(name)` で確保し、`sheet.clear()` で全クリア
j. ヘッダー書き込み:
   - A1: `📊 ユニットエコノミクス(LTV/CAC)`
   - 2 行目: A=`指標`, B=`単位`, C〜N=年月(`setNumberFormat('@')` + `setValue("'" + ym)` で apostrophe 前置。失敗パターン #23)
k. 指標行書き込み(10 行):
   - 各行で A=ラベル、B=単位、C〜N=月次値(数値 or `'N/A'` or `'>60ヶ月'`)
   - 数値セルは `setNumberFormat('#,##0')`(粗利・CAC・LTV)、`setNumberFormat('0.00%')`(チャーンレート)、`setNumberFormat('0.00')`(LTV/CAC 比率)等を列ではなく行単位で適用
l. `Utils.logInfo` で完了ログ(境界月・処理件数・スキップ件数)を出力

### 3. オーケストレーションへの追記

`600_report/602_datamart_main.js` の `buildBudgetTrendDataMart()` L391(`buildKpiDashboard()` try/catch)の直後に以下を追加:

    // MAS-029: ユニットエコノミクス再描画 (失敗してもマート更新は成功扱い)
    try { buildUnitEconomicsSheet(); } catch (ueErr) { Utils.logError('buildBudgetTrendDataMart.uniteconomics', ueErr); }

### 4. CLAUDE.md の更新

編集前に `git pull origin main` を実行すること。

L191-192 の「DDL で管理されないタブ」リストに `94_metrics_uniteconomics` を追記:

    03_sys_params, 75_ss_equity_changes, 76_notes,
    77_pj_raw, 78_pj_pl, 91_fs_bs, 92_fs_pl, 93_kpi_dashboard, 94_metrics_uniteconomics, 90_test_results

## 制約

- 既存のビルダーファイル(601〜609)のロジックは一切変更しない
- 月列は C〜N 列(12列)固定。`getLastColumn()` 使用禁止(失敗パターン #21)
- 年月文字列は必ず apostrophe 前置(`setValue("'" + ym)`)または `setNumberFormat('@')` で書き込む(失敗パターン #23)
- 科目マスタ未登録の科目名は集計対象からスキップ(例外を throw しない・`Utils.logInfo` でスキップ件数を記録)
- 顧客名正規化は `Utils.normalizePartnerName()` のみを呼び出す(内部で `generateLogicalAbbr()` にフォールバック済み)
- Date ソートは `Utils.parseDateToYm()` 正規化後の文字列で辞書式比較すること(Date型と文字列型の混在ソートは順序が壊れる。失敗パターン #17)
- `94_metrics_uniteconomics` は DDL 管理外のため、`setupAllSchemas` 側には一切手を入れない

## エッジケース

仕様書「## エッジケース」テーブルの全項目に従うこと。特に以下の表示値を厳守:
- 新規顧客数=0 → CAC = `'N/A'`
- チャーンレート=0 → LTV = `'>60ヶ月'`(60 ヶ月は文字列表示)
- 前段指標が `'N/A'` → 派生指標(LTV, Payback Period, LTV/CAC 比率)も `'N/A'`

## 動作確認

1. `npm run push:dev` 後、GAS エディタで `buildUnitEconomicsSheet()` を単体実行する
2. `94_metrics_uniteconomics` シートが生成され、1 行目タイトル・2 行目年月ヘッダー・3 行目以降に 10 指標ラベルが正しく出力されることを確認する
3. 実データが存在する月で LTV・CAC の値が算出され、チャーンレートが `0.00%` 形式で表示されることを確認する
4. 全期間最初の月で チャーンレート = `'N/A'` になることを確認する(前月データなし)
5. 新規顧客数=0 の月で CAC = `'N/A'` になることを確認する
6. チャーンレート=0 の月で LTV = `'>60ヶ月'` になることを確認する
7. サイドバー「📊 マート更新」→「📊 ユニットエコノミクス再描画」から `buildUnitEconomicsSheet()` が実行できることを確認する
8. 「財務3表の更新」(`buildBudgetTrendDataMart()`)実行後に `94_metrics_uniteconomics` も更新されていることを確認する

### 拡張思考の使用状況

| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| Phase 1(設計・調査) | あり | ファイル番号・関数名・シートレイアウト・計算ロジック・エッジケース網羅を確定 |
| Phase 2(実装) | なし | Phase 1 確定内容の書き下しに徹する |

推奨実行モデル

工程推奨モデル理由
仕様書作成(本ドキュメント)Claude Opus 4.6会計ロジック(LTV/CAC の計算方法選択・CAC対象費用の範囲確定)・エッジケース網羅・ドメイン判断が必要
Step 1: Constants.CAC_ACCOUNTS とメニュー追加Claude Haiku 4.5配列・オブジェクト追記のみ。判断要素なし
Step 2: buildUnitEconomicsSheet() 実装Claude Sonnet 4.6複数ファイル横断の設計理解・Map/Set による月次集計ロジック構築が必要。既存マートパターン(609_datamart_kpi.js)の踏襲が中心のため Opus 不要
Step 3: オーケストレーション追記Claude Haiku 4.51 行追記のみ
Step 4: CLAUDE.md 更新Claude Haiku 4.5DDL管理外リストへの追記のみ

変更履歴

日付変更内容
2026-04-21初版作成(MAS-029 ユニットエコノミクス LTV/CAC 仕様書)

仕様書作成プロンプト(再現性・監査性のため必ず記録)

展開して表示
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**:
   - Phase 1(設計フェーズ)では拡張思考をフル活用する。ファイル名形式・エッジケース一覧・Step 分割粒度・固有名詞(関数名/シート名/メニュー名/行番号)を **ここで完全に確定** させる。Read によるコード裏取りを妥協しない(失敗パターン #18-#20 の直接対策)。
   - Phase 2(清書フェーズ)の各 Step 内では拡張思考を **最小限** に抑える。Phase 1 で決定済みの内容をそのまま書き出すことに徹する。出力途中で再考しない。途中の再考はストリーム idle timeout の直接原因。
2. **テキストでの状況報告の禁止**: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
3. **4-5 分割の Write/Edit 実行**: 仕様書作成は Step 2-1〜2-4 に分割。1 回の Write/Edit は約 300 行以内。
4. **各 Step で何を書くかを具体指示**: 設計判断を Phase 2 実行時に持ち込まない。Phase 1 で確定した内容の書き下しに徹する。

======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 F-29「ユニットエコノミクス(LTV/CAC)」の開発仕様書を作成してください。
作成後は `docs/_config.json` の `nav` 配列 §E.5 FP&A・レポーティング セクションに必ず登録すること。

**Grep は「どこにあるか」の発見まで。「どう書くか」の判断は必ず Read で行うこと。名前や記憶から推測した瞬間に手を止めて Read する(失敗パターン #18-#20)。**

---

## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)

Phase 2 着手前に以下を全て Read/Grep で確認し、設計判断を確定させること。

### 1-A: 案件要件の把握
- `docs/_internal/TODO_future.md` の F-29 行を検索し、案件名・概要・人間が検討すべき事項を取得する。

### 1-B: データ構造の確認(Read 必須)
- `000_infra/003_contracts.js` — InvoiceDTO の全フィールド名を確認。特に `収支区分`・`発生日(P/L計上日)`・`取引先名`・`科目名`・`税抜金額_計画`・`有効フラグ`・`請求ステータス`・`諸表区分`・`大分類` の実在と型を確認する。
- `000_infra/002_constants.js` — `Constants` オブジェクトの末尾構造を確認し、`CAC_ACCOUNTS` 配列の追記位置(`CC_MERCHANT_MAP` の直後)を特定する。
- `000_infra/004_utils.js` — `parseDateToYm`・`generateLogicalAbbr`・`normalizePartnerName` の引数・戻り値を確認する(`normalizePartnerName` は MST_PART 照合付きの上位版として実装済み)。
- `200_data/202_repository.js` — `InvoiceRepository.findAll()` と `AccountRepository.findAsMap()` の戻り値型(`{ headers, dtos }` の構造)を確認する。

### 1-C: 既存データマートパターンの把握(Read 必須・推測禁止)
- `600_report/` ディレクトリ内のファイル一覧を確認し、**オーケストレーション関数名を Read で実在確認** する(`buildAllDatamarts()` という名前は未検証。Grep で実際の関数名を発見してから Read で構造を確認し、確定した名前のみを仕様書に使用すること)。
- `600_report/` の既存ビルダーファイルを 1 本 Read し、シート構築パターン(ヘッダー行の書き方・値のセット方式・シートのクリア方法)を把握する。
- 新規ファイル番号を確定する: `600_report/` 内の最大番号(`608` が上限か `609` 以降が存在するか)を確認し、追加する番号(`609` または `610`)を決定する。

### 1-D: メニュー名と DDL 管理の確認(Read 必須・失敗パターン #20 の再発防止)
- `100_config/101_sys_config.js` の `onOpen()` を Read し、データマート更新系メニュー項目の **正確な文字列(絵文字含む)** を確認する。確認した実在文字列のみを仕様書に記載すること(造語禁止)。
- 同ファイルで `94_*` シートの DDL 管理状況を確認し、新規シート `94_metrics_uniteconomics` が動的生成(DDL管理外)として扱われるべきか確認する(CLAUDE.md の「DDL で管理されないタブ」では `93_kpi_dashboard` が同列に掲載されており、同パターンが想定される)。

### 1-E: 類似仕様書のフォーマット確認
- `docs/dev/dev_mas-024_bep_analysis.md` を Read し、エッジケーステーブル・実装プロンプトのフォーマットを把握する。

---

## Phase 2: 仕様書の分割作成

出力先: `docs/dev/dev_mas-029_unit_economics.md`

**絶対に 1 回のツール呼び出しで全内容を出力しない。以下の Step に厳密に分割して実行すること。**

| Step | 内容 | ツール | 目安行数 |
|:---:|---|---|:---:|
| 2-1 | 骨格(見出しのみ) | Write | ~20 |
| 2-2 | 概要・目的・現在のコード・修正方針・影響範囲・注意事項 | Edit または Bash heredoc | ~300 |
| 2-3a | エッジケース・実データ検証・関連ドキュメント・人間が検討すべき事項 | Edit または Bash | ~200 |
| 2-3b | 実装プロンプト・推奨実行モデル・変更履歴 | Edit または Bash | ~250 |
| 2-4 | `<details>` に仕様書作成プロンプト全文を記録 | Edit または Bash | プロンプトサイズ依存 |

---

### Step 2-1: 骨格の作成(Write、~20 行)

`docs/_internal/dev_spec_prompt_template.md` の必須セクション構成に従い、以下の見出しのみの骨格を Write で作成する:

`# F-29: ユニットエコノミクス(LTV/CAC)` / `## 概要` / `## 目的` / `## 現在のコード` / `## 修正方針` / `## 影響範囲` / `## 注意事項` / `## エッジケース` / `## 実データ検証` / `## 関連ドキュメント` / `## 人間が検討すべき事項` / `## 実装プロンプト(Claude Code 用)` / `## 推奨実行モデル` / `## 変更履歴` / `## 仕様書作成プロンプト`

---

### Step 2-2: 前半セクションの追記(Edit または Bash heredoc、~300 行)

**Phase 1 で確認した実在する関数名・シート名・メニュー名のみを使用すること。未確認の名前は「要調査」と記載する。**

以下の内容を追記する:

**## 概要**(テーブル形式): 案件ID=F-29、カテゴリ=FP&A/KPIレポーティング、対象ファイル=(Phase 1 で確定したファイル名を列挙)、前提案件=(TODO_future.md に記載があれば転記、なければ「なし」)

**## 目的**: 月次でユニットエコノミクス(LTV・CAC・LTV/CAC 比率・Payback Period)を自動集計し、`94_metrics_uniteconomics` シートに出力する。顧客単位の収益性把握と投資回収管理を目的とする。

**## 現在のコード**: 当該分析シート・ビルダー関数ともに未存在。新規作成案件。

**## 修正方針**(以下のアーキテクチャ設計を記載):

- **新規ファイル**: `600_report/6NN_datamart_uniteconomics.js`(NN は Phase 1 で確定した番号)
- **新規シート**: `94_metrics_uniteconomics`(`93_kpi_dashboard` と同様の DDL管理外・動的生成シート。CLAUDE.md の「DDL で管理されないタブ」リストに追記が必要)。行 = 指標ラベル(LTV, CAC, LTV/CAC 比率, Payback Period, 新規顧客数, アクティブ顧客数, チャーン顧客数, チャーンレート, 月次粗利, CAC対象費用合計)、列 = 年月(YYYY-MM)、月列は C〜N 列(12列固定)
- **エントリポイント**: Phase 1 で確認した実在のオーケストレーション関数に `buildUnitEconomicsSheet()` の呼び出しを追加。Phase 1 で確認した実在メニューから実行可能にする。
- **データソース**: `InvoiceRepository.findAll()` で全 `InvoiceDTO` を取得。`有効フラグ === true` かつ `請求ステータス === '承認済'` のレコードのみを対象とする。
- **月次集計**: `発生日(P/L計上日)` を `Utils.parseDateToYm()` で正規化して集計キーとする(`Date` 型・文字列型の混在に対応済み)。
- **顧客名正規化**: `Utils.normalizePartnerName()`(MST_PART 照合)を第一選択とし、MST_PART 未登録の場合は `Utils.generateLogicalAbbr()`(法人格除去のみ)にフォールバックする。
- **新規顧客管理**: `Map<正規化顧客名, 初回売上年月>` をインメモリ構築。全期間の InvoiceDTO を日付昇順でスキャンして初回月を記録する(Date オブジェクトのソートは `Utils.parseDateToYm()` で正規化してから行う。失敗パターン #17)。
- **粗利計算**: `AccountRepository.findAsMap()` で `大分類` マップを取得し、売上系科目と原価系科目を判別する。`月次粗利 = 収入側の売上系科目の税抜金額_計画合計 - 支出側の原価系科目の税抜金額_計画合計`(`大分類` の実在値は Phase 1 の MCP 確認結果を使用すること)。
- **CAC 対象費用**: `000_infra/002_constants.js` の `Constants` オブジェクト末尾(`CC_MERCHANT_MAP` の直後)に `CAC_ACCOUNTS` 配列を新設し、対象科目名を定義する(暫定リスト: `'広告宣伝費', '販売促進費', '採用エージェント費'`。ビジネスサイドレビュー必須)。
- **計算式(全指標を明示的に定義)**:
  - `CAC(月) = Σ CAC対象費用の税抜金額_計画 / 当月新規顧客数`(新規顧客数=0 → `'N/A'`)
  - `ARPU_粗利(月) = 月次粗利 / 当月アクティブ顧客数`(アクティブ顧客数=0 → `'N/A'`)
  - `チャーンレート(月) = 当月チャーン顧客数 / 前月末アクティブ顧客数`(前月データなし または 前月末アクティブ顧客数=0 → `'N/A'`)
  - `LTV(月) = ARPU_粗利 / チャーンレート`(チャーンレート=0 → `'>60ヶ月'`、いずれかが `'N/A'` → `'N/A'`)
  - `Payback Period(月) = CAC / ARPU_粗利`(ARPU_粗利=0 または `'N/A'` → `'N/A'`)
- **顧客定義**:
  - アクティブ顧客: 当月に `収支区分='収入'` の InvoiceDTO が存在するユニーク取引先(正規化名)
  - 新規顧客: `Map` で初回売上年月が当月と一致する取引先
  - チャーン顧客: 前月アクティブセットに含まれ、かつ当月アクティブセットに含まれない取引先

**## 影響範囲**:
- 新規ファイル追加: `600_report/6NN_datamart_uniteconomics.js`
- `000_infra/002_constants.js` への `CAC_ACCOUNTS` 追記(既存コード変更なし・末尾追記のみ)
- `100_config/101_sys_config.js` のオーケストレーション関数への 1 行追記(Phase 1 で確認した箇所)
- CLAUDE.md の「DDL で管理されないタブ」リストへの `94_metrics_uniteconomics` 追記(編集前に `git pull origin main`)

**## 注意事項**:
1. `CAC_ACCOUNTS` の科目リストはビジネスサイドのレビューを必須とする。暫定リストで動作確認後、正式確定すること。
2. 顧客名の正規化精度は `12_mst_partner` の登録状況に依存する。MST_PART 未登録の取引先は `generateLogicalAbbr()` の法人格除去のみ適用される(過剰な名寄せは行わない)。
3. 月列は C〜N 列(12列)固定。`getLastColumn()` による動的取得は禁止(YoY差異列等の書式列を含む可能性がある。失敗パターン #21)。
4. 年月のセル書き込みは `setValue("'YYYY-MM")` または `setNumberFormat('@')` を使用し、Sheets の自動数値解釈(`2026-03` → `2023` に変換される問題)を防ぐこと(失敗パターン #23)。
5. `InvoiceDTO['発生日(P/L計上日)']` は `Date` 型と文字列型の混在があり得る。必ず `Utils.parseDateToYm()` 経由で正規化する。
6. 科目マスタ (`11_mst_account`) に未登録の科目名は集計対象からスキップする(エラーにしない)。

---

### Step 2-3a: エッジケース〜人間検討事項の追記(Edit または Bash、~200 行)

**## エッジケース**(`| 条件 | 表示値 | 理由 |` テーブル形式で以下を網羅):

| 条件 | 表示値 | 理由 |
|------|--------|------|
| 当月新規顧客数 = 0 | CAC = `'N/A'` | ゼロ除算 |
| 前月末アクティブ顧客数 = 0(全期間最初の月を含む) | チャーンレート = `'N/A'` | ゼロ除算 / 前月データなし |
| 月次チャーンレート = 0(全顧客継続) | LTV = `'>60ヶ月'` | 無限大に対する上限表示(実務上の上限を60ヶ月と設定) |
| 当月アクティブ顧客数 = 0 | ARPU_粗利 = `'N/A'` | ゼロ除算 |
| ARPU_粗利 = 0 または `'N/A'` | Payback Period = `'N/A'` | ゼロ除算または前段計算不能 |
| 前段指標のいずれかが `'N/A'` | 派生指標(LTV, Payback Period)も `'N/A'` | 伝播ルール |
| 月次粗利 < 0(逆ざや) | 計算値をそのまま表示(負値) | 異常値だが人間の判断に委ねる。フィルタリングしない |
| CAC対象科目の当月費用 = 0(新規顧客あり) | CAC = `0` | 費用ゼロとして正常計算 |

**## 実データ検証**(実装前に MCP 等で以下を確認すること):
1. `11_mst_account` の `大分類` 列の実際の値(売上系・原価系の正確な表記を確認。DDLコード値 vs 実データの乖離チェック)
2. `12_mst_partner` に主要取引先が登録されているか確認(`normalizePartnerName()` の照合精度に直結)
3. `CAC_ACCOUNTS` 暫定リストの科目(`広告宣伝費`, `販売促進費`, `採用エージェント費`)が `11_mst_account` に実在するか確認
4. テスト対象月に `収支区分='収入'` かつ `請求ステータス='承認済'` の InvoiceDTO が存在するか確認

**## 関連ドキュメント**(テーブル形式で Phase 1 で参照したファイルを列挙)

**## 人間が検討すべき事項**:
TODO_future.md の F-29 記載内容を転記した上で、以下を追記する:
- LTV 計算は簡易な「ARPU_粗利 / チャーンレート 方式」を採用。コホート分析ベースの正確な LTV(将来キャッシュフロー割引現在価値)は将来拡張として TODO_future.md に追記する。
- `Constants.CAC_ACCOUNTS`(科目リスト)はビジネスサイドのレビューが必須。暫定リストでの運用開始後、四半期ごとに見直す。
- 現実装は `InvoiceDTO['税抜金額_計画']` を使用しているが、実績確定後は `42_trn_journal` の `税抜金額_実績` ベースへの切り替えを検討する(予算ベース vs 実績ベースの選択)。
- 請求ステータス `'承認済'` のみを対象としているが、`'未処理'`(草稿中)を含めるかをビジネスサイドで確認する。

---

### Step 2-3b: 実装プロンプト〜変更履歴の追記(Edit または Bash、~250 行)

(以下、task_F-29.md 本文の Step 2-3b の指示内容を厳密に転記し、行頭 4 スペースインデントで実装プロンプト本体を記述。推奨実行モデル・変更履歴テーブルを続けて追記する)

---

### Step 2-4: 仕様書作成プロンプトの記録(Edit または Bash)

仕様書末尾に `<details><summary>展開して表示</summary>` ブロックを設け、この `<instruction>` タグの全文をそのまま記録する。

---

## Phase 3: docs/_config.json への登録

`docs/_config.json` の `nav` 配列 §E.5 FP&A・レポーティング セクションの末尾(F-28 の直後)に以下を追加する:

    { "file": "dev/dev_mas-029_unit_economics.md", "title": "E.5.23 F-29 ユニットエコノミクス(LTV/CAC)" }

編集前に `git pull origin main` を実行し、最新状態に同期すること。