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();