Management UI
CI / ci (push) Canceled after 0s

This commit is contained in:
Stefan Lazic committed 2026-09-05 09:11:24 +01:00
1 parent cd6abeca6e
commit f11404c574
29 files changed
+876 -89

No files matched your search

@@ -0,0 +1,14 @@
<?php
namespace App\Services\Businesses;
use App\Models\Business;
use App\Models\StaffInvitation;
final readonly class BusinessProvisioningResult
{
public function __construct(
public Business $business,
public StaffInvitation $ownerInvitation,
) {}
}
@@ -0,0 +1,156 @@
<?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;
}
}