Compare commits
No commits in common. "feat/artisan-provision-command" and "dev" have entirely different histories.
feat/artis
...
dev
2 changed files with 0 additions and 283 deletions
1
Boot.php
1
Boot.php
|
|
@ -185,7 +185,6 @@ class Boot extends ServiceProvider
|
|||
$event->command(Console\Commands\ProcessAccountDeletions::class);
|
||||
$event->command(Console\Commands\CheckUsageAlerts::class);
|
||||
$event->command(Console\Commands\ResetBillingCycles::class);
|
||||
$event->command(Console\Commands\ProvisionPackage::class);
|
||||
|
||||
// Security migration commands
|
||||
$event->command(Console\Commands\EncryptTwoFactorSecrets::class);
|
||||
|
|
|
|||
|
|
@ -1,282 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Tenant\Console\Commands;
|
||||
|
||||
use Core\Tenant\Models\EntitlementLog;
|
||||
use Core\Tenant\Models\Package;
|
||||
use Core\Tenant\Models\Workspace;
|
||||
use Core\Tenant\Services\EntitlementService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
|
||||
/**
|
||||
* Manually provision a package for a workspace.
|
||||
*
|
||||
* Replaces the need to use Tinker for manual package provisioning,
|
||||
* providing proper validation, confirmation, and output.
|
||||
*/
|
||||
class ProvisionPackage extends Command
|
||||
{
|
||||
protected $signature = 'tenant:provision-package
|
||||
{workspace : The workspace ID or slug}
|
||||
{package : The package code to provision}
|
||||
{--source=admin : Provisioning source (admin, system, blesta, commerce)}
|
||||
{--expires-at= : Optional expiry date (Y-m-d or Y-m-d H:i:s)}
|
||||
{--billing-anchor= : Billing cycle anchor date (Y-m-d, default: now)}
|
||||
{--blesta-service-id= : Optional Blesta service ID}
|
||||
{--dry-run : Show what would happen without making changes}
|
||||
{--force : Skip confirmation prompt}';
|
||||
|
||||
protected $description = 'Provision a package for a workspace, granting access to all features in that package';
|
||||
|
||||
public function __construct(
|
||||
protected EntitlementService $entitlementService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$workspaceIdentifier = $this->argument('workspace');
|
||||
$packageCode = $this->argument('package');
|
||||
$dryRun = $this->option('dry-run');
|
||||
|
||||
// Resolve workspace
|
||||
$workspace = is_numeric($workspaceIdentifier)
|
||||
? Workspace::find($workspaceIdentifier)
|
||||
: Workspace::where('slug', $workspaceIdentifier)->first();
|
||||
|
||||
if (! $workspace) {
|
||||
$this->error("Workspace not found: {$workspaceIdentifier}");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
// Resolve package
|
||||
$package = Package::where('code', $packageCode)->first();
|
||||
|
||||
if (! $package) {
|
||||
$this->error("Package not found: {$packageCode}");
|
||||
$this->newLine();
|
||||
$this->showAvailablePackages();
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if (! $package->is_active) {
|
||||
$this->warn("Package '{$package->name}' ({$package->code}) is not active.");
|
||||
|
||||
if (! $this->option('force') && ! $this->confirm('Do you want to provision it anyway?')) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse options
|
||||
$options = $this->buildOptions();
|
||||
|
||||
if ($options === null) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
// Show what will happen
|
||||
$this->displayProvisioningSummary($workspace, $package, $options);
|
||||
|
||||
// Check for existing base package conflict
|
||||
if ($package->is_base_package) {
|
||||
$existingBase = $workspace->workspacePackages()
|
||||
->whereHas('package', fn ($q) => $q->where('is_base_package', true))
|
||||
->active()
|
||||
->first();
|
||||
|
||||
if ($existingBase) {
|
||||
$this->warn("Workspace already has an active base package: {$existingBase->package->name} ({$existingBase->package->code})");
|
||||
$this->line('The existing base package will be cancelled and replaced.');
|
||||
$this->newLine();
|
||||
}
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info('[DRY RUN] No changes were made.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
// Confirm unless --force
|
||||
if (! $this->option('force') && ! $this->confirm('Proceed with provisioning?')) {
|
||||
$this->info('Cancelled.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
// Provision the package
|
||||
try {
|
||||
$workspacePackage = $this->entitlementService->provisionPackage(
|
||||
$workspace,
|
||||
$packageCode,
|
||||
$options
|
||||
);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
$this->error("Failed to provision package: {$e->getMessage()}");
|
||||
|
||||
return self::FAILURE;
|
||||
} catch (\Exception $e) {
|
||||
$this->error("Provisioning failed: {$e->getMessage()}");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
// Output result
|
||||
$this->newLine();
|
||||
$this->info('Package provisioned successfully.');
|
||||
$this->newLine();
|
||||
|
||||
$this->table(
|
||||
['Field', 'Value'],
|
||||
[
|
||||
['Workspace', "{$workspace->name} ({$workspace->slug})"],
|
||||
['Package', "{$package->name} ({$package->code})"],
|
||||
['Workspace Package ID', $workspacePackage->id],
|
||||
['Status', $workspacePackage->status],
|
||||
['Starts At', $workspacePackage->starts_at?->toDateTimeString() ?? 'Now'],
|
||||
['Expires At', $workspacePackage->expires_at?->toDateTimeString() ?? 'Never'],
|
||||
['Billing Anchor', $workspacePackage->billing_cycle_anchor?->toDateTimeString() ?? '-'],
|
||||
['Source', $options['source'] ?? EntitlementLog::SOURCE_ADMIN],
|
||||
]
|
||||
);
|
||||
|
||||
// Show features included
|
||||
$features = $package->features;
|
||||
|
||||
if ($features->isNotEmpty()) {
|
||||
$this->newLine();
|
||||
$this->info('Features included:');
|
||||
|
||||
$this->table(
|
||||
['Feature', 'Code', 'Type', 'Limit'],
|
||||
$features->map(fn ($f) => [
|
||||
$f->name,
|
||||
$f->code,
|
||||
$f->type,
|
||||
$f->pivot->limit_value ?? ($f->type === 'unlimited' ? 'Unlimited' : '-'),
|
||||
])->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build provisioning options from command arguments.
|
||||
*/
|
||||
protected function buildOptions(): ?array
|
||||
{
|
||||
$options = [
|
||||
'source' => $this->option('source') ?? EntitlementLog::SOURCE_ADMIN,
|
||||
];
|
||||
|
||||
// Validate source
|
||||
$validSources = [
|
||||
EntitlementLog::SOURCE_ADMIN,
|
||||
EntitlementLog::SOURCE_SYSTEM,
|
||||
EntitlementLog::SOURCE_BLESTA,
|
||||
EntitlementLog::SOURCE_COMMERCE,
|
||||
EntitlementLog::SOURCE_API,
|
||||
];
|
||||
|
||||
if (! in_array($options['source'], $validSources, true)) {
|
||||
$this->error("Invalid source: {$options['source']}");
|
||||
$this->line('Valid sources: '.implode(', ', $validSources));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($expiresAt = $this->option('expires-at')) {
|
||||
try {
|
||||
$options['expires_at'] = new \DateTimeImmutable($expiresAt);
|
||||
} catch (\Exception) {
|
||||
$this->error("Invalid expiry date: {$expiresAt}");
|
||||
$this->line('Expected format: Y-m-d or Y-m-d H:i:s');
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($billingAnchor = $this->option('billing-anchor')) {
|
||||
try {
|
||||
$options['billing_cycle_anchor'] = new \DateTimeImmutable($billingAnchor);
|
||||
} catch (\Exception) {
|
||||
$this->error("Invalid billing anchor date: {$billingAnchor}");
|
||||
$this->line('Expected format: Y-m-d');
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($blestaServiceId = $this->option('blesta-service-id')) {
|
||||
$options['blesta_service_id'] = $blestaServiceId;
|
||||
}
|
||||
|
||||
$options['metadata'] = [
|
||||
'provisioned_via' => 'artisan',
|
||||
'provisioned_at' => now()->toIso8601String(),
|
||||
];
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a summary of the provisioning action.
|
||||
*/
|
||||
protected function displayProvisioningSummary(Workspace $workspace, Package $package, array $options): void
|
||||
{
|
||||
$this->info('Provisioning Summary');
|
||||
$this->newLine();
|
||||
|
||||
$rows = [
|
||||
['Workspace', "{$workspace->name} (ID: {$workspace->id}, slug: {$workspace->slug})"],
|
||||
['Package', "{$package->name} ({$package->code})"],
|
||||
['Package Type', $package->is_base_package ? 'Base package' : 'Add-on package'],
|
||||
['Source', $options['source']],
|
||||
['Expires At', isset($options['expires_at']) ? $options['expires_at']->format('Y-m-d H:i:s') : 'Never'],
|
||||
];
|
||||
|
||||
if (isset($options['billing_cycle_anchor'])) {
|
||||
$rows[] = ['Billing Anchor', $options['billing_cycle_anchor']->format('Y-m-d H:i:s')];
|
||||
}
|
||||
|
||||
if (isset($options['blesta_service_id'])) {
|
||||
$rows[] = ['Blesta Service ID', $options['blesta_service_id']];
|
||||
}
|
||||
|
||||
$this->table(['Field', 'Value'], $rows);
|
||||
$this->newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show available packages when an invalid code is given.
|
||||
*/
|
||||
protected function showAvailablePackages(): void
|
||||
{
|
||||
$packages = Package::active()->ordered()->get();
|
||||
|
||||
if ($packages->isEmpty()) {
|
||||
$this->line('No active packages found.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info('Available packages:');
|
||||
|
||||
$this->table(
|
||||
['Code', 'Name', 'Type', 'Active'],
|
||||
$packages->map(fn (Package $p) => [
|
||||
$p->code,
|
||||
$p->name,
|
||||
$p->is_base_package ? 'Base' : 'Add-on',
|
||||
$p->is_active ? 'Yes' : 'No',
|
||||
])->toArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue