157 lines
6.0 KiB
PHP
157 lines
6.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Businesses;
|
|
|
|
use App\Enums\Permission;
|
|
use App\Models\BookingRule;
|
|
use App\Models\Business;
|
|
use App\Models\BusinessDomain;
|
|
use App\Models\BusinessHours;
|
|
use App\Models\BusinessSettings;
|
|
use App\Models\Role;
|
|
use App\Models\RolePermission;
|
|
use App\Models\StaffInvitation;
|
|
use App\Models\StaffProfile;
|
|
use App\Models\User;
|
|
use App\Services\Tenant\DomainNormalizer;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
|
|
final class BusinessProvisioningService
|
|
{
|
|
public function __construct(private readonly DomainNormalizer $domains) {}
|
|
|
|
/**
|
|
* Create a complete, isolated tenant and an owner onboarding invitation.
|
|
*
|
|
* The owner is deliberately created without a password. They can only
|
|
* enter the business workspace after completing the one-time invitation.
|
|
*
|
|
* @param array<string, mixed> $businessData
|
|
* @param array<string, mixed> $ownerData
|
|
*/
|
|
public function provision(array $businessData, array $ownerData, ?User $invitedBy = null): BusinessProvisioningResult
|
|
{
|
|
$hostname = $this->domains->normalize((string) $businessData['domain']);
|
|
|
|
return DB::transaction(function () use ($businessData, $ownerData, $hostname, $invitedBy): BusinessProvisioningResult {
|
|
$slug = strtolower(trim((string) $businessData['slug']));
|
|
|
|
if (Business::query()->where('slug', $slug)->exists()) {
|
|
throw new \InvalidArgumentException('That business slug is already in use.');
|
|
}
|
|
|
|
if (BusinessDomain::query()->where('hostname', $hostname)->exists()) {
|
|
throw new \InvalidArgumentException('That hostname is already assigned to another business.');
|
|
}
|
|
|
|
$business = Business::create([
|
|
'slug' => $slug,
|
|
'name' => trim((string) $businessData['name']),
|
|
'shortName' => filled($businessData['shortName'] ?? null) ? trim((string) $businessData['shortName']) : null,
|
|
'description' => filled($businessData['description'] ?? null) ? trim((string) $businessData['description']) : null,
|
|
'phone' => filled($businessData['phone'] ?? null) ? trim((string) $businessData['phone']) : null,
|
|
'email' => strtolower(trim((string) ($businessData['email'] ?? ''))),
|
|
'timezone' => $businessData['timezone'] ?? 'Europe/London',
|
|
'currency' => strtoupper((string) ($businessData['currency'] ?? 'GBP')),
|
|
'locale' => $businessData['locale'] ?? 'en-GB',
|
|
]);
|
|
|
|
BusinessDomain::create([
|
|
'businessId' => $business->id,
|
|
'hostname' => $hostname,
|
|
'isPrimary' => true,
|
|
'isActive' => true,
|
|
]);
|
|
|
|
BusinessSettings::create([
|
|
'businessId' => $business->id,
|
|
'cancellationPolicy' => 'Please give us 24 hours notice if you need to cancel.',
|
|
]);
|
|
|
|
BookingRule::create([
|
|
'businessId' => $business->id,
|
|
'minNoticeMinutes' => 120,
|
|
'maxAdvanceDays' => 60,
|
|
'cancellationWindowHours' => 24,
|
|
'rescheduleWindowHours' => 24,
|
|
'slotIntervalMinutes' => 30,
|
|
'bufferAfterMinutes' => 5,
|
|
'customerCanChooseStaff' => true,
|
|
'allowGuestBooking' => true,
|
|
]);
|
|
|
|
foreach ([
|
|
'MONDAY' => [600, 1140],
|
|
'TUESDAY' => [600, 1140],
|
|
'WEDNESDAY' => [600, 1140],
|
|
'THURSDAY' => [600, 1140],
|
|
'FRIDAY' => [600, 1200],
|
|
'SATURDAY' => [540, 1020],
|
|
'SUNDAY' => [null, null],
|
|
] as $day => [$opens, $closes]) {
|
|
BusinessHours::create([
|
|
'businessId' => $business->id,
|
|
'dayOfWeek' => $day,
|
|
'isClosed' => $opens === null,
|
|
'opensAtMinutes' => $opens,
|
|
'closesAtMinutes' => $closes,
|
|
]);
|
|
}
|
|
|
|
$ownerRole = $this->createRole($business, 'Owner', Permission::cases());
|
|
$barberRole = $this->createRole($business, 'Barber', [
|
|
Permission::ManageOwnAppointments,
|
|
Permission::CheckInCustomers,
|
|
Permission::ManageOwnAvailability,
|
|
Permission::BookOnBehalfOfCustomer,
|
|
Permission::ManageLoyaltyStamps,
|
|
]);
|
|
|
|
$owner = User::create([
|
|
'name' => trim((string) $ownerData['name']),
|
|
'email' => strtolower(trim((string) $ownerData['email'])),
|
|
'passwordHash' => null,
|
|
'isSuperAdmin' => false,
|
|
]);
|
|
|
|
$staff = StaffProfile::create([
|
|
'userId' => $owner->id,
|
|
'businessId' => $business->id,
|
|
'roleId' => $ownerRole->id,
|
|
'status' => 'ACTIVE',
|
|
'isBookableOnline' => true,
|
|
]);
|
|
|
|
$invitation = StaffInvitation::create([
|
|
'businessId' => $business->id,
|
|
'staffId' => $staff->id,
|
|
'email' => $owner->email,
|
|
'token' => Str::random(64),
|
|
'invitedByUserId' => $invitedBy?->id,
|
|
'expiresAt' => now()->addDays(7),
|
|
]);
|
|
|
|
// Keep the local variable explicit so static analysers understand
|
|
// that the default staff role is intentionally provisioned too.
|
|
unset($barberRole);
|
|
|
|
return new BusinessProvisioningResult($business, $invitation);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param array<int, Permission> $permissions
|
|
*/
|
|
private function createRole(Business $business, string $name, array $permissions): Role
|
|
{
|
|
$role = Role::create(['businessId' => $business->id, 'name' => $name, 'isSystem' => true]);
|
|
|
|
foreach ($permissions as $permission) {
|
|
RolePermission::create(['roleId' => $role->id, 'permission' => $permission->value]);
|
|
}
|
|
|
|
return $role;
|
|
}
|
|
}
|