Auto-provision Meet workspace for Sales Centre demo links.
Deploy Ladill Meet / deploy (push) Successful in 1m38s

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 <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-11 21:12:52 +00:00
co-authored by Cursor
parent d14f8a4b25
commit da8d64045b
3 changed files with 123 additions and 3 deletions
@@ -10,6 +10,7 @@ use App\Services\Integrations\WebhookDispatcher;
use App\Services\Meet\CalendarService; use App\Services\Meet\CalendarService;
use App\Services\Meet\InvitationService; use App\Services\Meet\InvitationService;
use App\Services\Meet\RoomService; use App\Services\Meet\RoomService;
use App\Services\Meet\SalesCentreWorkspaceProvisioner;
use App\Services\Meet\SessionService; use App\Services\Meet\SessionService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -25,12 +26,17 @@ class ServiceRoomController extends Controller
protected InvitationService $invitations, protected InvitationService $invitations,
protected CalendarService $calendar, protected CalendarService $calendar,
protected WebhookDispatcher $webhooks, protected WebhookDispatcher $webhooks,
protected SalesCentreWorkspaceProvisioner $salesCentreWorkspace,
) {} ) {}
public function store(Request $request): JsonResponse public function store(Request $request): JsonResponse
{ {
$validated = $this->validateRoomPayload($request); $validated = $this->validateRoomPayload($request);
if (($validated['source']['app'] ?? '') === 'sales_centre') {
$this->salesCentreWorkspace->ensureHost($validated);
}
$organization = $this->resolveOrganization($validated); $organization = $this->resolveOrganization($validated);
abort_unless($organization->owner_ref === $validated['owner_ref'], 403); abort_unless($organization->owner_ref === $validated['owner_ref'], 403);
@@ -147,6 +153,8 @@ class ServiceRoomController extends Controller
'owner_ref' => [$required, 'string'], 'owner_ref' => [$required, 'string'],
'organization_id' => ['nullable', 'integer', 'exists:meet_organizations,id'], 'organization_id' => ['nullable', 'integer', 'exists:meet_organizations,id'],
'host_user_ref' => [$partial ? 'sometimes' : 'required', 'string'], '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'], 'title' => [$partial ? 'sometimes' : 'required', 'string', 'max:255'],
'description' => ['nullable', 'string'], 'description' => ['nullable', 'string'],
'type' => ['nullable', 'string', Rule::in(self::SERVICE_TYPES)], 'type' => ['nullable', 'string', Rule::in(self::SERVICE_TYPES)],
@@ -169,9 +177,17 @@ class ServiceRoomController extends Controller
/** @param array<string, mixed> $validated */ /** @param array<string, mixed> $validated */
private function resolveOrganization(array $validated): Organization private function resolveOrganization(array $validated): Organization
{ {
return isset($validated['organization_id']) if (isset($validated['organization_id'])) {
? Organization::findOrFail($validated['organization_id']) return Organization::findOrFail($validated['organization_id']);
: Organization::owned($validated['owner_ref'])->firstOrFail(); }
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<string, mixed> $validated */ /** @param array<string, mixed> $validated */
@@ -0,0 +1,77 @@
<?php
namespace App\Services\Meet;
use App\Models\Branch;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Support\Str;
class SalesCentreWorkspaceProvisioner
{
public function __construct(
protected OrganizationResolver $organizations,
) {}
/**
* @param array<string, mixed> $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';
}
}
+27
View File
@@ -530,6 +530,33 @@ class MeetWebTest extends TestCase
->assertJsonPath('room.title', 'Ladill demo with Prospect'); ->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 public function test_service_api_can_create_town_hall_room(): void
{ {
\Illuminate\Support\Facades\Http::fake(); \Illuminate\Support\Facades\Http::fake();