Files
ladill-meet/app/Http/Controllers/Api/V1/ServiceRoomController.php
T
isaaccladandCursor da8d64045b
Deploy Ladill Meet / deploy (push) Successful in 1m38s
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 <cursoragent@cursor.com>
2026-07-11 21:12:52 +00:00

306 lines
11 KiB
PHP

<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\Organization;
use App\Models\Room;
use App\Models\User;
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;
use Illuminate\Validation\Rule;
class ServiceRoomController extends Controller
{
private const SERVICE_TYPES = ['instant', 'scheduled', 'town_hall', 'webinar'];
public function __construct(
protected RoomService $rooms,
protected SessionService $sessions,
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);
$roomUuid = trim((string) ($validated['meet_room_uuid'] ?? ''));
if ($roomUuid !== '') {
$existing = Room::query()
->where('uuid', $roomUuid)
->where('owner_ref', $validated['owner_ref'])
->first();
if ($existing) {
return $this->respondWithRoom(
$this->persistRoomUpdate($existing, $validated, $organization),
);
}
}
$entityId = (string) ($validated['source']['entity_id'] ?? '');
if ($entityId !== '') {
$existing = Room::query()
->forSource((string) $validated['source']['app'], $entityId, $validated['owner_ref'])
->first();
if ($existing) {
return $this->respondWithRoom(
$this->persistRoomUpdate($existing, $validated, $organization),
);
}
}
return $this->respondWithRoom(
$this->persistRoomCreate($validated, $organization),
201,
);
}
public function lookup(Request $request): JsonResponse
{
$validated = $request->validate([
'source_app' => ['required', 'string'],
'entity_id' => ['required', 'string'],
'owner_ref' => ['nullable', 'string'],
]);
$room = Room::query()
->forSource(
$validated['source_app'],
$validated['entity_id'],
$validated['owner_ref'] ?? null,
)
->first();
if (! $room) {
return response()->json(['error' => 'Room not found.'], 404);
}
return $this->respondWithRoom($room);
}
public function show(Room $room): JsonResponse
{
return $this->respondWithRoom($room->load('sessions'));
}
public function update(Request $request, Room $room): JsonResponse
{
$validated = $this->validateRoomPayload($request, partial: true);
abort_unless($room->owner_ref === ($validated['owner_ref'] ?? $room->owner_ref), 403);
$organization = isset($validated['organization_id'])
? Organization::findOrFail($validated['organization_id'])
: $room->organization;
return $this->respondWithRoom(
$this->persistRoomUpdate($room, $validated, $organization),
);
}
public function cancel(Request $request, Room $room): JsonResponse
{
$validated = $request->validate([
'owner_ref' => ['required', 'string'],
'host_user_ref' => ['nullable', 'string'],
]);
abort_unless($room->owner_ref === $validated['owner_ref'], 403);
$host = User::where('public_id', $validated['host_user_ref'] ?? $room->host_user_ref)->first();
$this->rooms->cancel($room, $validated['owner_ref'], $host);
return $this->respondWithRoom($room->fresh());
}
public function start(Request $request, Room $room): JsonResponse
{
$host = User::where('public_id', $request->input('host_user_ref', $room->host_user_ref))->firstOrFail();
abort_unless($room->host_user_ref === $host->ownerRef(), 403);
$session = $this->sessions->start($room, $host);
return response()->json([
'session' => $session,
'room_url' => route('meet.room', $session),
'join_url' => $room->joinUrl(),
]);
}
/** @return array<string, mixed> */
private function validateRoomPayload(Request $request, bool $partial = false): array
{
$required = $partial ? 'sometimes' : 'required';
return $request->validate([
'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)],
'scheduled_at' => ['nullable', 'date'],
'duration_minutes' => ['nullable', 'integer', 'min:15', 'max:480'],
'timezone' => ['nullable', 'timezone'],
'passcode' => ['nullable', 'string', 'max:20'],
'settings' => ['nullable', 'array'],
'source' => [$partial ? 'sometimes' : 'required', 'array'],
'source.app' => [$partial ? 'sometimes' : 'required', 'string'],
'source.entity_type' => ['nullable', 'string'],
'source.entity_id' => ['nullable', 'string'],
'source.event_id' => ['nullable', 'string'],
'invite_emails' => ['nullable', 'array'],
'invite_emails.*' => ['email'],
'meet_room_uuid' => ['nullable', 'string', 'max:64'],
]);
}
/** @param array<string, mixed> $validated */
private function resolveOrganization(array $validated): Organization
{
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<string, mixed> $validated */
private function persistRoomCreate(array $validated, Organization $organization): Room
{
$host = User::where('public_id', $validated['host_user_ref'])->firstOrFail();
$type = $this->resolveType($validated);
$payload = array_merge($validated, [
'type' => $type,
'status' => 'scheduled',
'settings' => $this->mergeSettingsForType($type, $validated['settings'] ?? []),
]);
$room = $type === 'instant' && empty($validated['scheduled_at'])
? $this->rooms->createInstant($host, $organization, $payload)
: $this->rooms->create($host, $organization, $payload);
$this->afterRoomPersisted($room, $validated, $host);
return $room->fresh();
}
/** @param array<string, mixed> $validated */
private function persistRoomUpdate(Room $room, array $validated, Organization $organization): Room
{
$hostRef = $validated['host_user_ref'] ?? $room->host_user_ref;
$host = User::where('public_id', $hostRef)->firstOrFail();
$update = array_filter([
'title' => $validated['title'] ?? null,
'description' => array_key_exists('description', $validated) ? $validated['description'] : null,
'scheduled_at' => $validated['scheduled_at'] ?? null,
'duration_minutes' => $validated['duration_minutes'] ?? null,
'timezone' => $validated['timezone'] ?? null,
'passcode' => array_key_exists('passcode', $validated) ? $validated['passcode'] : null,
'host_user_ref' => $hostRef,
'source' => $validated['source'] ?? null,
], fn ($value) => $value !== null);
if (isset($validated['settings'])) {
$type = $this->resolveType(array_merge(['type' => $room->type], $validated));
$update['settings'] = $this->mergeSettingsForType(
$type,
array_merge($room->settings ?? [], $validated['settings']),
);
}
$room = $this->rooms->update($room, $update, $host);
$this->afterRoomPersisted($room, $validated, $host);
return $room;
}
/** @param array<string, mixed> $validated */
private function afterRoomPersisted(Room $room, array $validated, User $host): void
{
if (! empty($validated['invite_emails'])) {
$invites = collect($validated['invite_emails'])->map(fn ($email) => ['email' => $email])->all();
$this->invitations->inviteToRoom($room, $invites, $host);
}
if ($room->scheduled_at) {
$this->calendar->syncRoomCreated($room, $host);
}
}
/** @param array<string, mixed> $validated */
private function resolveType(array $validated): string
{
if (! empty($validated['type'])) {
return (string) $validated['type'];
}
return empty($validated['scheduled_at']) ? 'instant' : 'scheduled';
}
/** @param array<string, mixed> $settings */
private function mergeSettingsForType(string $type, array $settings): array
{
$defaults = match ($type) {
'town_hall' => [
'waiting_room' => true,
'mute_on_join' => true,
'green_room' => true,
'stage_layout' => 'plenary',
'stage_mode' => true,
'panel_discussions' => false,
'panels' => [],
'presenter_refs' => [],
],
'webinar' => [
'waiting_room' => true,
'mute_on_join' => true,
'webinar_auto_approve' => false,
'stage_layout' => 'plenary',
'stage_mode' => true,
'panel_discussions' => false,
'panels' => [],
],
default => [],
};
return array_merge(config('meet.default_settings', []), $defaults, $settings);
}
private function respondWithRoom(Room $room, int $status = 200): JsonResponse
{
return response()->json([
'room' => $room->loadMissing('sessions'),
'join_url' => $room->joinUrl(),
'embed_url' => route('meet.join', $room),
], $status);
}
}