Auto-provision Meet workspace for Sales Centre demo links.
Deploy Ladill Meet / deploy (push) Successful in 1m38s
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:
@@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user