diff --git a/src/app/Console/Commands/Domain/CreateCommand.php b/src/app/Console/Commands/Domain/CreateCommand.php --- a/src/app/Console/Commands/Domain/CreateCommand.php +++ b/src/app/Console/Commands/Domain/CreateCommand.php @@ -3,7 +3,6 @@ namespace App\Console\Commands\Domain; use App\Console\Command; -use Illuminate\Support\Facades\Queue; class CreateCommand extends Command { diff --git a/src/app/Console/Commands/Tenant/CreateCommand.php b/src/app/Console/Commands/Tenant/CreateCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/Tenant/CreateCommand.php @@ -0,0 +1,166 @@ +getUser($this->argument('user')); + + if (!$user) { + $this->error('User not found.'); + return 1; + } + + DB::beginTransaction(); + + // Create a tenant + $tenant = \App\Tenant::create(['title' => $this->option('title') ?: $user->name()]); + + // Clone plans, packages, skus for the tenant + $sku_map = []; + \App\Sku::withEnvTenant()->where('active', true)->get() + ->each(function ($sku) use ($sku_map, $tenant) { + $sku_new = \App\Sku::create([ + 'title' => $sku->title, + 'name' => $sku->getTranslations('name'), + 'description' => $sku->getTranslations('description'), + 'cost' => $sku->cost, + 'units_free' => $sku->units_free, + 'period' => $sku->period, + 'handler_class' => $sku->handler_class, + 'active' => true, + 'fee' => $sku->fee, + ]); + + $sku_new->tenant_id = $tenant->id; + $sku_new->save(); + + $sku_map[$sku->id] = $sku_new->id; + }); + + $plan_map = []; + \App\Plan::withEnvTenant()->get() + ->each(function ($plan) use ($plan_map, $tenant) { + $plan_new = \App\Plan::create([ + 'title' => $plan->title, + 'name' => $plan->getTranslations('name'), + 'description' => $plan->getTranslations('description'), + 'promo_from' => $plan->promo_from, + 'promo_to' => $plan->promo_to, + 'qty_min' => $plan->qty_min, + 'qty_max' => $plan->qty_max, + 'discount_qty' => $plan->discount_qty, + 'discount_rate' => $plan->discount_rate, + ]); + + $plan_new->tenant_id = $tenant->id; + $plan_new->save(); + + $plan_map[$plan->id] = $plan_new->id; + }); + + $package_map = []; + \App\Package::withEnvTenant()->get() + ->each(function ($package) use ($package_map, $tenant) { + $package_new = \App\Package::create([ + 'title' => $package->title, + 'name' => $package->getTranslations('name'), + 'description' => $package->getTranslations('description'), + 'discount_rate' => $package->discount_rate, + ]); + + $package_new->tenant_id = $tenant->id; + $package_new->save(); + + $package_map[$package->id] = $package_new->id; + }); + + DB::table('package_skus')->whereIn('package_id', array_keys($package_map))->get() + ->each(function ($item) use ($package_map, $sku_map) { + if (isset($sku_map[$item->sku_id])) { + DB::table('package_skus')->insert([ + 'qty' => $item->qty, + 'cost' => $item->cost, + 'sku_id' => $sku_map[$item->sku_id], + 'package_id' => $package_map[$item->package_id], + ]); + } + }); + + DB::table('plan_packages')->whereIn('plan_id', array_keys($plan_map))->get() + ->each(function ($item) use ($package_map, $plan_map) { + if (isset($package_map[$item->package_id])) { + DB::table('plan_packages')->insert([ + 'qty' => $item->qty, + 'qty_min' => $item->qty_min, + 'qty_max' => $item->qty_max, + 'discount_qty' => $item->discount_qty, + 'discount_rate' => $item->discount_rate, + 'plan_id' => $plan_map[$item->plan_id], + 'package_id' => $package_map[$item->package_id], + ]); + } + }); + + // Disable jobs, they would fail anyway as the TENANT_ID is different + // TODO: We could probably do config(['app.tenant' => $tenant->id]) here + Queue::fake(); + + // Assign 'reseller' role to the user + $user->role = 'reseller'; + $user->tenant_id = $tenant->id; + $user->save(); + + // Switch tenant_id for all of the user belongings + $user->wallets->each(function ($wallet) use ($tenant) { + $wallet->entitlements->each(function ($entitlement) use ($tenant) { + $entitlement->entitleable->tenant_id = $tenant->id; + $entitlement->entitleable->save(); + + // TODO: If user already has any entitlements, they will have to be + // removed/replaced by SKUs in the newly created tenant + // I think we don't really support this yet anyway. + }); + + // TODO: If the wallet has a discount we should remove/replace it too + // I think we don't really support this yet anyway. + }); + + DB::commit(); + + // Make sure the transaction wasn't aborted + $tenant = \App\Tenant::find($tenant->id); + + if (!$tenant) { + $this->error("Failed to create a tenant."); + return 1; + } + + $this->info("Created tenant {$tenant->id}."); + } +} diff --git a/src/tests/Feature/Console/Tenant/CreateTest.php b/src/tests/Feature/Console/Tenant/CreateTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/Tenant/CreateTest.php @@ -0,0 +1,98 @@ +deleteTestUser('test-tenant@kolabnow.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + if ($this->tenantId) { + Queue::fake(); + + \App\User::where('tenant_id', $this->tenantId)->forceDelete(); + \App\Plan::where('tenant_id', $this->tenantId)->delete(); + \App\Package::where('tenant_id', $this->tenantId)->delete(); + \App\Sku::where('tenant_id', $this->tenantId)->delete(); + \App\Tenant::find($this->tenantId)->delete(); + } + + parent::tearDown(); + } + + /** + * Test command runs + */ + public function testHandle(): void + { + Queue::fake(); + + // Warning: We're not using artisan() here, as this will not + // allow us to test "empty output" cases + + // User not existing + $code = \Artisan::call("tenant:create unknown@user.com"); + $output = trim(\Artisan::output()); + $this->assertSame(1, $code); + $this->assertSame("User not found.", $output); + + $user = $this->getTestUser('test-tenant@kolabnow.com'); + + $this->assertEmpty($user->role); + $this->assertEquals($user->tenant_id, \config('app.tenant_id')); + + // User not existing + $code = \Artisan::call("tenant:create {$user->email} --title=\"Test Tenant\""); + $output = trim(\Artisan::output()); + $this->assertSame(0, $code); + $this->assertRegExp("/^Created tenant [0-9]+./", $output); + + preg_match("/^Created tenant ([0-9]+)./", $output, $matches); + $this->tenantId = $matches[1]; + + $tenant = \App\Tenant::find($this->tenantId); + $user->refresh(); + + $this->assertNotEmpty($tenant); + $this->assertSame('Test Tenant', $tenant->title); + $this->assertSame('reseller', $user->role); + $this->assertSame($tenant->id, $user->tenant_id); + + // Assert cloned SKUs + $skus = \App\Sku::where('tenant_id', \config('app.tenant_id'))->where('active', true); + + $skus->each(function ($sku) use ($tenant) { + $sku_new = \App\Sku::where('tenant_id', $tenant->id) + ->where('title', $sku->title)->get(); + + $this->assertSame(1, $sku_new->count()); + $sku_new = $sku_new->first(); + $this->assertSame($sku->name, $sku_new->name); + $this->assertSame($sku->description, $sku_new->description); + $this->assertSame($sku->cost, $sku_new->cost); + $this->assertSame($sku->units_free, $sku_new->units_free); + $this->assertSame($sku->period, $sku_new->period); + $this->assertSame($sku->handler_class, $sku_new->handler_class); + $this->assertNotEmpty($sku_new->active); + }); + + // TODO: Plans, packages + } +}