Sales CRM 集約: 認証・権限・企業設定 / Aggregate: IAM & Configuration
バウンデッドコンテキスト: IAM (認証・権限) + Configuration (設定)
→ 知識ベース:
knowledge/10-people-operations/sales-team-management.md
集約ルート (Aggregate Roots)
1. User (ユーザー)
User (集約ルート — 認証エンティティ)
├── username: string (一意識別子)
├── full_name: string (表示名)
├── email: string (Payload Auth)
├── role: UserRole (admin/manager/sales_rep/executive)
├── status: enum (active/inactive)
├── group: string (StaffGroups.slug)
├── must_change_password: bool (初回パスワード変更フラグ)
├── password_changed_at: Date (最終パスワード変更日時)
└── last_login: Date (最終ログイン日時)
| プロパティ | 型 | 必須 | 説明 |
|---|---|---|---|
| username | string | ✓ | 一意、[a-z0-9_]+ パターン |
| full_name | string | ✓ | ユーザーの表示名 |
string | ✓ | Payload 認証用 (Auth collection) | |
| role | UserRole | ✓ | admin / manager / sales_rep / executive |
| status | select | ✓ | active / inactive (デフォルト: active) |
| group | string | StaffGroups.slug | |
| notes | string | 管理者用メモ | |
| must_change_password | boolean | 初回ログイン時パスワード変更強制 | |
| password_changed_at | Date | 最終パスワード変更日時 (読取専用) | |
| last_login | Date | 最終ログイン日時 (読取専用) | |
| createdBy | → User | 作成者 (読取専用) | |
| updatedBy | → User | 最終更新者 (読取専用) |
不変条件 (Invariants):
usernameはシステム全体で一意 (beforeChangeで重複チェック)usernameは小文字英数字 + アンダースコアのみ (/^[a-z0-9_]+$/)- アカウントロック: 3 回連続ログイン失敗で 15 分間ロック
- 新規ユーザーは
must_change_password = trueで作成 - トークン有効期限: 2 時間
アクセス制御:
| 操作 | admin | manager / executive | sales_rep |
|---|---|---|---|
| 読取 | 全ユーザー | 全ユーザー | 自分のみ |
| 作成 | ✓ | ✗ | ✗ |
| 更新 | 全ユーザー | ✗ | 自分のみ |
| 削除 | ✓ | ✗ | ✗ |
2. StaffGroup (部署グループ)
StaffGroup (集約ルート)
├── name: string (表示名)
├── slug: string (一意識別子)
└── sortOrder: number (表示順)
| プロパティ | 型 | 必須 | 説明 |
|---|---|---|---|
| name | string | ✓ | グループ表示名 (例: 営業本部) |
| slug | string | ✓ | 一意識別子 (例: eigyo-honbu)、インデックス付き |
| sortOrder | number | 表示順序 (デフォルト: 0、小さい値が先) |
不変条件 (Invariants):
slugはシステム全体で一意- admin のみ作成・更新・削除可能
- 読取は全ユーザーに公開
参照元: User.group, Account.group, Contact.group, Deal.group, Activity.group, File.group → 全エンティティが StaffGroups.slug を参照してグループ所属を管理
3. CompanySettings (企業設定)
CompanySettings (集約ルート — シングルトン)
├── company_name: string (企業名、必須)
├── postal_code: string (XXX-XXXX)
├── prefecture: string (都道府県)
├── city: string (市区町村)
├── street_address: string (番地・建物名)
├── logo_url: string (ロゴ画像 URL)
├── business_hours_start: string (HH:MM)
├── business_hours_end: string (HH:MM)
└── fiscal_year_start_month: number (1-12)
| プロパティ | 型 | 必須 | 説明 |
|---|---|---|---|
| company_name | string | ✓ | 企業名 (最大 100 文字) |
| postal_code | string | 郵便番号 (/^\d{3}-\d{4}$/) | |
| prefecture | string | 都道府県 | |
| city | string | 市区町村 | |
| street_address | string | 番地・建物名 | |
| logo_url | string | ロゴ画像 URL (読取専用) | |
| logo_file_key | string | ロゴファイルストレージキー (読取専用) | |
| business_hours_start | string | 営業開始時刻 (`/^([01]\d | |
| business_hours_end | string | 営業終了時刻 (同上) | |
| fiscal_year_start_month | number | 会計年度開始月 (1-12) | |
| createdBy | → User | 作成者 (読取専用) | |
| updatedBy | → User | 最終更新者 (読取専用) | |
| version | number | 楽観ロック用 |
不変条件 (Invariants):
- シングルトンパターン: レコードは 1 件のみ。2 件目の作成は
beforeChangeで禁止 postal_codeはXXX-XXXX形式のみbusiness_hours_start<business_hours_end(営業開始は終了より前)business_hours_*はHH:MM形式 (00:00~23:59)fiscal_year_start_monthは 1-12 の整数- admin のみ作成・更新・削除可能
- 読取は全ユーザーに公開
- 楽観ロック: バージョン不整合で 409 Conflict
ロールベースアクセス制御 (RBAC) モデル
┌───────────────────────────────────────────────────────┐
│ User Roles │
├───────────┬───────────┬─────────────┬────────────────┤
│ admin │ manager │ sales_rep │ executive │
├───────────┼───────────┼─────────────┼────────────────┤
│ 全権限 │ 全レポート│ 自分の担当 │ 全レポート │
│ │ 閲覧+編集 │ 分のみ │ 閲覧のみ │
│ ユーザー │ │ │ │
│ 管理 │ 日報確認 │ 日報提出 │ 日報閲覧 │
│ │ │ │ │
│ 設定変更 │ │ │ │
└───────────┴───────────┴─────────────┴────────────────┘
- admin: 全操作可能。ユーザー管理、企業設定、カスタムフィールド管理
- manager: 全データ閲覧可。日報レビュー。配下メンバーの活動確認
- sales_rep: 自分が担当する取引先・案件・活動・日報のみ操作可能
- executive: 経営層。全データ閲覧可。編集権限なし (日報閲覧含む)
RBAC データアクセスマトリクス (per-collection × per-role)
各コレクションの CRUD 操作をロール別に定義する。
all= 全レコード、own= salesRep/user が自分のレコードのみ、—= 不可。 スコーピング条件:salesRep: { equals: user.id }またはuser: { equals: user.id }
営業データコレクション
| コレクション | 操作 | admin | manager | sales_rep | executive |
|---|---|---|---|---|---|
| accounts | read | all | all | own (salesRep) | all |
| create | ✓ | ✓ | ✓ | — | |
| update | all | all | own (salesRep) | — | |
| delete | ✓ | — | — | — | |
| contacts | read | all | all | own (salesRep) | all |
| create | ✓ | ✓ | ✓ | — | |
| update | all | all | own (salesRep) | — | |
| delete | ✓ | — | — | — | |
| deals | read | all | all | own (salesRep) | all |
| create | ✓ | ✓ | ✓ | — | |
| update | all | all | own (salesRep) | — | |
| delete | ✓ | — | — | — | |
| activities | read | all | all | own (salesRep) | all |
| create | ✓ | ✓ | ✓ | — | |
| update | all | all | own (salesRep) | — | |
| delete | ✓ | — | — | — | |
| daily-reports | read | all | all | own (user) | all |
| create | ✓ | ✓ | ✓ | — | |
| update | all | all | own (user) | — | |
| delete | ✓ | — | — | — | |
| files | read | all | all | own (salesRep) | all |
| create | ✓ | ✓ | ✓ | — | |
| update | all | all | own (salesRep) | — | |
| delete | ✓ | — | — | — |
管理コレクション
| コレクション | 操作 | admin | manager | sales_rep | executive |
|---|---|---|---|---|---|
| users | read | all | all | own (id) | all |
| create | ✓ | — | — | — | |
| update | all | own (id) | own (id) | — | |
| delete | ✓ | — | — | — | |
| staff_groups | read | all | all | all | all |
| create | ✓ | — | — | — | |
| update | ✓ | — | — | — | |
| delete | ✓ | — | — | — | |
| company_settings | read | all | all | all | all |
| create | ✓ | — | — | — | |
| update | ✓ | — | — | — | |
| delete | ✓ | — | — | — | |
| custom_field_configs | read | ✓ | — | — | — |
| create | ✓ | — | — | — | |
| update | ✓ | — | — | — | |
| delete | ✓ | — | — | — |
スコーピングパターン
営業データコレクション (accounts, contacts, deals, activities, files) の sales_rep スコーピングは以下の共通パターンに従う:
// access.read for sales_rep
({ req: { user } }) => {
if (user.role === 'sales_rep') {
return { salesRep: { equals: user.id } };
}
return true; // admin, manager, executive → all
}
参考:
daily-reportsはuserフィールドでこのパターンを使用。 営業データ用にはsalesRepフィールドでの同等実装が必要。