Files
HRM-System/app/Jobs/ProvisionTenantJob.php

208 lines
8.5 KiB
PHP

<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use App\Models\Inquiry;
use App\Models\Tenant;
use App\Models\TenantEntitlement;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
class ProvisionTenantJob implements ShouldQueue
{
use Queueable;
public Inquiry $inquiry;
public function __construct(Inquiry $inquiry)
{
$this->inquiry = $inquiry;
}
public function handle(): void
{
try {
$this->inquiry->update(['status' => 'provisioning']);
$tenantId = Str::slug($this->inquiry->desired_subdomain);
// 1. Create or retrieve Tenant Record (Idempotent)
$tenant = Tenant::find($tenantId);
if (!$tenant) {
$tenant = Tenant::create([
'id' => $tenantId,
'tenancy_db_name' => 'tenant_' . $tenantId,
'company_name' => $this->inquiry->company_name,
'email' => $this->inquiry->email,
]);
}
// 2. Create Subdomain Mapping
$domainHost = config('app.url_domain', parse_url(config('app.url'), PHP_URL_HOST) ?? 'localhost');
$domainName = $tenantId . '.' . $domainHost;
\Stancl\Tenancy\Database\Models\Domain::firstOrCreate(
['domain' => $domainName],
['tenant_id' => $tenant->id]
);
// 3. Store Tenant Entitlements in Central DB
$requestedFeatures = $this->inquiry->requested_features ?? [];
if (empty($requestedFeatures)) {
$requestedFeatures = \App\Constants\ModuleContract::getAllInquiryKeys();
}
TenantEntitlement::updateOrCreate(
['tenant_id' => $tenantId],
[
'max_employees' => (int) ($this->inquiry->employee_count ?? 25),
'enabled_modules' => $requestedFeatures,
'source_inquiry_id' => $this->inquiry->id,
]
);
// 4. Store tenant module configurations in Central DB
foreach (\App\Constants\ModuleContract::MODULE_MAP as $inquiryKey => $config) {
$systemModuleKey = $config['module_key'];
$isEnabled = in_array($inquiryKey, $requestedFeatures);
\App\Models\CentralTenantModule::on('sqlite')->updateOrCreate(
[
'tenant_id' => $tenantId,
'module_key' => $systemModuleKey,
],
[
'enabled' => $isEnabled,
'enabled_at' => $isEnabled ? now() : null,
]
);
}
// 5. Migrate Tenant DB & Seed Initial Setup Permission Matrix
$dbFile = database_path('tenant_' . $tenantId . '.sqlite');
if (app()->environment('testing')) {
if (file_exists($dbFile)) { @unlink($dbFile); }
}
if (!file_exists($dbFile)) { touch($dbFile); }
$tenant->run(function () use ($dbFile, $requestedFeatures) {
config([
'database.connections.tenant.database' => $dbFile,
'database.default' => 'tenant',
]);
\Illuminate\Support\Facades\DB::purge('tenant');
\Illuminate\Support\Facades\DB::reconnect('tenant');
\Illuminate\Support\Facades\DB::setDefaultConnection('tenant');
\Illuminate\Support\Facades\Artisan::call('migrate', [
'--database' => 'tenant',
'--path' => 'database/migrations/tenant',
'--force' => true,
]);
// Seed/update initial admin user with default password 12345678
$user = \App\Models\User::updateOrCreate(
['email' => $this->inquiry->email],
[
'name' => $this->inquiry->contact_name,
'password' => Hash::make('12345678'),
'type' => 'company',
'status' => 'active',
'is_enable_login' => 1,
'email_verified_at' => now(),
]
);
// Ensure company role exists and is assigned
$companyRole = \Spatie\Permission\Models\Role::findOrCreate('company', 'web');
if (method_exists($user, 'assignRole')) {
$user->assignRole($companyRole);
}
// Seed tenant_modules table inside tenant DB
foreach (\App\Constants\ModuleContract::MODULE_MAP as $inquiryKey => $config) {
$systemModuleKey = $config['module_key'];
$isEnabled = in_array($inquiryKey, $requestedFeatures);
\Illuminate\Support\Facades\DB::connection('tenant')->table('tenant_modules')->updateOrInsert(
['module_key' => $systemModuleKey],
[
'enabled' => $isEnabled ? 1 : 0,
'enabled_at' => $isEnabled ? now() : null,
'created_at' => now(),
'updated_at' => now(),
]
);
}
// Store max_employees setting inside tenant DB
\Illuminate\Support\Facades\DB::connection('tenant')->table('settings')->updateOrInsert(
['key' => 'max_employees'],
[
'user_id' => $user->id,
'value' => (string) ($this->inquiry->employee_count ?? 25),
'created_at' => now(),
'updated_at' => now(),
]
);
// Grant permissions for enabled modules
$allowedPermissions = \App\Constants\ModuleContract::getPermissionsForInquiryKeys($requestedFeatures);
if (method_exists($user, 'syncPermissions')) {
foreach ($allowedPermissions as $permissionName) {
\Spatie\Permission\Models\Permission::findOrCreate($permissionName, 'web');
}
$user->syncPermissions($allowedPermissions);
}
});
$centralConn = config('tenancy.database.central_connection', 'sqlite');
\Illuminate\Support\Facades\DB::setDefaultConnection($centralConn);
config(['database.default' => $centralConn]);
\Illuminate\Support\Facades\DB::purge('tenant');
// 6. Generate One-Time Hashed Setup Token (Valid 24 Hours)
$rawToken = Str::random(40);
$tokenHash = hash('sha256', $rawToken);
\App\Models\TenantSetupToken::create([
'tenant_id' => $tenantId,
'email' => $this->inquiry->email,
'token_hash' => $tokenHash,
'expires_at' => now()->addHours(24),
]);
// Construct setup link: https://{subdomain}.domain.com/setup-admin?token={rawToken}
$scheme = parse_url(config('app.url'), PHP_URL_SCHEME) ?? 'http';
$setupUrl = "{$scheme}://{$domainName}/setup-admin?token={$rawToken}";
// 7. Dispatch Email Invitation
try {
Mail::to($this->inquiry->email)->send(new \App\Mail\TenantSetupInvitationMail($this->inquiry->company_name, $setupUrl));
} catch (\Exception $mailEx) {
Log::warning("Could not send setup invitation email to {$this->inquiry->email}: " . $mailEx->getMessage());
}
// 8. Update Inquiry Status to active
$this->inquiry->update([
'status' => 'active',
]);
Log::info("Successfully provisioned tenant [{$tenantId}] & dispatched setup token invitation for inquiry ID: {$this->inquiry->id}");
} catch (\Exception $e) {
Log::error("Failed to provision tenant for inquiry ID: {$this->inquiry->id}. Error: " . $e->getMessage());
$this->inquiry->update([
'status' => 'pending_payment_approval',
'rejection_reason' => 'Provisioning Error: ' . $e->getMessage(),
]);
throw $e;
}
}
}