From da8d64045b852ae35b3a7f9008ecd37f182b7c78 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sat, 11 Jul 2026 21:12:52 +0000 Subject: [PATCH] Auto-provision Meet workspace for Sales Centre demo links. Create rep user, organization, and branch on first demo room request so sales reps do not need to onboard in Meet first. Co-authored-by: Cursor --- .../Api/V1/ServiceRoomController.php | 22 +++++- .../Meet/SalesCentreWorkspaceProvisioner.php | 77 +++++++++++++++++++ tests/Feature/MeetWebTest.php | 27 +++++++ 3 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 app/Services/Meet/SalesCentreWorkspaceProvisioner.php diff --git a/app/Http/Controllers/Api/V1/ServiceRoomController.php b/app/Http/Controllers/Api/V1/ServiceRoomController.php index b63f4ec..560b1c8 100644 --- a/app/Http/Controllers/Api/V1/ServiceRoomController.php +++ b/app/Http/Controllers/Api/V1/ServiceRoomController.php @@ -10,6 +10,7 @@ use App\Services\Integrations\WebhookDispatcher; use App\Services\Meet\CalendarService; use App\Services\Meet\InvitationService; use App\Services\Meet\RoomService; +use App\Services\Meet\SalesCentreWorkspaceProvisioner; use App\Services\Meet\SessionService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -25,12 +26,17 @@ class ServiceRoomController extends Controller protected InvitationService $invitations, protected CalendarService $calendar, protected WebhookDispatcher $webhooks, + protected SalesCentreWorkspaceProvisioner $salesCentreWorkspace, ) {} public function store(Request $request): JsonResponse { $validated = $this->validateRoomPayload($request); + if (($validated['source']['app'] ?? '') === 'sales_centre') { + $this->salesCentreWorkspace->ensureHost($validated); + } + $organization = $this->resolveOrganization($validated); abort_unless($organization->owner_ref === $validated['owner_ref'], 403); @@ -147,6 +153,8 @@ class ServiceRoomController extends Controller 'owner_ref' => [$required, 'string'], 'organization_id' => ['nullable', 'integer', 'exists:meet_organizations,id'], 'host_user_ref' => [$partial ? 'sometimes' : 'required', 'string'], + 'host_name' => ['nullable', 'string', 'max:255'], + 'host_email' => ['nullable', 'email', 'max:255'], 'title' => [$partial ? 'sometimes' : 'required', 'string', 'max:255'], 'description' => ['nullable', 'string'], 'type' => ['nullable', 'string', Rule::in(self::SERVICE_TYPES)], @@ -169,9 +177,17 @@ class ServiceRoomController extends Controller /** @param array $validated */ private function resolveOrganization(array $validated): Organization { - return isset($validated['organization_id']) - ? Organization::findOrFail($validated['organization_id']) - : Organization::owned($validated['owner_ref'])->firstOrFail(); + if (isset($validated['organization_id'])) { + return Organization::findOrFail($validated['organization_id']); + } + + if (($validated['source']['app'] ?? '') === 'sales_centre') { + $host = User::where('public_id', $validated['host_user_ref'])->firstOrFail(); + + return $this->salesCentreWorkspace->ensureOrganization($host); + } + + return Organization::owned($validated['owner_ref'])->firstOrFail(); } /** @param array $validated */ diff --git a/app/Services/Meet/SalesCentreWorkspaceProvisioner.php b/app/Services/Meet/SalesCentreWorkspaceProvisioner.php new file mode 100644 index 0000000..44ee4d4 --- /dev/null +++ b/app/Services/Meet/SalesCentreWorkspaceProvisioner.php @@ -0,0 +1,77 @@ + $validated + */ + public function ensureHost(array $validated): User + { + $publicId = (string) $validated['host_user_ref']; + + return User::updateOrCreate( + ['public_id' => $publicId], + [ + 'name' => trim((string) ($validated['host_name'] ?? '')) ?: 'Sales representative', + 'email' => $this->resolveEmail($publicId, $validated['host_email'] ?? null), + ], + ); + } + + public function ensureOrganization(User $host): Organization + { + $ref = $host->ownerRef(); + $existing = Organization::owned($ref)->first(); + + if ($existing) { + $this->organizations->ensureOwnerMember($host, $existing); + + return $existing; + } + + $label = trim($host->name) !== '' ? $host->name.' workspace' : 'Sales workspace'; + + $organization = Organization::create([ + 'owner_ref' => $ref, + 'name' => $label, + 'slug' => Str::slug($label).'-'.Str::lower(Str::random(4)), + 'timezone' => config('app.timezone', 'Africa/Accra'), + 'settings' => [ + 'onboarded' => true, + 'org_type' => 'sales', + 'provisioned_by' => 'sales_centre', + ], + ]); + + $this->organizations->ensureOwnerMember($host, $organization); + + Branch::create([ + 'owner_ref' => $ref, + 'organization_id' => $organization->id, + 'name' => 'Main', + 'is_active' => true, + ]); + + AuditLogger::record($ref, 'organization.created', $organization->id, $ref, Organization::class, $organization->id); + + return $organization; + } + + private function resolveEmail(string $publicId, mixed $email): string + { + $email = trim((string) $email); + + return $email !== '' ? $email : $publicId.'@users.ladill.com'; + } +} diff --git a/tests/Feature/MeetWebTest.php b/tests/Feature/MeetWebTest.php index 0e70879..c560a5a 100644 --- a/tests/Feature/MeetWebTest.php +++ b/tests/Feature/MeetWebTest.php @@ -530,6 +530,33 @@ class MeetWebTest extends TestCase ->assertJsonPath('room.title', 'Ladill demo with Prospect'); } + public function test_service_api_provisions_sales_centre_workspace_when_missing(): void + { + \Illuminate\Support\Facades\Http::fake(); + config(['meet.service_api_keys.sales_centre' => 'test-sales-centre-key']); + + $this->postJson('/api/service/v1/rooms', [ + 'owner_ref' => 'sales-rep-without-meet', + 'host_user_ref' => 'sales-rep-without-meet', + 'host_name' => 'Ama Sales', + 'host_email' => 'ama@sales.test', + 'title' => 'Ladill demo with Prospect', + 'type' => 'instant', + 'source' => ['app' => 'sales_centre', 'entity_type' => 'sales_lead', 'entity_id' => '99'], + 'invite_emails' => ['prospect@demo.test'], + ], ['Authorization' => 'Bearer test-sales-centre-key']) + ->assertCreated() + ->assertJsonPath('room.host_user_ref', 'sales-rep-without-meet'); + + $this->assertDatabaseHas('users', [ + 'public_id' => 'sales-rep-without-meet', + 'email' => 'ama@sales.test', + ]); + $this->assertDatabaseHas('meet_organizations', [ + 'owner_ref' => 'sales-rep-without-meet', + ]); + } + public function test_service_api_can_create_town_hall_room(): void { \Illuminate\Support\Facades\Http::fake();