# Pack Authoring Guide

> How to add a 6th (or nth) domain profile without touching engine code.

---

## The three things that NEVER need a migration

1. **New display-only custom field** → add to pack JSON `custom_fields[]`
2. **New pipeline stage** → add to pack JSON `pipelines[].stages[]`
3. **New label rebranding** → add to pack JSON `entity_domain_maps[]`

Pack → `FeaturePackSeeder` → DB row. Zero PHP changes.

---

## The one thing that requires a migration (R6)

A new **filterable/sortable** custom field (e.g. `territory` for a pharma v2 profile).

- Add a migration: `$table->string('territory')->nullable()->index();` on `leads`
- In `LeadFilter`: `public function territory(string $v): self { return $this->where('territory', $v); }`
- In pack JSON: `"is_searchable": true, "real_column": "territory"` — documents the column
- `HasCustomFields::setCustomField()` must write both `custom_field_values` AND `leads.territory`

Display-only fields (no filter/sort) are unaffected — they go in `custom_field_values` only.

---

## Step-by-step: add a new profile

```
1. Create AdminApp/data/crm/feature-packs/{slug}_pack.json
2. Author: pipelines, custom_fields, workflow_templates, entity_domain_maps (see JSON keys below)
3. Create AdminApp/config/product-profiles/taskco-crm-{slug}.php
4. php artisan setup:crm --profile=taskco-crm-{slug} --tenant=xxx --dry-run
5. Fix validation errors → run without --dry-run
6. Verify: php artisan setup:crm --profile=taskco-crm-{slug} --tenant=xxx (second run = no duplicates)
```

---

## JSON keys reference

| Key | Type | Seeded into |
|-----|------|-------------|
| `pipelines` | array | `pipelines` + `pipeline_stages` |
| `custom_fields` | array | `custom_fields` |
| `workflow_templates` | array | `workflow_rules` |
| `entity_domain_maps` | array | `entity_domain_maps` |

### Minimal `custom_fields` entry
```json
{
  "slug": "university",
  "label": "University",
  "field_type": "text",
  "entity_type": "lead",
  "group_name": "Education Background",
  "sort_order": 1,
  "is_required": false,
  "is_searchable": false,
  "is_system": true
}
```

### Minimal `workflow_templates` entry (round-robin assign on lead.created)
```json
{
  "slug": "edu-auto-assign-counselor",
  "name": "Auto-assign Counselor on Lead Created",
  "trigger_event": "lead.created",
  "entity_type": "lead",
  "conditions": [],
  "actions": [
    { "type": "assign_to_user", "config": { "role": "counselor", "strategy": "round_robin" } }
  ],
  "is_active": true,
  "is_system": true
}
```

### Minimal `entity_domain_maps` entry
```json
{ "entity_type": "lead",    "label": "Application", "label_plural": "Applications" },
{ "entity_type": "deal",    "label": "Enrollment",  "label_plural": "Enrollments"  },
{ "entity_type": "contact", "label": "Student",     "label_plural": "Students"     }
```

---

## Product profile PHP file

```php
// AdminApp/config/product-profiles/taskco-crm-{slug}.php
return [
    'name'         => 'Taskco CRM — {Name}',
    'feature_pack' => '{slug}_pack',     // matches filename without .json
    'apps'         => ['CrmApp'],
    'modules'      => ['Lead', 'Deal', 'Pipeline', 'Contact', 'Activity'],
];
```

---

## Idempotency guarantee

`FeaturePackSeeder` uses `upsert()` / `updateOrCreate()` keyed on `slug`. Re-running never:
- Deletes user-created pipelines, stages, or custom fields
- Overwrites `custom_field_values` (user data)
- Creates duplicate rows

Running `setup:crm` twice on a live tenant is safe.
