# setup:crm Command + Product Profiles

> Provisions CRM for one or all tenants. The single entry point after Phase 1 is implemented.

## Prerequisites

| Requirement | Why |
|-------------|-----|
| `CACHE_DRIVER=redis` | `AssignToUser` round-robin stores its cursor in the shared cache. APCu is per-process and breaks on multi-server deployments. |
| Default pipeline seeded | Lead/Deal `create()` calls `HasPipeline::defaultPipelineForType()` — throws `PipelineNotConfiguredException` if no seeded pipeline exists. Run `setup:crm` before creating records. |
| Feature pack JSON valid | `FeaturePackSeeder` validates JSON structure before running. Malformed packs abort with a descriptive error — no partial state. |
| Laravel scheduler enabled | `workflow:tick` runs every 5 minutes via `schedule:run`. Without `* * * * * cd /path && php artisan schedule:run` in your crontab, scheduled workflow rules will not fire. |

---

## Profile Switching Policy — v1

A tenant's profile is set **once** at provisioning via `setup:crm --profile=<slug> --tenant=<id>`.

**Switching a tenant from one profile to another is unsupported in v1.** The `FeaturePackSeeder` is
additive-only — it never deletes seeded rows. So running `setup:crm` with a different profile *adds*
the new profile's pipelines/fields/labels alongside the existing ones, leaving the tenant in a hybrid
state with no path back.

