diff --git a/src/app/Console/Commands/Tenant/CreateCommand.php b/src/app/Console/Commands/Tenant/CreateCommand.php index 80fb43ab..547b601e 100644 --- a/src/app/Console/Commands/Tenant/CreateCommand.php +++ b/src/app/Console/Commands/Tenant/CreateCommand.php @@ -1,166 +1,192 @@ getUser($this->argument('user')); + $email = $this->argument('user'); - if (!$user) { - $this->error('User not found.'); + if ($user = \App\User::where('email', $email)->first()) { + $this->error("The user already exists."); return 1; } + if ($domain = \App\Domain::where('namespace', $this->argument('domain'))->first()) { + $this->error("The domain already exists."); + return 1; + } + + DB::beginTransaction(); // Create a tenant - $tenant = \App\Tenant::create(['title' => $this->option('title') ?: $user->name()]); + $tenant = \App\Tenant::create(['title' => $this->option('title')]); // Clone plans, packages, skus for the tenant $sku_map = \App\Sku::withEnvTenantContext()->where('active', true)->get() ->mapWithKeys(function ($sku) use ($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(); return [$sku->id => $sku_new->id]; }) ->all(); $plan_map = \App\Plan::withEnvTenantContext()->get() ->mapWithKeys(function ($plan) use ($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(); return [$plan->id => $plan_new->id]; }) ->all(); $package_map = \App\Package::withEnvTenantContext()->get() ->mapWithKeys(function ($package) use ($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(); return [$package->id => $package_new->id]; }) ->all(); 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}."); + + // Set up the primary tenant domain + $domain = \App\Domain::create( + [ + 'namespace' => $this->argument('domain'), + 'type' => \App\Domain::TYPE_PUBLIC, + ] + ); + $domain->tenant_id = $tenant->id; + $domain->status = \App\Domain::STATUS_CONFIRMED | \App\Domain::STATUS_ACTIVE; + $domain->save(); + $this->info("Created domain {$domain->id}."); + + $user = new \App\User(); + $user->email = $email; + $user->password = $this->option('password'); + $user->role = 'reseller'; + $user->tenant_id = $tenant->id; + + if ($error = UsersController::validateEmail($email, $user)) { + $this->error("{$email}: {$error}"); + return 1; + } + + $user->save(); + $this->info("Created user {$user->id}."); + + $tenant->setSettings([ + "app.name" => $this->option("title"), + "app.url" => $this->argument("domain"), + "app.public_url" => "https://" . $this->argument("domain"), + "app.support_url" => "https://" . $this->argument("domain") . "/support", + "mail.sender.address" => "noreply@" . $this->argument("domain"), + "mail.sender.name" => $this->option("title"), + "mail.replyto.address" => "noreply@" . $this->argument("domain"), + "mail.replyto.name" => $this->option("title"), + ]); + + DB::commit(); + + $this->info("Applied default tenant settings."); } } diff --git a/src/tests/Feature/Console/Tenant/CreateTest.php b/src/tests/Feature/Console/Tenant/CreateTest.php index 6ff9619f..d9131f74 100644 --- a/src/tests/Feature/Console/Tenant/CreateTest.php +++ b/src/tests/Feature/Console/Tenant/CreateTest.php @@ -1,98 +1,100 @@ deleteTestUser('test-tenant@kolabnow.com'); + $this->deleteTestUser('unknown@user.com'); + $this->deleteTestDomain('tenant.com'); + $this->deleteTestDomain('user.com'); + \App\Tenant::where('title', 'Test Tenant')->delete(); } /** * {@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\Domain::where('tenant_id', $this->tenantId)->delete(); \App\Tenant::find($this->tenantId)->delete(); } + $this->deleteTestUser('unknown@user.com'); 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"); + // User already existing + $user = $this->getTestUser('unknown@user.com'); + $code = \Artisan::call("tenant:create {$user->email} user.com --title=\"Test Tenant\""); $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')); - - // Existing user - $code = \Artisan::call("tenant:create {$user->email} --title=\"Test Tenant\""); + // User not existing + $code = \Artisan::call("tenant:create test-tenant@tenant.com tenant.com --title=\"Test Tenant\""); $output = trim(\Artisan::output()); $this->assertSame(0, $code); $this->assertMatchesRegularExpression("/^Created tenant [0-9]+./", $output); - preg_match("/^Created tenant ([0-9]+)./", $output, $matches); + 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); + + preg_match("/Created user ([0-9]+)./", $output, $matches); + $userId = $matches[1]; + $user = \App\User::find($userId); + $this->assertNotEmpty($user); $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 } }