Sales CRM 集約: 営業パイプライン / Aggregate: Sales Pipeline
バウンデッドコンテキスト: Sales Pipeline (営業パイプライン)
→ 知識ベース:
knowledge/06-crm-fundamentals/crm-strategy.md(CRM 理論) → 知識ベース:knowledge/07-sales-management/sales-process.md(営業プロセス)
集約ルート (Aggregate Roots)
1. Account (取引先)
Account (集約ルート)
├── coordinates: Point (地理座標)
├── dealCount: number (計算: 関連 Deal 数)
├── activityCount: number (計算: 関連 Activity 数)
├── lastActivityAt: Date (計算: 最終活動日)
├── nextActivityAt: Date (計算: 次回予定)
├── → Contact[] (別集約への参照)
├── → Deal[] (別集約への参照)
├── → Activity[] (別集約への参照)
└── → File[] (別集約への参照)
| プロパティ | 型 | 必須 | 説明 |
|---|---|---|---|
| name | string | ✓ | 取引先名 (インデックス付き) |
| website | string | WEB サイト URL | |
| phone | string | 代表電話番号 | |
| postalCode | string | 郵便番号 (XXX-XXXX) | |
| address | string | 住所テキスト | |
| coordinates | Point | 地図表示用の緯度経度 | |
| group | string | StaffGroups.slug への参照 | |
| salesRep | → User | 営業担当者 | |
| dealCount | number | 関連案件数 (計算値、読取専用) | |
| activityCount | number | 関連活動件数 (計算値、読取専用) | |
| lastActivityAt | Date | 最終活動日 (計算値、読取専用) | |
| nextActivityAt | Date | 次回活動予定日 (計算値、読取専用) | |
| createdBy | → User | 作成者 (読取専用) | |
| updatedBy | → User | 最終更新者 (読取専用) | |
| version | number | 楽観ロック用バージョン番号 |
不変条件 (Invariants):
nameは空文字不可 (必須フィールド)versionは更新ごとに単調増加- 楽観ロック:
incomingVersion !== currentVersionで 409 Conflict dealCount,activityCount,lastActivityAt,nextActivityAtはシステム計算値 (ユーザー変更不可)
ライフサイクル:
Account には明示的なステートマシンはない。作成後、更新・削除が可能。 活動指標は関連 Activity / Deal の変化に応じて自動計算される。
[*] → 作成 → 運用中 (更新可) → 削除
2. Contact (担当者)
Contact (集約ルート)
├── nameKana: string (カナ名、ソート用)
├── account: → Account (必須参照)
├── → Activity[] (別集約への参照)
├── → Deal[] (別集約への参照)
└── → File[] (別集約への参照)
| プロパティ | 型 | 必須 | 説明 |
|---|---|---|---|
| name | string | ✓ | 担当者名 (インデックス付き) |
| nameKana | string | カタカナ表記 (ソート用) | |
| title | string | 部署・役職 | |
email | メールアドレス | ||
| phone | string | オフィス電話 | |
| mobile | string | 携帯番号 | |
| account | → Account | ✓ | 所属取引先 (インデックス付き) |
| group | string | StaffGroups.slug | |
| salesRep | → User | 営業担当者 | |
| createdBy | → User | 作成者 (読取専用) | |
| updatedBy | → User | 最終更新者 (読取専用) | |
| version | number | 楽観ロック用 |
不変条件 (Invariants):
accountは必須。担当者は必ず 1 つの取引先に所属するnameは空文字不可- 楽観ロック: バージョン不整合で 409 Conflict
3. Deal (案件)
Deal (集約ルート)
├── dealType: DealType (商談 / 引合 / リード)
├── status: DealStatus (進行中 / 成約 / 失注)
├── amount: number (金額)
├── probability: number (確度 0-100%)
├── account: → Account (必須参照)
├── contact: → Contact (任意参照)
├── → Activity[] (別集約への参照)
├── → File[] (別集約への参照)
├── → DailyReport.dealSummaries[] (日報での参照)
└── productionTracking (PMS 連携: 仮想フィールド)
| プロパティ | 型 | 必須 | 説明 |
|---|---|---|---|
| name | string | ✓ | 案件名 (インデックス付き) |
| dealType | DealType | ✓ | negotiation / inquiry / lead |
| status | DealStatus | ✓ | open / won / lost (デフォルト: open) |
| amount | number | 金額 (円、0 以上) | |
| probability | number | 確度 (0-100) | |
| expectedCloseDate | Date | 成約予定日 | |
| account | → Account | ✓ | 関連取引先 (インデックス付き) |
| contact | → Contact | 主担当者 (任意) | |
| commentCount | number | コメント数 (計算、読取専用) | |
| group | string | StaffGroups.slug | |
| salesRep | → User | 営業担当者 | |
| createdBy | → User | 作成者 (読取専用) | |
| updatedBy | → User | 最終更新者 (読取専用) | |
| version | number | 楽観ロック用 |
不変条件 (Invariants):
accountは必須。案件は必ず取引先に紐付くdealTypeは DEAL_TYPE_VALUES の値のみstatusは DEAL_STATUS_VALUES の値のみprobabilityは 0-100 の範囲 (Zod バリデーション)amountは 0 以上 (nonnegative)- 楽観ロック: バージョン不整合で 409 Conflict
ライフサイクル (State Machine):
注意: 現在の実装では
open→won/lostの遷移に制約はない。 ステータスは自由に変更可能 (won → open への戻しも可能)。 将来的に、成約後の PMS 連携が発生した場合は遷移制限が必要になる可能性がある。
4. Comment (案件コメント)
Comment (エンティティ — Deal の子)
├── deal: → Deal (親案件、必須)
├── author: → User (投稿者、必須)
├── body: string (コメント本文、必須)
├── createdAt: Date (投稿日時、自動設定)
└── updatedAt: Date (更新日時、自動設定)
| プロパティ | 型 | 必須 | 説明 |
|---|---|---|---|
| deal | → Deal | ✓ | 親案件への参照 |
| author | → User | ✓ | コメント投稿者 (自動設定: req.user) |
| body | string | ✓ | コメント本文 (最大 2000 文字) |
| createdAt | Date | 投稿日時 (自動設定) | |
| updatedAt | Date | 更新日時 (自動設定) |
不変条件 (Invariants):
dealは必須。コメントは必ず案件に紐付くauthorは必須。投稿者は自動的に req.user から設定bodyは空文字不可。最大 2000 文字- コメント作成時に Deal.commentCount をインクリメント
- コメント削除時に Deal.commentCount をデクリメント
- 削除は投稿者本人または admin のみ
設計判断:
- Deal の
commentCountフィールドは読取専用計算値 - Comment は独立コレクションまたは Deal 内の配列として実装可能
- コメントスレッド (返信) は初期スコープ外
トランザクション境界
| 操作 | 同一トランザクション | 結果整合性 (Eventual) |
|---|---|---|
| Account 作成 | Account のみ | — |
| Contact 作成 | Contact のみ | Account の contactCount 更新 |
| Deal 作成 | Deal のみ | Account の dealCount 更新 |
| Deal 成約 (won) | Deal ステータス更新 | PMS への CreateOrderRequest 送信 |
| Deal 失注 (lost) | Deal ステータス更新 | — |
| Comment 作成 | Comment 作成 + Deal.commentCount++ | — |
| Comment 削除 | Comment 削除 + Deal.commentCount-- | — |
関連図
┌──────────────┐
│ Account │
│ (取引先) │
└──────┬───────┘
│ 1
┌─────┼──────┐
│ │ │
N│ N│ N│
┌──────┴─┐ ┌┴──────┴─┐
│Contact │ │ Deal │
│(担当者)│ │ (案件) │
└────────┘ └────┬────┘
│ │ 1
│ ┌─────┼──────┐
│ │ │
▼ ▼ N│
┌──────────────┐ ┌────┴─────┐
│ Activity │ │ Comment │
│ (活動) │ │(コメント)│
└──────────────┘ └──────────┘
- Account : Contact = 1 : N
- Account : Deal = 1 : N
- Account : Activity = 1 : N
- Deal : Activity = 1 : N (任意)
- Deal : Comment = 1 : N
- Contact : Activity = 1 : N (任意)
- Deal : Contact = N : 1 (任意)
重複検出 / Duplicate Detection
→ ベンチマーク:
benchmark_translead/設定/見積書設定0.html(重複チェック画面) TransLead は 1 日 1 回自動チェック + 手動統合 UI を提供。
DuplicateCandidate (重複候補)
重複検出バッチジョブの結果を表すエンティティ。永続化して手動確認待ちとする。
DuplicateCandidate (エンティティ)
├── entityType: string (account / contact)
├── sourceId: string (元レコード ID)
├── targetId: string (重複候補レコード ID)
├── sourceName: string (元レコード名 — 表示用)
├── targetName: string (候補レコード名 — 表示用)
├── similarityScore: number (類似度スコア 0-100)
├── status: DuplicateStatus (pending / merged / dismissed)
├── mergedAt: Date (統合日時)
├── mergedBy: → User (統合実行者)
├── detectedAt: Date (検出日時)
└── lastCheckedAt: Date (最終チェック日時)
| プロパティ | 型 | 必須 | 説明 |
|---|---|---|---|
| entityType | string | ✓ | account / contact — 対象エンティティ種別 |
| sourceId | string | ✓ | 元レコード ID |
| targetId | string | ✓ | 重複候補レコード ID |
| sourceName | string | ✓ | 元レコード名 (非正規化、表示用) |
| targetName | string | ✓ | 候補レコード名 (非正規化、表示用) |
| similarityScore | number | ✓ | 類似度スコア (0-100、高いほど類似) |
| status | DuplicateStatus | ✓ | pending / merged / dismissed (デフォルト: pending) |
| mergedAt | Date | 統合実行日時 | |
| mergedBy | → User | 統合実行者 | |
| detectedAt | Date | ✓ | 最初に検出された日時 |
| lastCheckedAt | Date | ✓ | 最終チェック実行日時 |
不変条件 (Invariants):
sourceId !== targetId— 自己参照禁止sourceId + targetIdの組み合わせはユニーク (順序問わず)similarityScoreは 0-100 の範囲status=mergedの場合、mergedAtとmergedByは必須- 統合操作は admin / manager のみ
検出ロジック (バッチジョブ):
1. 対象コレクション (accounts / contacts) の全レコードを取得
2. 名前フィールドの類似度を計算 (レーベンシュタイン距離 or トリグラム)
3. 閾値 (例: 70%) 以上のペアを DuplicateCandidate として保存
4. 既存の pending/dismissed 候補は再検出しない
統合操作:
1. source レコードを master として選択
2. target レコードの関連データ (Deals, Contacts, Activities, Files) を source に移行
3. target レコードを削除
4. DuplicateCandidate.status → merged
設計判断: 自動統合は行わない。全ての統合は手動確認後に実行。 TransLead の実装に準拠 (「取引先を統合する」/ 「統合せずに確定」の 2 択)。
ライフサイクル (State Machine):