Meetings — Developer Guide
Status: Implemented
Required dependency: Calendar. Meetings owns booking, host, attendees, providers, and reminders; Calendar owns the projected event.
Ownership
| Concept | Owner |
|---|---|
meetings, attendees, reminders, provider connections | Meetings module |
| Zoom / Google Meet OAuth + remote meeting sync | Meetings module |
Calendar projection (source=meeting) | CalendarEventService::upsertFromSource |
Domain
App\Models\Meeting—BelongsToTenant,LogsActivity,SoftDeletesMeetingAttendee— internaluser_idand/or externalemail/nameMeetingProviderConnection— encrypted tokens, one connection per provider per tenantMeetingReminder— single durable reminder row per meeting
Enums: MeetingStatusEnum, MeetingProviderEnum, MeetingAttendeeRoleEnum, MeetingProviderConnectionStatusEnum, MeetingReminderStatusEnum, MeetingProviderSyncStatusEnum.
Morph alias: meeting → App\Models\Meeting (registered in AppServiceProvider).
Timezone
Follow the platform Workspace timezone convention: one Settings → General timezone for wall clocks; no per-module timezone.
starts_at / ends_at / cancelled_at / completed_at / reminder remind_at use App\Casts\UtcDateTime (DB = UTC instants). MeetingResource serializes them with App\Support\UtcIso. The SPA form locks timezone to the workspace setting (falling back to the meeting’s stored timezone when Settings are still on the UTC shell default) and converts wall-clock ↔ UTC via src/lib/datetime.ts. List/detail formatters pass that same resolved timezone into formatAppDateTime.
Service contract
App\Services\Tenant\MeetingService is the sole writer:
- CRUD, complete, cancel, assign host, attendee sync
completePastMeetings()for scheduler auto-complete- Provider sync via
MeetingProviderRegistry(none/ Zoom / Google Meet; fake drivers in testing) - Calendar projection / cancel / delete (meeting
completedmaps to calendarscheduled) - Reminder schedule / reschedule / cancel
Status transitions: scheduled → completed (manual or auto) or cancelled. Completed/cancelled meetings are not mutable.
Providers
- Interface:
App\Contracts\Meetings\MeetingProviderInterface(authorize, exchange, refreshAccessToken, create/update/delete) - Registry:
App\Services\Meetings\MeetingProviderRegistry - Per-tenant BYOK: each workspace stores its own OAuth
client_id/client_secret/ optionalwebhook_secret(encrypted) onMeetingProviderConnection - OAuth account connect:
MeetingIntegrationController(meetings.manage_integrations) with one-time cache nonce instate - Platform-hosted callback:
GET /api/oauth/meetings/{provider}/callback(tenant + nonce from encryptedstate; re-checksmanage_integrations) - Access tokens refresh automatically near expiry (Zoom + Google)
- Google Meet creates a Meet space join link (not a Google Calendar conference); update is a no-op; delete removes the space when possible
- Webhooks:
MeetingWebhookControlleris a stub (custom HMAC ack only — not Zoom/Google-native event ingestion). Do not rely on it for production reconciliation yet. - Retry:
RetryProviderSyncJob(tenant id + Laravel$tries/$backoff; does not re-dispatch forever fromsyncProvider)
Config: config/meetings.php (fake flag + Google/Zoom OAuth scopes + callback path only — no platform client secrets). Fake drivers are forced off in production. Prefer fail-closed on create when provider ≠ none and account is not connected. Remote provider delete on cancel/delete is best-effort (local status always wins; failures land on sanitized provider_sync_*). Manual retry: POST /meetings/{id}/retry-sync.
Notifications
| Type | Channels |
|---|---|
meeting.invite / meeting.updated / meeting.cancelled | mail + database + broadcast + web push (users); mail-only for external guests |
meeting.reminder | same; dispatched by crm:send-due-notifications |
Subscriber: MeetingEventSubscriber (audit + notifications; skips actor for invite/update/cancel). MeetingCompleted is audit-only (no attendee email).
Scheduler
crm:send-due-notifications(every 5 minutes,withoutOverlapping,onOneServer) claims dueMeetingReminderrows atomically (pending→sending→sent) withNotificationIdempotencyfor users and cache dedupe for external guest mail. Only reminders forscheduledmeetings are sent.meetings:auto-complete(every 5 minutes,withoutOverlapping,onOneServer) marksscheduledmeetings withends_at <= now()ascompleted.
Frontend
| Piece | Location |
|---|---|
| Page | src/pages/meetings/meetings-page.tsx |
| Form / detail / integrations | meeting-form-dialog.tsx, meeting-detail-sheet.tsx, meeting-integrations-panel.tsx |
| API | meetingService in src/api/services.ts |
| Notifications | src/notifications/modules/meetings.ts |
| E2E | e2e/tests/meetings/, npm run test:e2e:meetings |
Provider options in the form are disabled until Integrations reports connected. Calendar projections link back to Meetings and are read-only on the Calendar sheet.
Permissions
| Role | Grants |
|---|---|
| admin | all including view_all, assign_host, manage_integrations |
| manager | view/create/update/view_all/assign_host (no delete / manage_integrations) |
| staff | view/create/update/delete (scoped); no view_all / manage_integrations |
Registration (migrate-only)
2026_07_22_000000_create_meetings_tables.php2026_07_22_000001_register_meetings_module.php2026_07_22_000002_add_meetings_permissions.php2026_07_22_000003_add_meetings_calendar_dependency.php
Also listed in CatalogSeeder for fresh/local/CI.
Tests
- Pest:
tests/Feature/Tenant/Meeting/* - Playwright:
npm run test:e2e:meetings
Explicit non-goals (current)
- Per-user personal Zoom/Google accounts
- Multiple reminders per meeting
- Changing Calendar ACLs so invitees see projected events (Meetings list remains invitee source of truth)