Fab Forward Dev/

DDD ドキュメント

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[]               (別集約への参照)
プロパティ必須説明
namestring取引先名 (インデックス付き)
websitestringWEB サイト URL
phonestring代表電話番号
postalCodestring郵便番号 (XXX-XXXX)
addressstring住所テキスト
coordinatesPoint地図表示用の緯度経度
groupstringStaffGroups.slug への参照
salesRep→ User営業担当者
dealCountnumber関連案件数 (計算値、読取専用)
activityCountnumber関連活動件数 (計算値、読取専用)
lastActivityAtDate最終活動日 (計算値、読取専用)
nextActivityAtDate次回活動予定日 (計算値、読取専用)
createdBy→ User作成者 (読取専用)
updatedBy→ User最終更新者 (読取専用)
versionnumber楽観ロック用バージョン番号

不変条件 (Invariants):

  • name は空文字不可 (必須フィールド)
  • version は更新ごとに単調増加
  • 楽観ロック: incomingVersion !== currentVersion で 409 Conflict
  • dealCount, activityCount, lastActivityAt, nextActivityAt はシステム計算値 (ユーザー変更不可)

ライフサイクル:

Account には明示的なステートマシンはない。作成後、更新・削除が可能。 活動指標は関連 Activity / Deal の変化に応じて自動計算される。

[*] → 作成 → 運用中 (更新可) → 削除

2. Contact (担当者)

Contact (集約ルート)
├── nameKana: string       (カナ名、ソート用)
├── account: → Account     (必須参照)
├── → Activity[]           (別集約への参照)
├── → Deal[]               (別集約への参照)
└── → File[]               (別集約への参照)
プロパティ必須説明
namestring担当者名 (インデックス付き)
nameKanastringカタカナ表記 (ソート用)
titlestring部署・役職
emailemailメールアドレス
phonestringオフィス電話
mobilestring携帯番号
account→ Account所属取引先 (インデックス付き)
groupstringStaffGroups.slug
salesRep→ User営業担当者
createdBy→ User作成者 (読取専用)
updatedBy→ User最終更新者 (読取専用)
versionnumber楽観ロック用

不変条件 (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 連携: 仮想フィールド)
プロパティ必須説明
namestring案件名 (インデックス付き)
dealTypeDealTypenegotiation / inquiry / lead
statusDealStatusopen / won / lost (デフォルト: open)
amountnumber金額 (円、0 以上)
probabilitynumber確度 (0-100)
expectedCloseDateDate成約予定日
account→ Account関連取引先 (インデックス付き)
contact→ Contact主担当者 (任意)
commentCountnumberコメント数 (計算、読取専用)
groupstringStaffGroups.slug
salesRep→ User営業担当者
createdBy→ User作成者 (読取専用)
updatedBy→ User最終更新者 (読取専用)
versionnumber楽観ロック用

不変条件 (Invariants):

  • account は必須。案件は必ず取引先に紐付く
  • dealType は DEAL_TYPE_VALUES の値のみ
  • status は DEAL_STATUS_VALUES の値のみ
  • probability は 0-100 の範囲 (Zod バリデーション)
  • amount は 0 以上 (nonnegative)
  • 楽観ロック: バージョン不整合で 409 Conflict

ライフサイクル (State Machine):

注意: 現在の実装では openwon / lost の遷移に制約はない。 ステータスは自由に変更可能 (won → open への戻しも可能)。 将来的に、成約後の PMS 連携が発生した場合は遷移制限が必要になる可能性がある。


4. Comment (案件コメント)

Comment (エンティティ — Deal の子)
├── deal: → Deal              (親案件、必須)
├── author: → User            (投稿者、必須)
├── body: string              (コメント本文、必須)
├── createdAt: Date           (投稿日時、自動設定)
└── updatedAt: Date           (更新日時、自動設定)
プロパティ必須説明
deal→ Deal親案件への参照
author→ Userコメント投稿者 (自動設定: req.user)
bodystringコメント本文 (最大 2000 文字)
createdAtDate投稿日時 (自動設定)
updatedAtDate更新日時 (自動設定)

不変条件 (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         (最終チェック日時)
プロパティ必須説明
entityTypestringaccount / contact — 対象エンティティ種別
sourceIdstring元レコード ID
targetIdstring重複候補レコード ID
sourceNamestring元レコード名 (非正規化、表示用)
targetNamestring候補レコード名 (非正規化、表示用)
similarityScorenumber類似度スコア (0-100、高いほど類似)
statusDuplicateStatuspending / merged / dismissed (デフォルト: pending)
mergedAtDate統合実行日時
mergedBy→ User統合実行者
detectedAtDate最初に検出された日時
lastCheckedAtDate最終チェック実行日時

不変条件 (Invariants):

  • sourceId !== targetId — 自己参照禁止
  • sourceId + targetId の組み合わせはユニーク (順序問わず)
  • similarityScore は 0-100 の範囲
  • status=merged の場合、mergedAtmergedBy は必須
  • 統合操作は 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):