**If a profile change is genuinely required:** delete the tenant's CRM data and re-provision from
scratch. A managed migration tool (deactivate previous profile's `is_system=true` rows, activate new
profile's rows) is Phase 3 work — see `crm_plan/_core/risks.md` Gap H.

**Safe re-runs:** running `setup:crm` with the *same* profile is always safe — it's an idempotent
reconcile, useful after a pack JSON version bump.

---

## Scheduler Registration

The `workflow:tick` command (see `engines/workflow-engine.md`) drives all `trigger_event = 'scheduled'`
rules. Register it in `app/Console/Kernel.php` (project root):

```php
protected function schedule(Schedule $schedule): void
{
    // Existing schedule entries...

    // CRM scheduled workflow rules — fires rules whose cron expression matches the current minute
    $schedule->command('workflow:tick')
        ->everyFiveMinutes()
        ->withoutOverlapping()
        ->onOneServer();   // critical for multi-server deploys to avoid duplicate dispatches
}
```

> **Tenant-aware ticks:** if scheduled rules need to fire per-tenant (likely), wrap the command in
> a tenant loop:
> ```php
> $schedule->call(function () {
>     tenancy()->runForMultiple(\App\Models\Tenant::all(), fn () =>
>         \Artisan::call('workflow:tick')
>     );
> })->everyFiveMinutes()->withoutOverlapping()->onOneServer();
> ```
> Use whichever pattern matches the rest of the project's tenant-bound scheduled jobs.

---

## `setup:crm` Artisan Command

File: `AdminApp/app/Console/Commands/SetupCrmCommand.php`

```php
namespace AdminApp\Console\Commands;

use CoreApp\Database\Seeders\{CrmEntityTypeSeeder, FeaturePackSeeder};
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Stancl\Tenancy\Database\Models\Tenant;

class SetupCrmCommand extends Command
{
    protected $signature = 'setup:crm
        {--profile=taskco-crm-general : Product profile slug}
        {--tenant=         : Single tenant ID (omit = all tenants)}
        {--dry-run         : Print steps without executing}';

    protected $description = 'Provision CRM for one or all tenants (idempotent)';

    public function handle(): int
    {
        $profile  = $this->option('profile');
        $tenantId = $this->option('tenant');
        $dryRun   = $this->option('dry-run');

        $profileConfig = config("product-profiles.{$profile}");
        if (!$profileConfig) {
            $this->error("Unknown profile: {$profile}");
            return 1;
        }

        $packSlug = $profileConfig['feature_pack']
            ?? throw new \RuntimeException("Profile {$profile} missing feature_pack key");

        $tenants = $tenantId
            ? [Tenant::findOrFail($tenantId)]
            : Tenant::all();

        foreach ($tenants as $tenant) {
            $this->info("Tenant: {$tenant->id}");
            if ($dryRun) {
                $this->line("  [dry-run] would run: migrate, CrmEntityTypeSeeder, FeaturePackSeeder({$packSlug})");
                continue;
            }

            try {
                tenancy()->initialize($tenant);

                Artisan::call('migrate', ['--force' => true]);
                $this->line('  ✓ migrations');

                (new CrmEntityTypeSeeder())->run();
                $this->line('  ✓ entity types');

                (new FeaturePackSeeder())->run($packSlug);
                $this->line("  ✓ feature pack: {$packSlug}");

                // Track completion (idempotent — log even on re-run)
                \App\Models\Setting::updateOrCreate(
                    ['key' => 'crm_setup_completed_at'],
                    ['value' => now()->toISOString()]
                );
                \App\Models\Setting::updateOrCreate(
                    ['key' => 'crm_product_profile'],
                    ['value' => $profile]
                );
                $this->info("  ✓ Done");

            } catch (\Throwable $e) {
                $this->error("  ✗ Failed for tenant {$tenant->id}: {$e->getMessage()}");
                report($e);
                // continue — never abort all tenants for one failure
            } finally {
                tenancy()->end();
            }
        }

        return 0;
    }
}
```

Register in `AdminApp/app/Console/Kernel.php` (or `AdminApp/routes/console.php` if using Laravel 11 style):
```php
protected $commands = [
    \AdminApp\Console\Commands\SetupCrmCommand::class,
];
```

---

## Product Profile PHP Files

All in `AdminApp/config/product-profiles/`.

### `taskco-crm-general.php`
```php
return [
    'name'         => 'Taskco CRM — General',
    'apps'         => ['CoreApp', 'CrmApp'],
    'modules'      => ['Lead', 'Deal', 'Pipeline', 'Contact', 'Activity'],
    'features'     => [],
    'feature_pack' => 'default_crm_pack',
    'domain_map'   => [
        'lead'    => ['singular' => 'Lead',    'plural' => 'Leads'],
        'deal'    => ['singular' => 'Deal',    'plural' => 'Deals'],
        'contact' => ['singular' => 'Contact', 'plural' => 'Contacts'],
    ],
];
```

### `taskco-crm-education.php`
```php
return [
    'name'         => 'Taskco CRM — Education',
    'apps'         => ['CoreApp', 'CrmApp'],
    'modules'      => ['Lead', 'Deal', 'Pipeline', 'Contact', 'Activity'],
    'features'     => [],
    'feature_pack' => 'education_pack',
    'domain_map'   => [
        'lead'    => ['singular' => 'Application', 'plural' => 'Applications'],
        'deal'    => ['singular' => 'Enrollment',  'plural' => 'Enrollments'],
        'contact' => ['singular' => 'Student',     'plural' => 'Students'],
    ],
];
```

### `taskco-crm-realstate.php`
```php
return [
    'name'         => 'Taskco CRM — Real Estate',
    'apps'         => ['CoreApp', 'CrmApp'],
    'modules'      => ['Lead', 'Deal', 'Pipeline', 'Contact', 'Activity'],
    'features'     => [],
    'feature_pack' => 'real_estate_pack',
    'domain_map'   => [
        'lead'    => ['singular' => 'Enquiry',       'plural' => 'Enquiries'],
        'deal'    => ['singular' => 'Property Deal', 'plural' => 'Property Deals'],
        'contact' => ['singular' => 'Client',        'plural' => 'Clients'],
    ],
];
```

### `taskco-crm-pharma.php`
```php
return [
    'name'         => 'Taskco CRM — Pharma',
    'apps'         => ['CoreApp', 'CrmApp'],
    'modules'      => ['Lead', 'Deal', 'Pipeline', 'Contact', 'Activity'],
    'features'     => [],
    'feature_pack' => 'pharma_pack',
    'domain_map'   => [
        'lead'    => ['singular' => 'Doctor',  'plural' => 'Doctors'],
        'deal'    => ['singular' => 'Order',   'plural' => 'Orders'],
        'contact' => ['singular' => 'Contact', 'plural' => 'Contacts'],
    ],
];
```

### `taskco-crm-garments.php`
```php
return [
    'name'         => 'Taskco CRM — Garments',
    'apps'         => ['CoreApp', 'CrmApp'],
    'modules'      => ['Lead', 'Deal', 'Pipeline', 'Contact', 'Activity'],
    'features'     => [],
    'feature_pack' => 'garments_pack',
    'domain_map'   => [
        'lead'    => ['singular' => 'Buyer',        'plural' => 'Buyers'],
        'deal'    => ['singular' => 'Order',         'plural' => 'Orders'],
        'contact' => ['singular' => 'Buyer Contact', 'plural' => 'Buyer Contacts'],
    ],
];
```

---

## AppRegistry Wiring

### `AdminApp/database/seeders/FeatureManagement/CoreApp.php` (new file)
```php
namespace AdminApp\Database\Seeders\FeatureManagement;

class CoreApp extends BaseAppSeeder
{
    protected string $name        = 'CoreApp';
    protected string $description = 'Headless CRM engine layer';
    protected bool   $isSystem    = true;  // not shown in tenant UI
}
```

### `AdminApp/database/seeders/FeatureManagement/CrmApp.php` (replace stub)
```php
namespace AdminApp\Database\Seeders\FeatureManagement;

class CrmApp extends BaseAppSeeder
{
    protected string $name        = 'CrmApp';
    protected string $description = 'Customer Relationship Management';
    protected array  $modules     = ['Lead', 'Deal', 'Pipeline', 'Contact', 'Activity', 'Proposal'];
}
```

### `AppRegistry.php` (uncomment + add)
```php
// In AppRegistry::apps() array — uncomment CrmApp and add CoreApp:
CoreApp::class,
CrmApp::class,
```

---

## `ProductProfileSeeder` Hook

When AdminApp applies a product profile to a tenant, call `FeaturePackSeeder` from `ProductProfileSeeder`:
```php
// In AdminApp/database/seeders/ProductProfileSeeder.php run() method:
if (str_starts_with($profile->slug, 'taskco-crm-')) {
    tenancy()->initialize($tenant);
    (new \CoreApp\Database\Seeders\FeaturePackSeeder())->run(
        $profile->feature_pack   // from config/product-profiles/*.php
    );
    tenancy()->end();
}
```
