最終更新: 2026/06/22 18:56
MAS-029: ユニットエコノミクス(LTV/CAC)
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-029 |
| カテゴリ | FP&A / KPIレポーティング(BizDev) |
| Phase | P2 |
| 優先度 | ★★ |
| 所要時間 | 3〜4時間 |
| 実装ステータス | 📝 仕様書段階・実装未着手 (2026-04-28 監査時点) |
| 対象ファイル | 600_report/610_datamart_uniteconomics.js(新規作成)000_infra/002_constants.js(CAC_ACCOUNTS 追記・MENU_DEFINITION 追記)600_report/602_datamart_main.js(buildBudgetTrendDataMart() 末尾に呼び出し追記)CLAUDE.md(DDL管理外タブリストに 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.js600_report/既存ファイルは601_datamart_ingest.js〜609_datamart_kpi.jsまで存在。次番号 610 で採番する。
- 新規シート:
94_metrics_uniteconomics93_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.jsのbuildBudgetTrendDataMart()の L391 付近(buildKpiDashboard()呼び出しの直後)にtry { buildUnitEconomicsSheet(); } catch (ueErr) { Utils.logError('buildBudgetTrendDataMart.uniteconomics', ueErr); }を追記する。失敗してもマート更新全体は成功扱いとする(MAS-003 と同等の Resilient 実装)。- メニューから単独実行可能にするため、
000_infra/002_constants.jsのMENU_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.jsのConstantsオブジェクト末尾、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 行)
| 行 | ラベル | 備考 |
|---|---|---|
| 1 | LTV | 円/社 |
| 2 | CAC | 円/社 |
| 3 | LTV/CAC 比率 | 倍 |
| 4 | Payback Period | 月 |
| 5 | 新規顧客数 | 社 |
| 6 | アクティブ顧客数 | 社 |
| 7 | チャーン顧客数 | 社 |
| 8 | チャーンレート | % |
| 9 | 月次粗利 | 円 |
| 10 | CAC対象費用合計 | 円 |
対象月の決定
- 既定では直近 12ヶ月(当月含む)を出力対象とする。C列=最古月、N列=最新月。
- 年月ヘッダーは 2 行目。1 行目はタイトル
📊 ユニットエコノミクス(LTV/CAC)。指標ラベルは A 列、単位は B 列、月次値は C〜N 列。 - 年月セルの書き込みは
setNumberFormat('@')+setValue("'" + ym)の apostrophe 前置方式で行う(Sheets 自動数値解釈で2026-03→2023変換される失敗パターン #23 を防止)。
影響範囲
- 新規ファイル追加:
600_report/610_datamart_uniteconomics.js(約 250〜300 行想定) - 既存ファイル末尾追記のみ:
000_infra/002_constants.js—Constants.CAC_ACCOUNTSプロパティ追加(L195 直後に 1 行)、MENU_DEFINITIONの「📊 マート更新」カテゴリに 1 項目追加600_report/602_datamart_main.js—buildBudgetTrendDataMart()の L391 直後にtry { buildUnitEconomicsSheet(); } catch (...) { ... }を 1 行追加
- ドキュメント更新:
CLAUDE.mdL191-192 の「DDL で管理されないタブ」リストに94_metrics_uniteconomicsを追記(編集前にgit pull origin main必須)
- 既存動作への影響: なし(末尾追記のみ・既存ロジック変更なし)
注意事項
Constants.CAC_ACCOUNTSの科目リストはビジネスサイドのレビューを必須とする。暫定リストで動作確認後、正式確定すること。- 顧客名の正規化精度は
12_mst_partnerの登録状況に依存する。MST_PART 未登録の取引先はgenerateLogicalAbbr()の法人格除去のみ適用される(過剰な名寄せは行わない)。 - 月列は C〜N 列(12列)固定。
getLastColumn()による動的取得は禁止(YoY差異列等の書式列を含む可能性。失敗パターン #21)。 - 年月のセル書き込みは
setNumberFormat('@')+setValue("'YYYY-MM")の apostrophe 前置方式を使用し、Sheets の自動数値解釈(2026-03→2023に変換される問題)を防ぐこと(失敗パターン #23)。 InvoiceDTO['発生日(P/L計上日)']はDate型と文字列型の混在があり得る。必ずUtils.parseDateToYm()経由で正規化すること。- 科目マスタ (
11_mst_account) に未登録の科目名は集計対象からスキップする(エラーにしない・ログ出力のみ)。 94_metrics_uniteconomicsは DDL 管理外。setupAllSchemasで初期化されないため、buildUnitEconomicsSheet()内でss.getSheetByName(...) || ss.insertSheet(...)パターンでシートを確保し、sheet.clear()でクリアしてから再構築する(93_kpi_dashboardと同じパターン)。- オーケストレーション呼び出し箇所は MAS-003 と同様に
try { ... } catch { Utils.logError }で囲み、失敗時もマート更新全体は成功扱いとする(Resilient 実装)。
エッジケース
以下の全パターンで例外を throw せず、表示値(文字列 or 数値)をシートに書き込むこと。派生指標は伝播ルールに従う。
| 条件 | 対象指標 | 表示値 | 理由 |
|---|---|---|---|
| 当月新規顧客数 = 0 | CAC | 'N/A' | ゼロ除算 |
| CAC対象科目の当月費用 = 0(新規顧客あり) | CAC | 0 | 費用ゼロとして正常計算 |
| 当月アクティブ顧客数 = 0 | ARPU_粗利 | '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 等で以下を確認し、仕様書の暫定値を確定させること。
11_mst_accountの大分類列の実際の値: 売上系・原価系の正確な表記(例:'売上高''売上原価''販管費''営業外収益'等)を確認。DDLコード値 vs 実データの乖離チェック。12_mst_partnerの主要取引先登録状況: プロダクト事業の主要顧客が登録されているか確認(normalizePartnerName()の照合精度に直結)。未登録の場合は暫定的にgenerateLogicalAbbr()ベースで集計し、ビジネスサイドに登録依頼を出す。CAC_ACCOUNTS暫定リストの実在確認:'広告宣伝費''販売促進費''採用エージェント費'が11_mst_accountに実在するか確認。未登録の科目はリストから除外し、存在する類似科目を追加提案する。- テスト対象月のデータ有無: 直近 12ヶ月の各月において、
収支区分='収入'かつ請求ステータス='承認済'の InvoiceDTO が最低 1 件存在する月を特定する。全月ゼロだと LTV/CAC の計算結果検証ができないため。 収支区分のプルダウン値:'収入''支出'の 2 値以外が格納されていないか確認(例外的な値が混在すると集計漏れになる)。
関連ドキュメント
| 仕様書 / 参照先 | 関連箇所 |
|---|---|
| CLAUDE.md | 「DDL で管理されないタブ」リスト(要追記)/「変更時の動作確認テスト」600_report/6*_datamart_*.js → マート更新テスト |
| TODO_future.md | MAS-029 案件定義行/関連案件 MAS-003(KPIダッシュボード)・MAS-028(ARR/MRR トラッキング) |
| dev_mas-024_bep_analysis.md | 類似の月次指標算出パターン(分母ゼロ処理・isActualOnly 非適用シートの例) |
| dev_mas-003_kpi_dashboard.md | DDL管理外シート(93_kpi_dashboard)の動的生成パターン |
| dev_mas-028_arr_mrr_tracking.md | プロダクト事業の顧客単位指標という近接ドメイン |
600_report/609_datamart_kpi.js | 動的シート生成・12ヶ月列固定・年月 apostrophe 書き込みの実装リファレンス |
000_infra/003_contracts.js | InvoiceDTO 型定義 |
200_data/202_repository.js | InvoiceRepository.findAll() / AccountRepository.findAsMap() |
000_infra/004_utils.js | parseDateToYm / 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.5 | 1 行追記のみ |
| Step 4: CLAUDE.md 更新 | Claude Haiku 4.5 | DDL管理外リストへの追記のみ |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 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` を実行し、最新状態に同期すること。